RecycleView源码分析(四)LayoutManager源码分析

1,639 阅读8分钟

上篇测绘流程的核心逻辑传递到LayoutManager中,本篇我们会详细分析LinearLayoutManager的源码,分析完成这个测绘流程,并且为以后自己实现LayoutManager作准备。

几个比较重要的方法。

1. generateDefaultLayoutParams()

LinearLayoutManager继承了RecyclerView.LayoutManager,RecyclerView.LayoutManager是一个抽象类。只有一个抽象方法generateDefaultLayoutParams是必须实现的。

   public abstract LayoutParams generateDefaultLayoutParams();

这个方法是设置每个item默认的LayoutParams的,我们必须指定这个方法的实现。
LinearLayoutManager的实现比较简单。横竖向都是自适应的。

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

2. onLayoutChildren

在分析RecycleView的测绘流程的时候,控件的测绘主要是调用了LayoutManager的onLayoutChildren方法,这是一个入口,内部完成了各个item的measure和layout,实现第一次item的填充。里面有两个参数,第一个就是RecycleView的复用item的重头戏控件Recycler,用于内部回收复用的方式获取view。第二个是保存各种测绘信息的集中单元。

    public void onLayoutChildren(Recycler recycler, State state) {
            Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
        }

3. onMeasure()和isAutoMeasureEnabled()

onMeasure()和isAutoMeasureEnabled()上一篇也说过了,不要同时实现它们的逻辑,两个方法重写是互斥的。isAutoMeasureEnabled()是系统提供的自动测绘,onMeasure()如果重写我们自己的测量流程。如果重写isAutoMeasureEnabled()返回true就不应该重写onMeasure()。如果没有重写isAutoMeasureEnabled(),默认是返回false,这时应该重写onMeasure()。

4. canScrollHorizontally()、canScrollVertically()、scrollHorizontallyBy()、scrollVerticallyBy()

滑动系列方法,前两个是是否可以水平竖直方向进行滑动。后两个是滑动过程中进行处理的方法。只有在前两个方法中放开,也就是返回true,RecycleView才会把touchEvent下放到LayoutManager里面。后两个方法随着滑动不但要实现view的回收和复用,还要进行新view的填充,和滚动出屏幕的回收。

如果要自己实现一个LayoutManager(后面的章节我们会自己实现一个LayoutManager),我们重写上面的几个方法就可以完成了。所以在分析系统的LinearLayoutManager时,我们着重分析上面的几个方法,即可了解大部分功能。

LayoutManager运行的整体思路

  1. 通过isAutoMeasureEnabled()判断是否使用自动测量,没有开启那么就靠LayoutManager#onMeasure()进行测量,这时我们就要自己去实现了。如果开启了,就走系统的dispatchLayoutStep系列方法,这时我们不需要自定义onMeasure()。
  2. 通过onLayoutChildren完成第一次的填充和后面的notify刷新,内部会通过计算可用面积,只填充显示区域下的各个item。这时第一屏的效果已经完成了。
  3. 我们滑动RecycleView,通过canScrollHorizontally()、canScrollVertically()、scrollHorizontallyBy()、scrollVerticallyBy()一些列方法,配置滑动下的反应。完成动态的填充,和不可见item的回收,新item的复用。 LayoutManager内分工比较明确,onLayoutChildren负责整屏的填充,scrollHorizontallyBy()、scrollVerticallyBy()处理滑动过程中的填充。两种填充方式都有了。

LayoutManager集成了RV的滑动处理和测量布局处理。实际上底层就是测量布局的处理,因为滑动过程中也需要重新进行测量和布局。

本篇只分析onLayoutChildren整屏填充部分,下一篇分析滑动部分。

onLayoutChildren整屏填充

前置条件:state.isPreLayout()为false,上文讲了和是否完成第一次测量和是否有动画相关。所以这个条件判断为true下的代码会在动画篇章讲解。

onLayoutChildren的代码比较多,但是从整体看,只有两个步骤

  1. 确认锚点信息
  2. 根据锚点信息进行测量布局

确认锚点信息步骤

    //处理SavedState数据
    if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
        }

        ensureLayoutState();
        mLayoutState.mRecycle = false;
        resolveShouldLayoutReverse();

        final View focused = getFocusedChild();
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // 初始化锚点信息
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                        >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
            mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
        }

我们依次看下需要分析的点

  1. mPendingSavedState和mPendingScrollPosition mPendingSavedStates是缓存恢复的数据,通过它可以恢复到之前的状态。通常发生在销毁重建的过程。mPendingScrollPosition是通过scrollToPosition设置的。也就是外部要求滚动到的位置。没有设置默认值就是RecyclerView.NO_POSITION。

  2. resolveShouldLayoutReverse()
    这个方法处理了布局处理的方向,从正方向或者反方向开始填充。如果是水平方向并且是从右往左的阅读方向(比如阿拉伯风格)默认就是开启了反转方向。mShouldReverseLayout为true则是反方向绘制,从底部或者右侧开始绘制。false则相反。

    private void resolveShouldLayoutReverse() {
        // A == B is the same result, but we rather keep it readable
        if (mOrientation == VERTICAL || !isLayoutRTL()) {
            mShouldReverseLayout = mReverseLayout;
        } else {
            //水平并且从右到左的方向,默认开启反转布局
            mShouldReverseLayout = !mReverseLayout;
        }
    }
    
  3. mLayoutFromEnd

    首先是 mAnchorInfo.mLayoutFromEnd的设置。只有mShouldReverseLayout和 mStackFromEnd不同时,才为true。 mStackFromEnd是通过setStackFromEnd设置的,表示是否初始定位是否在数据末端开始,如果为true那么就从最后一条数据开始显示。 mAnchorInfo.mLayoutFromEnd为true,形象的表示就是是在反方向开始进行布局。必入垂直方向,就是屏幕下端开始布局。

  4. updateAnchorInfoForLayout锚点配置
    锚点配置在updateAnchorInfoForLayout(recycler, state, mAnchorInfo)方法中。
    首先看下AnchorInfo,这个类就是存储锚点的信息,看下内部的具体细节。

    class AnchorInfo {
        OrientationHelper mOrientationHelper;
        int mPosition;
        int mCoordinate;
        boolean mLayoutFromEnd;
        boolean mValid;
       }
    
    属性意义
    mOrientationHelper是一个帮助类,里面有很多测量布局帮助方法。
    mPosition就是锚点的锚,具体就是屏幕上第一个position
    mCoordinate表示绘制起点,初始化就是顶部底部的padding
    mLayoutFromEnd表示是否从定位到屏幕底部
    mValid锚点是否可用

    updateAnchorInfoForLayout方法内部具体设置实现了锚点的配置。

    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;
    }
    
    void assignCoordinateFromPadding() {
            mCoordinate = mLayoutFromEnd
                    ? mOrientationHelper.getEndAfterPadding()
                    : mOrientationHelper.getStartAfterPadding();
    }
    

    大体结构:可以看出先通过peding的数据设置锚点,内部处理了mPendingScrollPosition的数据给锚点。再通过各个children设置锚点。最后都没有成功配置,设置默认的锚点。

    1. 通过mPending数据设置锚点
    private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
        if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
            return false;
        }
       
        if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
             // mPendingScrollPosition判断是否合法
            mPendingScrollPosition = RecyclerView.NO_POSITION;
            mPendingScrollPositionOffset = INVALID_OFFSET;
            return false;
        }
        anchorInfo.mPosition = mPendingScrollPosition;
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
            //处理 mPendingSavedState的数据
            anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
            if (anchorInfo.mLayoutFromEnd) {
                anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
                        - mPendingSavedState.mAnchorOffset;
            } else {
                anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding()
                        + mPendingSavedState.mAnchorOffset;
            }
            return true;
        }
    
        if (mPendingScrollPositionOffset == INVALID_OFFSET) {
            //处理没有设置offset的情况
            View child = findViewByPosition(mPendingScrollPosition);
            if (child != null) {
                final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
                if (childSize > mOrientationHelper.getTotalSpace()) {
                    // item does not fit. fix depending on layout direction
                    anchorInfo.assignCoordinateFromPadding();
                    return true;
                }
                final int startGap = mOrientationHelper.getDecoratedStart(child)
                        - mOrientationHelper.getStartAfterPadding();
                if (startGap < 0) {
                    anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding();
                    anchorInfo.mLayoutFromEnd = false;
                    return true;
                }
                final int endGap = mOrientationHelper.getEndAfterPadding()
                        - mOrientationHelper.getDecoratedEnd(child);
                if (endGap < 0) {
                    anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding();
                    anchorInfo.mLayoutFromEnd = true;
                    return true;
                }
                anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd
                        ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper
                        .getTotalSpaceChange())
                        : mOrientationHelper.getDecoratedStart(child);
            } else { // item is not visible.
                if (getChildCount() > 0) {
                    // get position of any child, does not matter
                    int pos = getPosition(getChildAt(0));
                    anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos
                            == mShouldReverseLayout;
                }
                anchorInfo.assignCoordinateFromPadding();
            }
            return true;
        }
        // override layout from end values for consistency
        anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
        // if this changes, we should update prepareForDrop as well
        if (mShouldReverseLayout) {
            anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
                    - mPendingScrollPositionOffset;
        } else {
            anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding()
                    + mPendingScrollPositionOffset;
        }
        return true;
    }
    

    这个方法主要处理通过scrollToPositionWithOffset(int position, int offset)方法传入的数据的处理。也就是指定滚动位置和offset的处理点。具体逻辑比较清晰,这里就不细讲了。

    1. 通过children数据设置锚点 这里主要通过已有的childer,根据绘制从底部还是顶部,找到一个最接近的锚点。应用于notify系列方法进行刷新数据。因为当前已经有children了,所以直接找屏幕上显示的第一个view作为锚,以保存当前滑动的位置。我们自己定义的LayoutManager,当弹起软键盘时,如果没有通过children数据设置锚点。因为RecyclerView重新绘制,会回到初始进行刷新,而不是当前已经滑动到的位置。

    2. 默认锚点 上面两个步骤都没有确认锚点,那么就会使用默认的锚点。

       anchorInfo.assignCoordinateFromPadding();
       anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    

    短短的两行代码,第一行从方法名可以看出是设置绘制起点为padding的部分,这里也说明了RecycleView的padding属性怎么生效的。第二行设置了锚点的位置,如果setStackFromEnd设置了数据显示方向,如果是屏幕底部(true)第一屏显示的就是最后一条数据。如果屏幕顶部(false 默认)显示的就是第一条数据。

经过上面三个步骤,锚点就确定完了。也就是起始绘制的数据index就确认了。接下来就需要确认绘制数据和调用fill进行填充了。

确认绘制数据

确认绘制数据主要是通过updateLayoutStateToFillStart(mAnchorInfo)和updateLayoutStateToFillEnd(mAnchorInfo)两个方法进行的。

        detachAndScrapAttachedViews(recycler);
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
        mLayoutState.mNoRecycleSpace = 0;
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } 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;
            }
        }

     
        if (!state.isPreLayout()) {
            mOrientationHelper.onLayoutComplete();
        } else {
            mAnchorInfo.reset();
        }
        mLastStackFromEnd = mStackFromEnd;
  1. 通过detachAndScrapAttachedViews方法detach所有的child。可以理解为detach掉所有add的字View,恢复到了一个无字View的初始状态,在后面的布局中统一逻辑进行新的布局。这部分和回收有关,后面的章节讲回收复用会详细讲这部分。包括detach和remove的区别。
  2. 通过mLayoutFromEnd判断填充的方向,上面也提到过这个变量。他是从mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd设置的。如果mLayoutFromEnd为true就是代表从末端开始填充,垂直布局就是从底部,水平布局就是从右侧。
    如果从末端布局,先通过updateLayoutStateToFillStart配置,调用fill方法往反方向初始段布局,再通过updateLayoutStateToFillEnd配置往末端布局,同样通过fill进行末端填充。从首段进行填充刚好相反。
    为什么会是这样的设计呢?而不是直接从底部开始
    我们通过反证法验证这个问题,如果是从末端填充,我们这里通过垂直方向,也就是从屏幕底部开始布局举例。 1D37112C-68F4-4306-AA3B-65E7C0EC0357.png 在这种情况,我们就需要确定距离屏幕顶部有几个可以填充的,才可以确定自己的位置,因为顶部填充不满剩余的空间,我们直接从底部绘制是错误,会开天窗。 B7438AE2-9773-4B0D-9FBF-7C7AC7470356.png 所以通过上面的分析我们知道为什么要这么设计了。相反,从首部开始填充的逻辑先填充下侧,因为这时第一个位置肯定是在屏幕最顶部的。填充完下侧再填充上侧的,这时没有地方填充了,但是如果我们通过 scrollToPositionWithOffset方法设置offset往上侧填充就可以有空间继续了。

调用fill进行填充

确认完锚点和绘制数据后,接下来就是重头戏了,通过fill进行填充,这是一个很重要的方法

fill(recycler, mLayoutState, state, false)

通过他的参数可以看出,recycler是RecycleView提供复用回收的统一控件。mLayoutState, state包含了绘制的各种参数,包括起始点、方向、可用空间等。所以这个方法大体的结构,通过recycler拿到待布局的view,通过参数进行填充。我们看下具体的代码。

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        //如果产生了滑动
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // 统一进行回收
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            // 根据可用空间进行布局
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.endSection();
            }
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                 // 布局了新的view,减少可用空间
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        if (DEBUG) {
            validateChildOrder();
        }
        return start - layoutState.mAvailable;
    }

逻辑比较简单

  1. 首先进行回收,这部分是进行滑动后的回收工作,下一章会讲滑动的部分。
  2. 算出remainingSpace,也就是可用的布局空间,while内部的判断也是通过remainingSpace判断是否有空间可以继续填充。消耗remainingSpace是通过layoutChunkResult这个变量。layoutChunkResult使用layoutChunk方法进行赋值的,所以主要的布局逻辑在layoutChunk中,这个也是一个很重要的方法。内部完成了指定postion的view测量和布局。

我们看下具体的layoutChunk代码:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        // 通过Recycler通过复用回收拿到下一个要填充postion的view
        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 {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addDisappearingView(view);
            } else {
                addDisappearingView(view, 0);
            }
        }
        // 测量
        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;
            }
        }
        // 布局
        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();
    }
  1. 首先提供next获取下一个要操作的view,这里直接调用了recycler.getViewForPosition方法,内部封装了复用的逻辑,下面的章节会细致的讲解。mItemDirection这里比较巧妙,他表示布局的方向,只能是1或者-1。
    View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }
  1. 拿到view后,我们终于找到了子view测量布局的地方,通过measureChildWithMargins进行测量,通过layoutDecoratedWithMargins布局。

这样一步一步,直到RecycleView的可用空间填充满了,这时RecycleView就完成了第一屏的填充。

总结

  1. LayoutManager的职责主要集中在整屏布局填充、滑动布局填充、滑动处理、item测量布局等功能,我们实现自己的LayoutManager时,也要实现这些功能
  2. 整屏填充的逻辑先确认锚点、确认布局参数,最终利用上面产生的数据进行填充。
  3. 填充填充可用区域的大小,也就是限制在RecyclerView的宽高内。

分析完LayoutManager的源码。并结合前两章的测绘流程,应该对整体的测绘,从表到里都有了很深的理解。

下一篇我们讲解RecycleView的滑动机制,分析滑动的处理和这是布局如何填充逻辑。