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
三步依次执行。
1 – LayoutManager绘制三步骤
二、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
的流程图:
2 – LinearLayoutManager绘制分析
虽然在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,这个请看图:
3 – RecyclerView的ItemView填充方向
示意图图来源:blog.csdn.net/qq_23012315…
圆形红点就是我们布局算法在第一步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填充、测量、布局过程分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21226.html