实现一个长表格控件 ---- 自定义LayoutManager学习

RecyclerView 源码学习(二):一步一步自定义LayoutManager 前言这篇文章实现了一个简单的LayoutManager,重点在于从原理方面一步一步去了解如何自定义一个LayoutManager。麻雀虽小,但五脏俱全。从自定义LayoutManager的layout布局,到scroll滑动,再到添加简单的动画效果。 其实,自定义一个LayoutManager也没那么难。基本概念RecyclerLayoutManafger调用 getViewForPosition 阅读详情

Android 学习之路从自定义View,自定义ViewGroup走到现在,难度越来越大,当然也能学到很多东西,而自定义RecyclerView的LayoutManager既是对之前所了解的View绘制机制\事件传递机制的巩固,也是一种提升。

Google自定义了一个ViewGroup叫做RecyclerView,却不走寻常路,把这个ViewGroup的测量和布局交给另一个类完成,为什么要这样做?能带来什么好处?到底它是怎么做到这一点的?疑问那么多,与其上网搜索答案,不如自己动手实现一个LayoutManager,探索其中的原理。

首先对于自定义LayoutManager推荐两篇文章:

自定义LayoutManager实现流式布局

创建一个 RecyclerView的LayoutManager - 第一部分

我也是看了这两篇文章,对自定义LayoutManager有了初步的了解,接下来就对我的自定义之旅做一个记录:

先思考怎么测量、布局:
我的目的是实现一个横竖都可以滚动的RecyclerView,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-otmGTUzZ-1631676335536)(http://note.youdao.com/yws/api/personal/file/1084212577CB4B5D92E715561ABFC815?method=download&shareKey=27505de4065957d0d3be828b268e2587)]

我们需要自定义一个SingleLinearLayout,这个ViewGroup非常简单,它不限制子View的大小,也就是它的MeasureMode是UNSPECIFIED,它多高,多宽,全凭子View决定,这样设计的原因在于RecyclerView默认传递给子View的Mode是AT_MOST,Size给的是0,这样如果SingleLinearLayout直接把这些参数传递给TextView,它算出来的宽高就是0,这明显不是我们想要的,我们希望TextView不要有压力,根据内部数据算出多少就是多少。然后根据算出的宽高,把每个TextView都横向layout一排。

图上可以看出,我们的控件实际上比屏幕大得多,因此我们的LayoutManager需要支持横向和纵向的滚动,不过在此之前,我们还需要将每行的SingleLinearLayout给布局到RecyclerView上,这就是onLayoutChildren方法要完成的任务了。

要完成LayoutManager是非常复杂的,它的可定制性太高,所以Google强调,只要我们能完成需求即可,不要过度设计。这点查看LinearLayoutManager可看到足有两千行,方方面面都要考虑到。我们为了简化问题,先从最简单的考虑:
如果我们不考虑回收,实现onLayoutChildren非常简单,如同我们自定义一个普通的ViewGroup,把一个一个的子View都竖直摆放就行;但我们没必要布局不在屏幕中显示的View,那就当发现子View摆到了屏幕的末尾,就停止布局过程:

minPos = 0;
lastVisPost = getItemCount() - 1;
offsetTop = 0;
 // 填充View
 for (int i = minPos; i <= lastVisPos; i++) {
     View child = recycler.getViewForPosition(i);
     addView(child);
     measureChild(child,0,0);
     ...
     if (offsetTop - dy > getHeight()) {
         // 到了屏幕的末尾 退出布局
         removeAndRecycleView(child,recycler);
         lastVisPos = i - 1;
     } else {
         int w = getDecoratedMeasuredWidth(child);
         int h = getDecoratedMeasuredHeight(child);
         offsetTop+=h;
         ...
         // 布局到RV上
         layoutDecorated(child,aRect.left,aRect.top,aRect.right,aRect.bottom);
     }
 }

但是RecyclerView还必须能回收掉不在屏幕中的View,这里我们可以参考上面说到的博客,把回收View做成一个独立的过程,虽然会带来一些冗余的计算,但比其它的一些自定义LayoutManager复杂的滑动回收计算简洁很多:

 //回收越界子View
 if (getChildCount() > 0) {//滑动时进来的
     for (int i = getChildCount() - 1; i >= 0; i--) {
         View child = getChildAt(i);
         if (dy > 0) {//需要回收当前屏幕,上越界的View
             if (getDecoratedBottom(child) < offsetTop) {
                 removeAndRecycleView(child,recycler);
                 firstVisPos++;
             }
         } else if (dy < 0) {//回收当前屏幕,下越界的View
             if (getDecoratedTop(child) > getHeight()) {
                 removeAndRecycleView(child,recycler);
                 lastVisPos--;
             }
         }
     }
 }

回收原理很简单,这里不再赘述。但是LayoutManager的重头戏在于滑动,滑动时候能动态的添加和回收View,如何完成滑动过程?首先滑动一定是一个不断对子View重新布局的过程,所以我们肯定要把onLayoutChildren方法中的代码抽一部分做成函数(fill方法)以供滑动调用,其次我们还必须记录垂直滑动和水平滑动的位移以判断滑动范围,在下滑时对子view的位置信息做一个保存,上滑时利用保存的位置信息恢复View的位置,同时考虑水平位移和垂直位移对位置信息的影响,fill时减去相应的位移。思路就是这样:在scrollVerticallyBy和scrollHorizontallyBy记录位移信息,同时判断是否超过滑动范围,然后在fill()中根据子View的位置和位移距离重新布局。

  @Override
    public int scrollVerticallyBy(
            int dy,RecyclerView.Recycler recycler,RecyclerView.State state) {
        if (dy == 0 || getChildCount() == 0) {
            return 0;
        }
        
        int realOffset = dy;

        View topView = getChildAt(0);
        View bottomView = getChildAt(getChildCount() - 1);

        ...

        if (verticalOffset + realOffset < 0) {
            //下划到了顶部
            realOffset = -verticalOffset;
        } else if (realOffset > 0) { //是否下滑到了底部
            //利用最后一个子View比较修正
            if (getPosition(bottomView) == getItemCount() - 1) {
                int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(bottomView);
                if (gap > 0) {
                    realOffset = -gap;
                } else if (gap == 0) {
                    realOffset = 0;
                } else {
                    realOffset = Math.min(realOffset,-gap);
                }
            }
        }
        //布局 , 并且吃掉多余的滑动
        realOffset = fill(recycler,state,realOffset);
        
        verticalOffset += realOffset;
        offsetChildrenVertical(-realOffset);

        return realOffset;
    }
//横向滑动不考虑回收和布局的,简单多了
 @Override
    public int scrollHorizontallyBy(
            int dx,RecyclerView.Recycler recycler,RecyclerView.State state) {
        View aView = getChildAt(0);

        ...
        
        if (horizontalOffset + dx > aViewWidth - getWidth()) {
            dx = 0;
        } else if (horizontalOffset + dx <= 0) {
            dx = 0;
        }

        horizontalOffset += dx;
        offsetChildrenHorizontal(-dx);
        
        return dx;
    }
...
//回收View
...
// fill 的部分代码

  if (dy >= 0) {   // 下滑 或者 初次进入
            int minPos = firstVisPos;
            lastVisPos = getItemCount() - 1;
            if (getChildCount() > 0) { // 下滑
                View lastView = getChildAt(getChildCount() - 1);
                minPos = getPosition(lastView) + 1;   //从最后一个 View + 1 开始
                offsetTop = getDecoratedBottom(lastView);
            }

            // 填充View
            for (int i = minPos; i <= lastVisPos; i++) {
                View child = recycler.getViewForPosition(i);
                addView(child);
                measureChild(child,0,0);
               
                if (offsetTop - dy > getHeight()) {
                    // 到了屏幕的末尾 退出布局
                    removeAndRecycleView(child,recycler);
                    lastVisPos = i - 1;
                } else {
                    int w = getDecoratedMeasuredWidth(child);
                    int h = getDecoratedMeasuredHeight(child);

                    //记录View的位置信息
                    Rect aRect = mItemAnchorMap.get(i);
                    if (aRect == null) {
                        aRect = new Rect();
                    }
                    //注意水平位移影响
                    aRect.set(-horizontalOffset,offsetTop,-horizontalOffset + w,offsetTop + h);
                    mItemAnchorMap.put(i,aRect);
                    
                    offsetTop += h;
                    
                    // 布局到RV上
                    layoutDecorated(child,aRect.left,aRect.top,aRect.right,aRect.bottom);
                }
            }
            
            //添加完后,判断是否已经没有更多的ItemView,并且此时屏幕仍有空白,则需要修正dy
            View lastChild = getChildAt(getChildCount() - 1);
            if (getPosition(lastChild) == getItemCount() - 1) {
                int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);
                if (gap > 0) {
                    dy -= gap;
                }
            }
            
        } else {
            //上滑 , 通过mItemAnchorMap 拿到布局信息
            int maxPos = getItemCount() - 1;
            firstVisPos = 0;
            if (getChildCount() > 0) {
                View firstView = getChildAt(0);
                maxPos = getPosition(firstView) - 1;
            }

            for (int i = maxPos; i >= firstVisPos; i--) {
                Rect aRect = mItemAnchorMap.get(i);
                if (aRect != null) {
                    if (aRect.bottom - verticalOffset - dy < 0) {
                        firstVisPos = i + 1;
                        break;
                    } else {
                        View child = recycler.getViewForPosition(i);
                        addView(child,0);
                        measureChild(child,0,0);
                        
                        //修正水平、垂直位移影响 
                        layoutDecorated(child,aRect.left - horizontalOffset,
                                        aRect.top - verticalOffset,aRect.right - horizontalOffset,
                                        aRect.bottom - verticalOffset);
                    }
                }
            }
        }

思路有了代码实现起来就不是很难,到此,这个能容纳大表格的RecyclerView就已经完成了,我们并没有动RecyclerView的任何函数,却通过LayoutManager完成了一个全新的ViewGroup,难怪alibaba开源了一个项目-----vlayout ,只通过自定义LayoutManager就实现了现有的ViewGroup并且有很多扩展。

通过自己的实验,亦对开篇提出的三个问题有了一定的认识:RecyclerView最大的特点就是适宜大量View的展现,回收不必要的View防止内存溢出,为此,拆分回收机制和布局机制,利于解耦,方便理解。正如上面实现的LayoutManager对回收和布局分开处理,也是这一思想的体现。有机会可以再深入阅读下RecyclerView的源码,搞懂preLayout和RecyclerView两次调用onLayoutChildren的原因,顺便附上源码,以及自己查阅源码的过程中,关于auto-measure过程的翻译:

gitHub地址


自动绘制的工作流程如下:

  1. LayoutManager应该调用setAutoMeasureEnabled(true)来开启它,注意所有framework中的LayoutManager都使用了自动绘制。
  2. 当RecyclerView的onMeasure被调用时,如果提供给它的measureMode是Exactly模式,RecyclerView将只会调用LayoutManager的onMeasure方法并不做任何布局计算直接返回。
  3. 如果RV的layoutwidth或layoutHeight中的一个measureMode不是Exactly,RecyclerView将开启layout过程的onMeasure(),它将处理所有挂起的Adapter的数据更新并且决定是否运行pre-layout过程。如果RecyclerView决定这样做,它首先调用onLayoutChilden(),参数中的state.isPreLayout将返回True。在这个阶段,getWidth()和getHeight()将仍返回RecyclerView最后一次的layout计算过程得到的宽高。
  4. 完成了pre-layout过程后,RecyclerView将调用onLayoutChildren(),此时isMeasuring返回true,isPreLayout返回false。LayoutManager此时能访问getWidth()/getHeight()和getWidthMode()/getHeightMode()。在layout计算完成后,RV利用children和自己的padding设置自己的绘制宽高,LayoutManager能重写setMeasuerdDimension()来处理不同的情况。例如:GridLayoutManager重写这个方法去处理这种case: 如果它是垂直布局且有三列,但是实际只有2列,它仍然以3列测量它。
  5. 在测量过程之后,RV将运行onLayoutChildren且isMeasuring为true,isPreLayout为false.RV将处理每个view的实际添加、删除、移动、改变动画,所以LM不应该担心怎么在onLayoutChildren调用的最后一次时去处理它们。
  6. 当测量完成,RecyclerView的onLayout将会被调用,RecyclerView检查布局计算是否在测量阶段已经完成,如果完成,他将重用那些信息,它可能仍然决定调用onLayoutChildren(),这种情况出现在最后一次的measureSpec不同于最后的dimensions或者在测量和布局过程中adapter的内容有所改变,最后,动画将被计算和运行。
Android强行进阶,自定义控件—LayoutManager,flutter中文教程 4、勇朝陈的Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager blog.youkuaiyun.com/ccy0122/art… 这几篇文章针对自定义LayoutManager的误区、注意事项,分析的非常到位,来来回回我看了好几篇,希望对你有所帮助。 自定义LayoutManager基本流程 让Items显示出来 我们在自定义ViewGroup中,想要显示子View,无非就三件事: 添加 通过addView方法把子View添加进ViewGroup或直接在xml中直接添加 阅读详情

相关推荐

【Android】掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。

转载请标明出处: http://blog.youkuaiyun.com/zxt0601/article/details/52948009 本文出自:【张旭童的博客】 本系列文章相关代码传送门: 自定义LayoutManager实现的流式布局 欢迎star,pr,issue。本系列文章目录: 深入掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。 深

zxt0601的博客 3万+

Android自定义LayoutManager第十一式之飞龙在天

Android自定义LayoutManager,轻松实现各种炫酷、特殊效果,再也不怕产品经理为难!

陈小缘的博客 4万+

阿里面试官:关于RecyclerView做下分享

之前看到个来面试阿里的小伙伴在自己的个人能力中写到: 更多面试内容,技术干货,技术交流: star一下我的GitHub 熟悉Java的多线程编程,熟悉Android下线程与线程,进程与进程间的通信机制,熟悉Android机型的适配、UI布局优 化等,能独立开发Android应用; 熟悉自定义控件,对ViewGroup 底层测量,绘制,渲染有深刻的了解,并且自己借鉴了RecyclerView的回收思想,设 计了表格控件 熟悉Framework层API,了解Handler,Messag

weixin_48012619的博客 491

Android控件RecyclerView(二)——LayoutManager及其自定义

目录 前言 1 常用LayoutManager 1.1 LinearLayoutManager 1.2 GridLayoutManager 1.3 StaggeredGridLayoutManager 1.4 FlexboxLayoutManager 2 自定义LayoutManager 2.1 创建自定义LayoutManager类 2.2 绘制RecyclerView子Vie...

xhuww的博客 2万+

自定义控件三部曲视图篇(六)——RecyclerView系列之三自定义LayoutManager

前言:把握生命里的每一分钟,全力以赴我们心中的梦,不经历风雨 怎么见彩虹,没有人能随随便便成功 在第一篇中已经讲过,LayoutManager主要用于布局其中的Item,在LayoutManager中能够对每个Item的大小,位置进行更改,将它放在我们想要的位置,在很多优秀的效果中,都是通过自定义LayoutManager来实现的,比如: 可以看到这些效果都非常棒,通过这一节的学习,大...

启舰 1万+

Android 一个RecyclerView的双向滑动表格控件实现--FormLayoutManager

前言 为什么会有这篇文章诞生呢?本人工作以为一直没做个表格控件的需求,直到最近的新公司,要做一个表格的需求。一开始遇到需求,自己脑海就先构思了一下思路。当然也会想大家一样百度谷歌看看大家普遍是怎么做的。一百度,我就惊讶了。 怎么大家都是recyclerview或listview外面一层套一个HorizontalScrollView来实现表格。我就想为啥不一个recyclerview用一个F...

DNWalter的博客 4748

Android LayoutManager高端玩家,实现花式表格!

其核心是二层RecyclerView的嵌套,如果只用一层RecyclerView将会带来性能的提升多个RecyclerView有的时候会导致界面变形在我深入学习以后,想能不能只用一层,借助实现,写着写着,发现该思路可行,并实现了一款基于一个的表格控件TableView。

2401_85730375的博客 854

FormLayoutManager -- 教你如何在原有的基础上,进行动态改变表格的列数或列顺序

FormLayoutManager首页,里面有github地址 前言 我Github的库上面有有个issue说我那个库不能动态改变表格的列数ColumnCount。我只能说我也非善男信女,接下来的话可能会引起不适。我想说你是有多蠢,是不是没脑子,怎么当程序员的(▼皿▼#) 把对应的部分弄成个变量,不就可以动态了吗。 也可能我太上帝视角了,也许只有我自己想到,别人没想到呢!那我就在demo新增了一个界面,告诉大家我是怎样去实现动态改列数和列顺序的。 改造你的adapter demo里面可参考.

DNWalter的博客 408

LayoutManager高端玩家,实现花式表格

本文的项目也是学习自定义LayoutManager绝佳资料。 1前言 表格是自打我进公司以后就使用的控件,起初使用的是ScrollablePanel,从一开始的被花式吊打,到后期的熟练使用。 https://github.com/Kelin-Hong/ScrollablePanel 大佬写的控件确实给我的工作带来了极大的方便,不过还是有些问题存在: 无法实现不规则的表格 其核心是二层Re...

Android 开发架构 488

Android-RecyclerView自定义LayoutManager

RecyclerView自定义LayoutManager

EndNote20保姆级安装教程:从下载到汉化一步到位(附闪退解决方案)

本文提供EndNote20的详细安装与汉化教程,涵盖系统准备、分步安装、路径优化及闪退解决方案。针对科研人员的需求,特别优化了中文环境下的使用体验,确保文献管理工具的高效稳定运行。

weixin_30415113的博客 339

TableRecyclerView:打造高效灵活的大表格控件

TableRecyclerView:打造高效灵活的大表格控件 在移动应用开发中,表格控件是展示复杂数据的重要工具。然而,传统的表格控件往往难以应对大数据量和高性能的需求。今天,我们向大家推荐一款高效、灵活的大表格控件——TableRecyclerView,它基于LayoutManager实现,能够轻松应对复杂的数据展示需求。 项目介绍 TableRecyclerView是一款基于RecyclerV...

gitblog_00606的博客 669

ANSYS Workbench瞬态结构分析(1)时域分析

本文围绕瞬态结构分析展开。首先阐释其概念与理论基础,涵盖控制方程及数值求解方法。接着详述在 ANSYS Workbench 中进行瞬态分析的操作流程,以音叉为例,介绍从创建分析系统、材料定义、几何处理与网格划分,到边界条件与载荷设置、求解器设置及结果后处理的各步骤。之后对分析结果进行探讨,并针对计算不收敛、计算时间过长等常见问题给出调试技巧,旨在为读者提供全面的瞬态结构分析知识与实践指导。

qq_58675332的博客 6530

RecyclerView系列之三自定义LayoutManager

自定义layoutmanager

QG 1883

shopro 商城,2025年新申请的微信支付提示失败,支付提示 Get Wechat Public Cert Error 问题的修复办法

shopro 商城,最近新申请的微信支付商户 支付提示 Get Wechat Public Cert Error 问题的修复办法

行走的卡路里 1245

菲涅尔波带片

菲涅尔波带片的简介,以及matlab绘制

Bestchinese的博客 1万+

Android 自定义LayoutManager

Android 自定义LayoutManager 实现自定义LayoutManager主要的4个步骤:  指定默认的LayoutParams 计算每个ItemView的位置  添加滑动事件 实现缓存 其中,主要在onLayoutChildern() 这个回调方法中实现主要功能。 指定默认的 LayoutParams         当你继承LayoutManager之后,必须要重写g...

m0_37218227的博客 1294

Java黑皮书课后题第5章:*5.9(找出得最高分的前两个学生)编写程序,提示用户输入学生的个数、每个学生名字及分数,最后显示获得最高分的学生

*5.9(找出得最高分的前两个学生)编写程序,提示用户输入学生的个数、每个学生名字及分数,最后显示获得最高分的学生名字、次高分的学生名字题目题目概述next()读取一个字符与nextLine()读取一行破题代码 题目 题目概述 *5.9(找出得最高分的前两个学生)编写程序,提示用户输入学生的个数、每个学生名字及分数,最后显示获得最高分的学生名字、次高分的学生名字 使用Scanner类的next()方法而不是nextLine()方法来读取名字 next()读取一个字符与nextLine()读取一行 举个例子理

weixin_46356698的博客 2154

Android 自定义LayoutManager

这个东西花哨的特别难,简单的特别简单,Github有一堆, 今天具体了解一下,方便以后有需求 ,找到相近的效果,知道在哪里修改一些参数就可以了 首先是自定义一个extens LayoutManager public class CustomLayoutManager extends RecyclerView.LayoutManager { // 设置布局宽高的 这里让它宽高 ...

FlyPig_Vip的博客 1073

Xilinx_JESD204B

Xilinx_JESD204B_AXI配置 说明:通过FPGA的高速数据接口JESD204B对AD9625进行高速采集。 环境:Vivado2018.3。 IP核:JESD204(7.2)。 参考手册:pg066_jesd204 v7.2。 资料以及IP_licence:JESD204B AXI协议资料:AXI 文章目录JESD204B概述JESD204B接口术语JESD204B 层JESD204B_AXI介绍AXI介绍JESD204B__AXI_Lite接口AXI-Lite接口介绍AXI-Lite接口信号

Crazzy_M的博客 1万+

prplOS介绍

介绍prplOS是什么,特性、框架、编译等。

四儿家的小祖宗的博客 2120

【高级UI】【030】自定义LayoutManager

前言 RecyclerView的核心功能,并不是由自己来完成的,而是转交给LayoutManager来完成的 LayoutManager负责的功能有:布局管理,滑动管理,缓存管理 自定义流式布局管理器 - FlowLayoutManager 这里,我们自定义了一个流式布局管理器,来演示LayoutManager是如何工作的 代码比较简单,对标准流程做了简化了,主要是为了演示,在性能和和细节上并不够好 public class MainActivity extends AppCompatActivity

天天向上 Up Up Up (*^▽^*) 2653

博途PLC PID仿真LSim_PT1库算法(一阶系统差分方程推导全过程)

博途PLC的PID仿真可以参看下面这篇博客,这篇文章我们主要论坛仿真库LSim的算法由来。如何利用博途PLC PID_Compact 进行PID仿真(一阶系统)_RXXW_Dor的博客-优快云博客博途PLC PID控制器 PID_Compact FB如何使用可以参看下面这篇文章,这里不再赘述。

RXXW_Dor的博客 2858
上一篇: 从ActiveAndroid到Realm的爬坑之路(一)
下一篇: 使用 ElasticSearch6.0 快速实现全文搜索功能
csdnYF
博客等级 码龄12年 406粉丝 32原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值