圖文詳解LinearLayoutManager填充、測(cè)量、布局過(guò)程

LinearLayoutManager并不是一個(gè)View,而是一個(gè)工具類,但是LinearLayoutManager承擔(dān)了一個(gè)View(當(dāng)然指的是RecyclerView)的布局、測(cè)量、子View 創(chuàng)建 復(fù)用 回收 緩存 滾動(dòng)等等操作。

一、回憶一下

上一篇文章Android Render(三)supportVersion 27.0.0源碼RecyclerView繪制流程解析已經(jīng)說(shuō)了 RecyclerView的繪制流程,dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3這三步都一定會(huì)執(zhí)行,只是在RecyclerView的寬高是寫死或者是match_parent的時(shí)候會(huì)提前執(zhí)行dispatchLayoutStep1 dispatchLayoutStep2者兩個(gè)方法。會(huì)在onLayout階段執(zhí)行dispatchLayoutStep3第三步。在RecyclerView 寫死寬高的時(shí)候onMeasure階段很容易,直接設(shè)定寬高。但是在onLayout階段會(huì)把dispatchLayoutStep1 dispatchLayoutStep2 dispatchLayoutStep3三步依次執(zhí)行。

1 - LayoutManager繪制三步驟

二、onLayoutChildren開(kāi)始布局準(zhǔn)備工作

上圖是在RecyclerView中繪制三步驟對(duì)dispatchLayoutStep三個(gè)方法的調(diào)用??吹酱a我們可以知道是在dispatchLayoutStep2方法中調(diào)用LayoutManageronLayoutChildren方法來(lái)布局ItemView的。

    private void dispatchLayoutStep2() {
      
        ......略

        // Step 2: Run layout
        mState.mInPreLayout = false;
        // 調(diào)用`LayoutManager`的`onLayoutChildren`方法來(lái)布局`ItemView`
        mLayout.onLayoutChildren(mRecycler, mState);

        ......略      

    }

下圖是LinearLayoutManager對(duì)循環(huán)布局所有的ItemView的流程圖:

2 - LinearLayoutManager繪制分析

雖然在RecyclerView的源碼中會(huì)三步繪制處理,但是都不是真正做繪制布局測(cè)量的地方,真正的繪制布局測(cè)量都放在了不同的LayoutManager中了,我們就以LinearLayoutManager為例來(lái)分析一下。
在三中LayoutManager中,LinearLayoutManager應(yīng)該是最為簡(jiǎn)單的一種了吧。GridLayoutManager也是繼承LinearLayoutManager實(shí)現(xiàn)的,只是在layoutChunk方法中實(shí)現(xiàn)了不同的布局。

LinearLayoutManager布局從onLayoutChildren方法開(kāi)始:

   
   //LinearLayoutManager布局從onLayoutChildren方法開(kāi)始
    @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. 
        // 通過(guò)檢查孩子和其他變量,找到錨坐標(biāo)和錨點(diǎn)項(xiàng)目位置   mAnchor為布局錨點(diǎn) 理解為不具有的起點(diǎn).
        // mAnchor包含了子控件在Y軸上起始繪制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)
        // 2) fill towards start, stacking from bottom 開(kāi)始填充, 從底部堆疊
        // 3) fill towards end, stacking from top 結(jié)束填充,從頂部堆疊
        // 4) scroll to fulfill requirements like stack from bottom. 滾動(dòng)以滿足堆棧從底部的要求

        ......略

        ensureLayoutState();
        mLayoutState.mRecycle = false;
        // resolve layout direction 設(shè)置布局方向(VERTICAL/HORIZONTAL)
        resolveShouldLayoutReverse();

        //重置繪制錨點(diǎn)信息
        mAnchorInfo.reset();

        // mStackFromEnd需要我們開(kāi)發(fā)者主動(dòng)調(diào)用,不然一直未false
        // VERTICAL方向?yàn)閙LayoutFromEnd為false HORIZONTAL方向是為true   
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;

        // calculate anchor position and coordinate
        // ====== 布局算法第 1 步 ======: 計(jì)算更新保存繪制錨點(diǎn)信息
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);

        ......略

        // HORIZONTAL方向時(shí)開(kāi)始繪制

        if (mAnchorInfo.mLayoutFromEnd) {
            //  ====== 布局算法第 2 步 ======: fill towards start 錨點(diǎn)位置朝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 錨點(diǎn)位置朝end方向填充ItemView
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            // 填充第二次
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            ......略
        } else {

            // VERTICAL方向開(kāi)始繪制

            //  ====== 布局算法第 2 步 ======: fill towards end 錨點(diǎn)位置朝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 錨點(diǎn)位置朝start方向填充ItemView
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            // 填充第二次
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;

            ......略
      }

        //  ===布局算法第 4 步===: 計(jì)算滾動(dòng)偏移量,如果有必要會(huì)在調(diào)用fill方法去填充新的ItemView
        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    
    }

layout algorithm: 布局算法:

  • 1.通過(guò)檢查孩子和其他變量,找到錨坐標(biāo)和錨點(diǎn)項(xiàng)目位置 mAnchor為布局錨點(diǎn) 理解為不具有的起點(diǎn),mAnchor包含了子控件在Y軸上起始繪制偏移量(coordinate),ItemView在Adapter中的索引位置(position)和布局方向(mLayoutFromEnd)。
  • 2.開(kāi)始填充, 從底部堆疊
  • 3.結(jié)束填充,從頂部堆疊
  • 4.滾動(dòng)以滿足堆棧從底部的要求

這四步驟我都在代碼中標(biāo)記出來(lái)了。

至于為什么有好幾次會(huì)調(diào)用到fill方法,什么formEnd,formStart,這個(gè)請(qǐng)看圖:


3 - RecyclerView的ItemView填充方向

示意圖圖來(lái)源:http://blog.csdn.net/qq_23012315/article/details/50807224

圓形紅點(diǎn)就是我們布局算法在第一步updateAnchorInfoForLayout方法中計(jì)算出來(lái)的填充錨點(diǎn)位置。

第一種情況是屏幕顯示的位置在RecyclerView的最底部,那么就只有一種填充方向?yàn)?code>formEnd

第二種情況是屏幕顯示的位置在RecyclerView的頂部,那么也只有一種填充方向?yàn)?code>formStart

第三種情況應(yīng)該是最常見(jiàn)的,屏幕顯示的位置在RecyclerView的中間,那么填充方向就有formEndformStart兩種情況,這就是 fill 方法調(diào)用兩次的原因。

上面是RecyclerView的方向?yàn)?code>VERTICAL的情況,當(dāng)為HORIZONTAL方向的時(shí)候填充算法是不變的。

二、fill 開(kāi)始布局ItemView

fill核心就是一個(gè)while循環(huán),while循環(huán)執(zhí)行了一個(gè)很核心的方法就是:

layoutChunk ,此方法執(zhí)行一次就填充一個(gè)ItemView到屏幕。

看一下 fill 方法的代碼:

    // fill填充方法, 返回的是填充ItemView需要的像素,以便拿去做滾動(dòng)
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // 填充起始位置
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            //如果有滾動(dòng)就執(zhí)行一次回收
            recycleByLayoutState(recycler, layoutState);
        }
        // 計(jì)算剩余可用的填充空間
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        // 用于記錄每一次while循環(huán)的填充結(jié)果
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;

        // ================== 核心while循環(huán) ====================

        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            
            // ====== 填充itemView核心填充方法 ====== 屏幕還有剩余可用空間并且還有數(shù)據(jù)就繼續(xù)執(zhí)行

            layoutChunk(recycler, state, layoutState, layoutChunkResult);

        }

        ......略

        // 填充完成后修改起始位置
        return start - layoutState.mAvailable;
    }

代碼看起來(lái)還是很簡(jiǎn)潔明了的。解釋都加了注釋,就不再羅列出來(lái)了??吹竭@里我們就知道了 fill 下一步的核心方法就是 layoutChunk , 此方法執(zhí)行一次就是填充一個(gè)ItemView。

三、layoutChunk 創(chuàng)建 填充 測(cè)量 布局 ItemView

layoutChunk 方法主要功能標(biāo)題已經(jīng)說(shuō)了 創(chuàng)建填充 、測(cè)量布局 一個(gè)ItemView,一共有四步:

  • 1 layoutState.next(recycler) 方法從一二級(jí)緩存中獲取或者是創(chuàng)建一個(gè)ItemView
  • 2 addView方法加入一個(gè)ItemViewViewGroup中。
  • 3 measureChildWithMargins方法測(cè)量一個(gè)ItemView。
  • 4 layoutDecoratedWithMargins方法布局一個(gè)ItemView。布局之前會(huì)計(jì)算好一個(gè)ItemView的left, top, right, bottom位置。

其實(shí)就是這四個(gè)關(guān)鍵步驟:

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {

        // ====== 第 1 步 ====== 從一二級(jí)緩存中獲取或者是創(chuàng)建一個(gè)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 步 ====== 根據(jù)情況來(lái)添加ItemV,最終調(diào)用的還是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 步 ====== 測(cè)量一個(gè)ItemView的大小包含其margin值
        measureChildWithMargins(view, 0, 0);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);

        // 計(jì)算一個(gè)ItemView的left, top, right, bottom坐標(biāo)值
        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.
        // 根據(jù)得到的一個(gè)ItemView的left, top, right, bottom坐標(biāo)值來(lái)確定其位置


        // ====== 第 4 步 ====== 確定一個(gè)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填充、測(cè)量、布局過(guò)程總結(jié)

RecyclerView 繪制觸發(fā)的一開(kāi)始,就會(huì)把需要繪制的ItemView做一次while循環(huán)繪制一次,中間要經(jīng)歷好多個(gè)步驟,還設(shè)計(jì)到緩存。RecyclerView的繪制處理等還是比較復(fù)雜的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容