图文详解LinearLayoutManager填充、测量、布局过程
来源:互联网 发布:arduino 网络模块 编辑:程序博客网 时间:2024/06/03 22:42
LinearLayoutManager
并不是一个View,而是一个工具类,但是LinearLayoutManager
承担了一个View(当然指的是RecyclerView)的布局、测量、子View 创建 复用 回收 缓存 滚动等等操作。
一、回忆一下
上一篇文章Android Render(三)supportVersion 27.0.0源码RecyclerView绘制流程解析已经说了 RecyclerView的绘制流程,dispatchLayoutStep1
dispatchLayoutStep2
dispatchLayoutStep3
这三步都一定会执行,只是在RecyclerView
的宽高是写死
或者是match_parent
的时候会提前执行dispatchLayoutStep1
dispatchLayoutStep2
者两个方法。会在onLayout
阶段执行dispatchLayoutStep3
第三步。在RecyclerView
写死宽高
的时候onMeasure阶段很容易,直接设定宽高。但是在onLayout阶段会把dispatchLayoutStep1
dispatchLayoutStep2
dispatchLayoutStep3
三步依次执行。
二、onLayoutChildren开始布局准备工作
上图是在RecyclerView
中绘制三步骤对dispatchLayoutStep
三个方法的调用。看到代码我们可以知道是在dispatchLayoutStep2
方法中调用LayoutManager
的onLayoutChildren
方法来布局ItemView
的。
private void dispatchLayoutStep2() { ......略 // Step 2: Run layout mState.mInPreLayout = false; // 调用`LayoutManager`的`onLayoutChildren`方法来布局`ItemView` mLayout.onLayoutChildren(mRecycler, mState); ......略 }
下图是LinearLayoutManager
对循环布局所有的ItemView
的流程图:
虽然在RecyclerView
的源码中会三步绘制处理,但是都不是真正做绘制布局测量的地方,真正的绘制布局测量都放在了不同的LayoutManage
r中了,我们就以LinearLayoutManager
为例来分析一下。
在三中LayoutManager
中,LinearLayoutManager
应该是最为简单的一种了吧。GridLayoutManager
也是继承LinearLayoutManager
实现的,只是在layoutChunk
方法中实现了不同的布局。
LinearLayoutManager
布局从onLayoutChildren
方法开始:
//LinearLayoutManager布局从onLayoutChildren方法开始 @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // layout algorithm: 布局算法 // 1) by checking children and other variables, find an anchor coordinate and an anchor item position. // 通过检查孩子和其他变量,找到锚坐标和锚点项目位置 mAnchor为布局锚点 理解为不具有的起点. // mAnchor包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd) // 2) fill towards start, stacking from bottom 开始填充, 从底部堆叠 // 3) fill towards end, stacking from top 结束填充,从顶部堆叠 // 4) scroll to fulfill requirements like stack from bottom. 滚动以满足堆栈从底部的要求 ......略 ensureLayoutState(); mLayoutState.mRecycle = false; // resolve layout direction 设置布局方向(VERTICAL/HORIZONTAL) resolveShouldLayoutReverse(); //重置绘制锚点信息 mAnchorInfo.reset(); // mStackFromEnd需要我们开发者主动调用,不然一直未false // VERTICAL方向为mLayoutFromEnd为false HORIZONTAL方向是为true mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; // calculate anchor position and coordinate // ====== 布局算法第 1 步 ======: 计算更新保存绘制锚点信息 updateAnchorInfoForLayout(recycler, state, mAnchorInfo); ......略 // HORIZONTAL方向时开始绘制 if (mAnchorInfo.mLayoutFromEnd) { // ====== 布局算法第 2 步 ======: fill towards start 锚点位置朝start方向填充ItemView updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtra = extraForStart; // 填充第一次 fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; final int firstElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForEnd += mLayoutState.mAvailable; } // ====== 布局算法第 3 步 ======: fill towards end 锚点位置朝end方向填充ItemView updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtra = extraForEnd; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; // 填充第二次 fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; ......略 } else { // VERTICAL方向开始绘制 // ====== 布局算法第 2 步 ======: fill towards end 锚点位置朝end方向填充ItemView updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtra = extraForEnd; // 填充第一次 fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; final int lastElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForStart += mLayoutState.mAvailable; } // ====== 布局算法第 3 步 ======: fill towards start 锚点位置朝start方向填充ItemView updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtra = extraForStart; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; // 填充第二次 fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; ......略 } // ===布局算法第 4 步===: 计算滚动偏移量,如果有必要会在调用fill方法去填充新的ItemView layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); }
layout algorithm: 布局算法:
- 1.通过检查孩子和其他变量,找到锚坐标和锚点项目位置 mAnchor为布局锚点 理解为不具有的起点,mAnchor包含了子控件在Y轴上起始绘制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)。
- 2.开始填充, 从底部堆叠
- 3.结束填充,从顶部堆叠
- 4.滚动以满足堆栈从底部的要求
这四步骤我都在代码中标记出来了。
至于为什么有好几次会调用到fill方法,什么formEnd,formStart,这个请看图:
示意图图来源:http://blog.csdn.net/qq_23012315/article/details/50807224
圆形红点就是我们布局算法在第一步updateAnchorInfoForLayout
方法中计算出来的填充锚点位置。
第一种情况是屏幕显示的位置在RecyclerView
的最底部,那么就只有一种填充方向为formEnd
第二种情况是屏幕显示的位置在RecyclerView
的顶部,那么也只有一种填充方向为formStart
第三种情况应该是最常见的,屏幕显示的位置在RecyclerView
的中间,那么填充方向就有formEnd
和formStart
两种情况,这就是 fill
方法调用两次的原因。
上面是RecyclerView
的方向为VERTICAL
的情况,当为HORIZONTAL
方向的时候填充算法是不变的。
二、fill 开始布局ItemView
fill
核心就是一个while
循环,while
循环执行了一个很核心的方法就是:
layoutChunk
,此方法执行一次就填充一个ItemView到屏幕。
看一下 fill 方法的代码:
// fill填充方法, 返回的是填充ItemView需要的像素,以便拿去做滚动 int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { // 填充起始位置 final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { //如果有滚动就执行一次回收 recycleByLayoutState(recycler, layoutState); } // 计算剩余可用的填充空间 int remainingSpace = layoutState.mAvailable + layoutState.mExtra; // 用于记录每一次while循环的填充结果 LayoutChunkResult layoutChunkResult = mLayoutChunkResult; // ================== 核心while循环 ==================== while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); // ====== 填充itemView核心填充方法 ====== 屏幕还有剩余可用空间并且还有数据就继续执行 layoutChunk(recycler, state, layoutState, layoutChunkResult); } ......略 // 填充完成后修改起始位置 return start - layoutState.mAvailable; }
代码看起来还是很简洁明了的。解释都加了注释,就不再罗列出来了。看到这里我们就知道了 fill 下一步的核心方法就是 layoutChunk
, 此方法执行一次就是填充一个ItemView。
三、layoutChunk 创建 填充 测量 布局 ItemView
layoutChunk
方法主要功能标题已经说了 创建
、填充
、测量
、布局
一个ItemView
,一共有四步:
- 1
layoutState.next(recycler)
方法从一二级缓存中获取或者是创建一个ItemView
。 - 2
addView
方法加入一个ItemView
到ViewGroup
中。 - 3
measureChildWithMargins
方法测量一个ItemView
。 - 4
layoutDecoratedWithMargins
方法布局一个ItemView
。布局之前会计算好一个ItemView的left, top, right, bottom位置。
其实就是这四个关键步骤:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { // ====== 第 1 步 ====== 从一二级缓存中获取或者是创建一个ItemView View view = layoutState.next(recycler); if (view == null) { if (DEBUG && layoutState.mScrapList == null) { throw new RuntimeException("received null view when unexpected"); } // if we are laying out views in scrap, this may return null which means there is // no more items to layout. result.mFinished = true; return; } // ====== 第 2 步 ====== 根据情况来添加ItemV,最终调用的还是ViewGroup的addView方法 LayoutParams params = (LayoutParams) view.getLayoutParams(); if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0); } } else { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addDisappearingView(view); } else { addDisappearingView(view, 0); } } // ====== 第 3 步 ====== 测量一个ItemView的大小包含其margin值 measureChildWithMargins(view, 0, 0); result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); // 计算一个ItemView的left, top, right, bottom坐标值 int left, top, right, bottom; if (mOrientation == VERTICAL) { if (isLayoutRTL()) { right = getWidth() - getPaddingRight(); left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); } else { left = getPaddingLeft(); right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); } if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { bottom = layoutState.mOffset; top = layoutState.mOffset - result.mConsumed; } else { top = layoutState.mOffset; bottom = layoutState.mOffset + result.mConsumed; } } else { top = getPaddingTop(); bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { right = layoutState.mOffset; left = layoutState.mOffset - result.mConsumed; } else { left = layoutState.mOffset; right = layoutState.mOffset + result.mConsumed; } } // We calculate everything with View's bounding box (which includes decor and margins) // To calculate correct layout position, we subtract margins. // 根据得到的一个ItemView的left, top, right, bottom坐标值来确定其位置 // ====== 第 4 步 ====== 确定一个ItemView的位置 layoutDecoratedWithMargins(view, left, top, right, bottom); if (DEBUG) { Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); } // Consume the available space if the view is not removed OR changed if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } result.mFocusable = view.hasFocusable(); }
四、LinearLayoutManager填充、测量、布局过程总结
从 RecyclerView
绘制触发的一开始,就会把需要绘制的ItemView
做一次while循环绘制一次,中间要经历好多个步骤,还设计到缓存。RecyclerView的绘制处理等还是比较复杂的。
- 图文详解LinearLayoutManager填充、测量、布局过程
- Android UI测量、布局、绘制过程探究
- 自定义RecyclerView之LinearLayoutManager垂直布局
- Android LayoutInflater(布局填充器)的详解
- PHPnow图文安装过程详解
- yum安装过程图文详解
- View绘制详解(三),扒一扒View的测量过程
- Oracle 11g 安装过程图文详解
- Linux yum安装过程图文详解
- CMCC-EDU 登陆过程分析 图文详解
- mysql5.7.17安装过程 图文详解
- Android应用程序窗口Activity的测量Measure布局Layout和绘制Draw过程分析
- LinearLayout测量过程疑问
- LinearLayout测量过程分析
- Android View 测量过程
- LinearLayoutManager浅析
- GridView 完美填充布局
- canvas-图像布局填充
- File类详解
- Ceph安装指南 Luminous版本
- 在 CentOS 7.2 下安装 Hadoop 2.7.5 并搭建伪分布式环境的方法
- LeetCode刷题 | 735. Asteroid Collision12_16
- javascript返回字符串的所有排列
- 图文详解LinearLayoutManager填充、测量、布局过程
- MyBatis核心API测试
- 并行复制的从库执行FTWRL死锁
- SQL Server 2008 连接字符串
- 词频统计(30 分)(map vector sort)
- LeetCode | 461. Hamming Distance 12_16
- Uninformed search
- 数据结构实验之排序五:归并求逆序数
- 前辈分享的经典语句