RecyclerView源码解析(二)LinearLayoutManager绘制篇

语言: CN / TW / HK

「这是我参与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的绘制部分说清楚吧。

灵活的代价就是复杂度啊~