图文详解LinearLayoutManager填充、测量、布局过程

来源:互联网 发布:arduino 网络模块 编辑:程序博客网 时间:2024/06/03 22:42

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方法中调用LayoutManageronLayoutChildren方法来布局ItemView的。

    private void dispatchLayoutStep2() {        ......略        // Step 2: Run layout        mState.mInPreLayout = false;        // 调用`LayoutManager`的`onLayoutChildren`方法来布局`ItemView`        mLayout.onLayoutChildren(mRecycler, mState);        ......略          }

下图是LinearLayoutManager对循环布局所有的ItemView的流程图:

2 - LinearLayoutManager绘制分析

虽然在RecyclerView的源码中会三步绘制处理,但是都不是真正做绘制布局测量的地方,真正的绘制布局测量都放在了不同的LayoutManager中了,我们就以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填充方向

示意图图来源:http://blog.csdn.net/qq_23012315/article/details/50807224

圆形红点就是我们布局算法在第一步updateAnchorInfoForLayout方法中计算出来的填充锚点位置。

第一种情况是屏幕显示的位置在RecyclerView的最底部,那么就只有一种填充方向为formEnd

第二种情况是屏幕显示的位置在RecyclerView的顶部,那么也只有一种填充方向为formStart

第三种情况应该是最常见的,屏幕显示的位置在RecyclerView的中间,那么填充方向就有formEndformStart两种情况,这就是 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方法加入一个ItemViewViewGroup中。
  • 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的绘制处理等还是比较复杂的。