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

2,619 阅读7分钟

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

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