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

图文详解LinearLayoutManager填充、测量、布局过程LinearLayoutManager并不是一个View,而是一个工具类,但是LinearLayoutManager承担了一个View(当然指的是RecyclerView)的布局、测量、子View创建

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三步依次执行。

图文详解LinearLayoutManager填充、测量、布局过程
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的流程图:

图文详解LinearLayoutManager填充、测量、布局过程
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,这个请看图:


图文详解LinearLayoutManager填充、测量、布局过程
3 – RecyclerView的ItemView填充方向

示意图图来源:blog.csdn.net/qq_23012315…

圆形红点就是我们布局算法在第一步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的绘制处理等还是比较复杂的。

今天的文章图文详解LinearLayoutManager填充、测量、布局过程分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21226.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注