「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」
前言
上一篇介绍了RecyclerView的绘制框架,了解到RecyclerView及其子view的具体绘制工作是通过具体的LayoutManager中的onLayoutChildren和setMeasuredDimension实现的。
LayoutManager作为RecyclerView的一个组件,它的任务是负责item的布局绘制,item的回收复用。前者是我们这篇文章要梳理的内容,后者涉及到滑动相关的内容,会在交互那条线上梳理。LayoutManager是一个抽象类,系统提供了继承它的LinearLayoutManager,GridViewLayoutManager,StaggeredGridLayoutManager三种LayoutManager。首先来看看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.
// 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
resolveShouldLayoutReverse();
...
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
...
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
...
}
复制代码
关于如何布局,onLayoutChildren在一开始注释中就给出了实现算法:
- 1根据子控件和一些变量,找到锚点位置和坐标
- 2从锚点位置开始填充子控件
- 3滑动到满足要求的位置(本文重点关注前两步,第三步将在交互部分梳理。)
我对onLayoutChildren的代码做了部分忽略,使得结构看起来更清晰些。先来看第一步,确定锚点。
确定锚点
所谓锚点,在这里就是指最先定位的那一个item,锚点相关信息在LinearLayoutManager中用AnchorInfo类表示
static class AnchorInfo {
OrientationHelper mOrientationHelper; //辅助类,用于获取item view布局相关的数据
int mPosition; //anchor所对应的item位置
int mCoordinate; //anchor对应的item位置距顶部的距离
boolean mLayoutFromEnd; //是否从底部往上布局,在本文讨论的场景中,值都为false
boolean mValid; //anchor信息是否设置完毕
...
}
复制代码
LinearLayoutManager中确定锚点的方法是updateAnchorInfoForLayout(),代码如下,updateAnchorInfoForLayout通过三种判断来获取anchor信息,首先从updateAnchorFromPendingData()中获取anchor信息,如果获取到了,直接返回;如果没有获取到,再从updateAnchorFromChildren()中获取anchor信息,如果获取到的话,也是直接返回;如果前两个方法都没有获取到anchor信息,代码会走到最后两行,获取anchor的mCoordinate和mPosition。
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
if (updateAnchorFromPendingData(state, anchorInfo)) {
...
return;
}
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
...
return;
}
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
复制代码
updateAnchorFromPendingData和updateAnchorFromChildren都是发生在已经有item view的情况下,前者和滑动相关,判断依据是mPendingScrollPosition,这个值是由scrollToPosition()设置的,它会将mPendingScrollPosition当作anchor的mPosition,再根据mPosition对应的item view得到mCoordinate,后续讨论到滑动时,会具体说明;后者是通过子控件来获取anchor的,代码如下,先通过getFocusedChild()和isViewValidAsAnchor()找满足锚点要求的焦点子控件,如果不存在的话,再通过findReferenceChildClosestToStart()找离开始位置最近的子控件,找到之后,调用assignFromView设置锚点的相关信息。
private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
RecyclerView.State state, AnchorInfo anchorInfo) {
...
final View focused = getFocusedChild();
if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
return true;
}
...
View referenceChild = anchorInfo.mLayoutFromEnd
? findReferenceChildClosestToEnd(recycler, state)
: findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
...
return true;
}
return false;
}
复制代码
如果updateAnchorFromPendingData()和updateAnchorFromChildren()都返回false,没有获取到anchor信息。在这种情况下,就会执行第三种方式获取anchor信息,通过assignCoordinateFromPadding(),设置mCoordinate的值,本文讨论的场景中,值为mOrientationHelper.getStartAfterPadding(), mPosition的值为0。
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
复制代码
至此,已经获取到锚点信息,下一步就是填充子控件了。
填充子控件
填充子控件的关键代码fill()如下,可以看到,是通过while循环填充子控件的,结束条件是没有可用空间了,或者没有需要填充的子控件了。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
...
}
return start - layoutState.mAvailable;
}
复制代码
fill()中的核心代码是layoutChunk(),在layoutChunk()中具体实现了子控件的测量和布局。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
...
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
...
}
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
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.
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
复制代码
layoutChunk做了下面几点事:
一,获取待布局的子view
具体是通过LayoutState的next()获取待布局子view,而next()内部使用了Recycler的getViewForPosition()方法获取到view,后续分析到Recycler的时候,会详细分析。获取到子view后,使用addView()方法添加到父容器RecyclerView中。
二,测量子view
体现在measureChildWithMargins()方法中,measureChildWidthMargins()将padding,margin,Decoration部分去掉,剩余的作为父容器分配给子view的尺寸,通过measure()方法传入子view,开始子view的测量。
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
复制代码
三,布局子view
布局用到的是layoutDecoratedWithMargins()方法,可以看到调用到了layout()方法,在这里进入到子view的布局了。
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
复制代码
至此,子view是如何测量布局的,就梳理完了。再回到fill()方法,来看一下结束while循环的几个判断条件:
一,remainingSpace小于或等于0,
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
while (…) {
...
remainingSpace -= layoutChunkResult.mConsumed;
...
}
复制代码
remainingSpace的初始值是layoutState.mAvailable + layoutState.mExtraFillSpace,在这里,mAvailable的值是由updateLayoutStateToFillStart()/updateLayoutStateToFillEnd()决定的,具体代码体现在onLayoutChildren()中,至于具体由哪个方法来决定,分好几种情况。首先依据锚点信息中的mLayoutFromEnd,通常我们遇到的情况值都为false,代表从头开始布局。在这种情况下,会以锚点开始,先填充锚点对应item后面的子控件,调用updateLayoutStateToFillEnd()设置mLayoutState的各种属性,其中就包含mAvailable;而后填充锚点前面的子控件,调用updateLayoutStateToFillStart()设置mLayoutState的各种属性。
private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
...
}
private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) {
updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillStart(int itemPosition, int offset) {
mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();
...
}
复制代码
mExtraFillSpace和滑动相关,为了滑动的时候更顺滑,在滑动的时候,mExtraFillSpace会赋值mOrientationHelper.getTotalSpace(),目的是额外填充一个页面的子view。其他情况,这个值为0。
进入到while循环体后,remainingSpace每次会减去layoutChunkResult.mConsumed,layoutChunkResult.mConsumed是在layoutChunk()中赋值的,值为mOrientationHelper.getDecoratedMeasurement(view)。
二,layoutState.hasMore(state)为false,
boolean hasMore(RecyclerView.State state) {
return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}
复制代码
mCurrentPosition的初始值是锚点对应的mPosition,每次layoutState.next(recycler)获取view时,会依据填充方向+1/-1。
View next(RecyclerView.Recycler recycler) {
...
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
复制代码
三,调用layoutChunk()之后,如果layoutChunkResult.mFinished为true,意味着已经没有需要填充的子控件了,这时执行跳出while循环操作。
setMeasuredDimension
从上文可知,setMeasuredDimension是用于处理RecyclerView的长宽尺寸中有wrap_content的情况都,这种情况下,RecyclerView的measuredWidth/measuredHeight由子控件们中的最大测量长/宽决定。
void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
...
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
final Rect bounds = mRecyclerView.mTempRect;
getDecoratedBoundsWithMargins(child, bounds);
if (bounds.left < minX) {
minX = bounds.left;
}
if (bounds.right > maxX) {
maxX = bounds.right;
}
if (bounds.top < minY) {
minY = bounds.top;
}
if (bounds.bottom > maxY) {
maxY = bounds.bottom;
}
}
mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
}
复制代码
LinearLayoutManager没有重写setMeasuredDimension(),使用的是LayoutManager的setMeasuredDimension()。
public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
setMeasuredDimension(width, height);
}
复制代码
总结
本文梳理了LinearLayoutManager绘制相关的代码。LayoutManager承载了RecyclerView中的子控件绘制(本文的内容),子控件的回收复用,滑动时的相关逻辑和优化。正因为承载的东西太多,所有的代码又缠在一起,而我又想尽可能的把每条线都梳理清晰,所以写的时候很痛苦。篇幅不算太长,但是花费的时间还挺长的。希望能把LLM的绘制部分说清楚吧。
灵活的代价就是复杂度啊~