AndroidX RecyclerView總結-測量佈局

概述

通過博文記錄RecyclerView的源碼學習過程有助於鞏固自己的記憶和加深整體實現機制的理解。

RecyclerView中通過Adapter將數據源各item轉換成各ViewHolder和監聽數據變化。ViewHolder顧名思義持有View,利用ViewHolder將item數據和持有的View進行綁定設置。RecyclerView對各item View的佈局排列交由LayoutManager的子類處理,而在佈局過程中,又會藉助Recycler進行ViewHolder的緩存和複用,達到優化目的。RecyclerView還把item View的動畫邏輯解耦至ItemAnimator。

RecyclerView架構

這裏從RecyclerView的測量和佈局過程入手,瞭解內部實現機制。

源碼探究

文中源碼基於 ‘androidx.recyclerview:recyclerview:1.1.0’

測量階段

打開onMeasure方法:
[RecyclerView.java]

protected void onMeasure(int widthSpec, int heightSpec) {
    // mLayout爲開發者設置的LayoutManager子類
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    // ···
}

首先判斷如果未設置LayoutManager,則採用默認測量規則。

看一下defaultOnMeasure方法:
[RecyclerView.java]

void defaultOnMeasure(int widthSpec, int heightSpec) {
    // calling LayoutManager here is not pretty but that API is already public and it is better
    // than creating another method since this is internal.
    // chooseSize方法中判斷SpecMode若爲EXACTLY,則取SpecSize;若爲AT_MOST,則取
    // max(SpecSize, min(Padding和, MinimumWidth));否則取max(Padding和, MinimumWidth)
    final int width = LayoutManager.chooseSize(widthSpec,
            getPaddingLeft() + getPaddingRight(),
            ViewCompat.getMinimumWidth(this));
    final int height = LayoutManager.chooseSize(heightSpec,
            getPaddingTop() + getPaddingBottom(),
            ViewCompat.getMinimumHeight(this));

    // 設置RecyclerView的尺寸
    setMeasuredDimension(width, height);
}

默認規則會以父容器給定的SpecSize或RecyclerView的Padding與MinimumWidth來計算尺寸。

回到onMeasure方法中:
[RecyclerView.java]

protected void onMeasure(int widthSpec, int heightSpec) {
    // ···
    // 判斷LayoutManager是否啓用自動測量,isAutoMeasureEnabled默認返回false,但是
    // 通常LayoutManager需要重寫該方法以返回true,例如LinearLayoutManager、StaggeredGridLayoutManager。
    if (mLayout.isAutoMeasureEnabled()) {
        // ···
    } else {
        // ···
        // onMeasure方法中默認有調用了RecyclerView的defaultOnMeasure方法
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        // ···
    }
}

判斷自定義LayoutManager是否重寫isAutoMeasureEnabled以返回true。若返回false,則會執行RecyclerView默認測量規則。

繼續看onMeasure中isAutoMeasureEnabled爲true的流程部分:
[RecyclerView.java]

protected void onMeasure(int widthSpec, int heightSpec) {
    // ···
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);

        /**
         * This specific call should be considered deprecated and replaced with
         * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
         * break existing third party code but all documentation directs developers to not
         * override {@link LayoutManager#onMeasure(int, int)} when
         * {@link LayoutManager#isAutoMeasureEnabled()} returns true.
         */
        // 按照默認規則設置一次尺寸(當SpecMode非EXACTLY且數據源不爲空時,尺寸未必準確)
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        // 當指定尺寸是明確的或還沒有設置數據源時,默認規則設置的尺寸滿足,結束測量階段
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }

        // mLayoutStep初始狀態爲STEP_START
        if (mState.mLayoutStep == State.STEP_START) {
            // 執行第一階段佈局---【1】
            dispatchLayoutStep1();
        }
        // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
        // consistency
        // 從Spec中獲取size和mode,賦值給對應mWidth、mWidthMode、mHeight、mHeightMode成員。
        // 當mode爲UNSPECIFIED時,若API版本低於23,會將對應對應mWidth、mHeight設爲0。
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        // mIsMeasuring用於標記RecyclerView當前正在計算佈局邊界
        mState.mIsMeasuring = true;
        // 執行第二階段佈局---【2】(在該階段中會對child進行測量)
        dispatchLayoutStep2();

        // now we can get the width and height from the children.
        // 遍歷child,計算child佈局邊界(包含分隔線),設置RecyclerView自身尺寸
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

        // if RecyclerView has non-exact width and height and if there is at least one child
        // which also has non-exact width & height, we have to re-measure.
        // 判斷是否需要二次測量,shouldMeasureTwice默認返回false。
        // 自定義LayoutManager可根據自身佈局特性重寫該方法,當寬高SpecMode都非EXACTLY且有一個child的寬高尺寸也都不是EXACTLY時需要返回true。
        if (mLayout.shouldMeasureTwice()) {
            mLayout.setMeasureSpecs(
                    MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            // now we can get the width and height from the children.
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
    } else {
        // ···
    }
}

可以看到,RecyclerView在測量階段若啓用AutoMeasure就會開始進行一部分佈局操作(dispatchLayoutStep1、dispatchLayoutStep2)。

佈局分三個階段,分別對應dispatchLayoutStep1dispatchLayoutStep2dispatchLayoutStep3三個方法。

mState實例爲State,用於保存當前RecyclerView狀態的有用信息,例如目標滾動位置或視圖焦點,還可以保留由資源ID標識的任意數據。它的mLayoutStep用於標記當前佈局階段狀態,初始狀態爲STEP_START,執行dispatchLayoutStep1方法後變更爲STEP_LAYOUT,執行dispatchLayoutStep2後變爲STEP_ANIMATIONS,執行dispatchLayoutStep3後又變回STEP_START

佈局階段

進入RecyclerView的onLayout方法:
[RecyclerView.java]

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    // 派發佈局流程
    dispatchLayout();
    TraceCompat.endSection();
    // 標記首次佈局完成
    mFirstLayoutComplete = true;
}

dispatchLayout

接着進入dispatchLayout方法:
[RecyclerView.java]

void dispatchLayout() {
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        // leave the state in START
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        // leave the state in START
        return;
    }
    mState.mIsMeasuring = false;
    // 判斷佈局階段狀態(onMeasure中有可能先執行dispatchLayoutStep1、dispatchLayoutStep2)
    if (mState.mLayoutStep == State.STEP_START) {
        // 若測量階段未進行任何佈局操作,則從階段一開始執行
        dispatchLayoutStep1();
        // 將生成EXACTLY的MeasureSpec,並調用setMeasureSpecs傳入
        mLayout.setExactMeasureSpecsFrom(this);
        // 執行佈局階段二
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to
        // changed size.
        // 若有item的變更或尺寸變化,則再執行一次佈局階段二
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    // 執行佈局階段三
    dispatchLayoutStep3();
}

dispatchLayout方法中會視狀態和變化情況依次調用佈局階段。接下來依次看佈局的三個階段方法。

dispatchLayoutStep1

RecyclerView在onMeasure方法中有可能先執行dispatchLayoutStep1和dispatchLayoutStep2,首先看dispatchLayoutStep1方法:
[RecyclerView.java]

private void dispatchLayoutStep1() {
    // 斷言檢查當前狀態
    mState.assertLayoutStep(State.STEP_START);
    // 更新mState中保存的滾動偏移量
    fillRemainingScrollValues(mState);
    mState.mIsMeasuring = false;
    // 在requestLayout前調用,用於判斷冗餘的佈局操作,和stopInterceptRequestLayout成對出現
    startInterceptRequestLayout();
    // ViewInfoStore用於保存動畫相關數據,此處先清空數據
    mViewInfoStore.clear();
    // mLayoutOrScrollCounter遞增1,可用於判斷當前是否在計算佈局,避免在佈局和滾動時更改適配器數據源
    onEnterLayoutOrScroll();
    // 處理適配器數據更新和設置mState動畫相關變量
    processAdapterUpdatesAndSetAnimationFlags();
    // 查找焦點view並設置mState焦點相關變量
    saveFocusInfo();
    // item動畫相關變量設置
    mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
    mItemsAddedOrRemoved = mItemsChanged = false;
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    // 數據個數
    mState.mItemCount = mAdapter.getItemCount();
    // 獲取最小和最大的ViewHolder佈局索引位置保存在mMinMaxLayoutPositions數組中
    findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);

    // 記錄item動畫相關數據,保存在ViewInfoStore中。
    // mRunSimpleAnimations爲true時,當適配器數據變化時可能執行item view的淡入/淡出動畫。
    if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
        // ···
    }
    // 記錄item動畫相關數據,保存在ViewInfoStore中。
    // mRunPredictiveAnimations爲true時,會保存各ViewHolder的舊適配器索引位置,並進行預佈局
    if (mState.mRunPredictiveAnimations) {
        // Step 1: run prelayout: This will use the old positions of items. The layout manager
        // is expected to layout everything, even removed items (though not to add removed
        // items back to the container). This gives the pre-layout position of APPEARING views
        // which come into existence as part of the real layout.

        // Save old positions so that LayoutManager can run its mapping logic.
        // ···
        
        // we don't process disappearing list because they may re-appear in post layout pass.
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    onExitLayoutOrScroll();
    // 在觸發佈局的代碼之後調用,與startInterceptRequestLayout成對出現。當入參傳入true時,
    // 若標記了推遲佈局和未抑制佈局則會執行佈局操作。
    stopInterceptRequestLayout(false);
    mState.mLayoutStep = State.STEP_LAYOUT;
}

該方法主要記錄佈局前的狀態以及索引位置信息等數據。

dispatchLayoutStep2

接着看dispatchLayoutStep2方法:
[RecyclerView.java]

private void dispatchLayoutStep2() {
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    mState.mInPreLayout = false;
    // 真正的佈局操作,交由LayoutManager實現,默認爲空方法,需要子類重寫來實現。
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;

    // onLayoutChildren may have caused client code to disable item animations; re-check
    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
}

該方法中會通過LayoutManager的onLayoutChildren方法來進行真正的child佈局操作,該方法需要由子類重寫實現,例如LinearLayoutManager、GridLayoutManager等根據自身排列特性來實現具體的佈局邏輯。

dispatchLayoutStep3

進入dispatchLayoutStep3方法:
[RecyclerView.java]

private void dispatchLayoutStep3() {
    mState.assertLayoutStep(State.STEP_ANIMATIONS);
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    mState.mLayoutStep = State.STEP_START;
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        // traverse list in reverse because we may call animateChange in the loop which may
        // remove the target view holder.
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            // 遍歷獲取ViewHolder
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore()) {
                continue;
            }
            long key = getChangedHolderKey(holder);
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
            // 若存在變更的ViewHolder(例如添加、移除、變換位置),則需要執行item動畫
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                // run a change animation

                // If an Item is CHANGED but the updated version is disappearing, it creates
                // a conflicting case.
                // Since a view that is marked as disappearing is likely to be going out of
                // bounds, we run a change animation. Both views will be cleaned automatically
                // once their animations finish.
                // On the other hand, if it is the same view holder instance, we run a
                // disappearing animation instead because we are not going to rebind the updated
                // VH unless it is enforced by the layout manager.
                final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                        oldChangeViewHolder);
                final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                if (oldDisappearing && oldChangeViewHolder == holder) {
                    // run disappear animation instead of change
                    // 將動畫信息添加至ViewInfoStore的mLayoutHolderMap成員中保存
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                } else {
                    final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                            oldChangeViewHolder);
                    // we add and remove so that any post info is merged.
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                    ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
                    if (preInfo == null) {
                        handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
                    } else {
                        // 提交動畫任務執行
                        animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                oldDisappearing, newDisappearing);
                    }
                }
            } else {
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        // Step 4: Process view info lists and trigger animations
        // 遍歷ViewInfoStore的mLayoutHolderMap集合中的動畫信息,提交動畫任務執行
        mViewInfoStore.process(mViewInfoProcessCallback);
    }

    // 清理、回收、復位等操作
    mLayout.removeAndRecycleScrapInt(mRecycler);
    mState.mPreviousLayoutItemCount = mState.mItemCount;
    mDataSetHasChangedAfterLayout = false;
    mDispatchItemsChangedEvent = false;
    mState.mRunSimpleAnimations = false;

    mState.mRunPredictiveAnimations = false;
    mLayout.mRequestedSimpleAnimations = false;
    if (mRecycler.mChangedScrap != null) {
        mRecycler.mChangedScrap.clear();
    }
    if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
        // Initial prefetch has expanded cache, so reset until next prefetch.
        // This prevents initial prefetches from expanding the cache permanently.
        mLayout.mPrefetchMaxCountObserved = 0;
        mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
        mRecycler.updateViewCacheSize();
    }

    // 完整的佈局流程完成時通知LayoutManager進行相應的清理善後操作
    mLayout.onLayoutCompleted(mState);
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    mViewInfoStore.clear();
    if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
        // 若索引位置發生改變,則執行onScrollChanged和onScrolled回調方法
        dispatchOnScrolled(0, 0);
    }
    // 重新獲取item view焦點
    recoverFocusFromState();
    resetFocusInfo();
}

該方法爲佈局的最後一步,主要處理item動畫相關和緩存信息的清理和回收。

LinearLayoutManager

RecyclerView的佈局邏輯是委託給LayoutManager來實現,開發者可以自定義LayoutManager來實現自定義佈局方式,而Android也提供了幾個自定義實現,例如LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager,這裏以LinearLayoutManager爲例,看看佈局實現過程。

RecyclerView在佈局階段會調用LayoutManager的onLayoutChildren方法,該方法佈局流程大致分爲兩個步驟:

  1. 更新錨點索引位置和座標位置等信息
  2. 以錨點爲基準向頂部和底部方向佈局排列

[LinearLayoutManager.java]

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ···
    
    // 在onSaveInstanceState回調中,會把錨點相關信息保存在SavedState(繼承自Parcelable)中,
    // 在onRestoreInstanceState回調中恢復數據賦值給mPendingSavedState。
    // mPendingScrollPosition表示要滾動到的item數據索引位置。
    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;
    // resolve layout direction
    resolveShouldLayoutReverse();

    // 獲取當前ViewGroup中焦點view
    final View focused = getFocusedChild();
    // mAnchorInfo實例爲AnchorInfo,用於保存錨點索引位置和座標位置等信息
    if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
            || mPendingSavedState != null) {
        // 若當前錨點信息無效或存在待滾動的目標位置或存在待恢復的數據
        mAnchorInfo.reset();
        // 標記排列方向是自底向頂還是自頂向底
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // calculate anchor position and coordinate
        // 更新錨點信息,保存在mAnchorInfo中
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                    >= mOrientationHelper.getEndAfterPadding()
            || mOrientationHelper.getDecoratedEnd(focused)
            <= mOrientationHelper.getStartAfterPadding())) {
        // This case relates to when the anchor child is the focused view and due to layout
        // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
        // up after tapping an EditText which shrinks RV causing the focused view (The tapped
        // EditText which is the anchor child) to get kicked out of the screen. Will update the
        // anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
        // the available space in layoutState will be calculated as negative preventing the
        // focused view from being laid out in fill.
        // Note that we won't update the anchor position between layout passes (refer to
        // TestResizingRelayoutWithAutoMeasure), which happens if we were to call
        // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
        // child which can change between layout passes).
        // 用焦點view位置更新錨點信息
        mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
    }
    
    // ···
}

updateAnchorInfoForLayout方法中首先查找待滾動目標位置和待恢復數據更新錨點信息。若不滿足,再用焦點view更新錨點信息。仍不滿足,則以最靠近佈局頂部或底部的可見的item作爲錨點。

[LinearLayoutManager.java]

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ···
    
    // LLM may decide to layout items for "extra" pixels to account for scrolling target,
    // caching or predictive animations.

    mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
            ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    // 當有滾動時,計算額外的佈局空間,頂部/左側空間存於mReusableIntPair[0],底部/右側空間存於mReusableIntPair[1]
    calculateExtraLayoutSpace(state, mReusableIntPair);
    int extraForStart = Math.max(0, mReusableIntPair[0])
            + mOrientationHelper.getStartAfterPadding();
    int extraForEnd = Math.max(0, mReusableIntPair[1])
            + mOrientationHelper.getEndPadding();
    // mPendingScrollPositionOffset表示item view的起始邊緣和RecyclerView的起始邊緣的偏移量
    // 通過scrollToPosition或scrollToPositionWithOffset使滾動到目標索引位置時,會設置mPendingScrollPosition和mPendingScrollPositionOffset
    if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
            && mPendingScrollPositionOffset != INVALID_OFFSET) {
        // if the child is visible and we are going to move it around, we should layout
        // extra items in the opposite direction to make sure new items animate nicely
        // instead of just fading in
        // 查找要滾動到的目標索引位置的view
        final View existing = findViewByPosition(mPendingScrollPosition);
        if (existing != null) {
            final int current;
            final int upcomingOffset;
            // mShouldReverseLayout默認爲false
            if (mShouldReverseLayout) {
                current = mOrientationHelper.getEndAfterPadding()
                        - mOrientationHelper.getDecoratedEnd(existing);
                upcomingOffset = current - mPendingScrollPositionOffset;
            } else {
                // 計算滾動目標view的起始(top/left)邊界(包含分割線和margin)和RecyclerView起始端內邊距之間的距離
                current = mOrientationHelper.getDecoratedStart(existing)
                        - mOrientationHelper.getStartAfterPadding();
                // 計算實際偏移量
                upcomingOffset = mPendingScrollPositionOffset - current;
            }
            if (upcomingOffset > 0) {
                extraForStart += upcomingOffset;
            } else {
                extraForEnd -= upcomingOffset;
            }
        }
    }
        
    // ···
}

這裏主要是當有滾動到指定位置時,預計算佈局item用於平滑滾動,計算分配額外空間。

[LinearLayoutManager.java]

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ···
    
    int startOffset;
    int endOffset;
    final int firstLayoutDirection;
    if (mAnchorInfo.mLayoutFromEnd) {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                : LayoutState.ITEM_DIRECTION_HEAD;
    } else {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                : LayoutState.ITEM_DIRECTION_TAIL;
    }

    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    // Recycler暫時回收ViewHolder和View
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mInfinite = resolveIsInfinite();
    mLayoutState.mIsPreLayout = state.isPreLayout();
    // noRecycleSpace not needed: recycling doesn't happen in below's fill
    // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
    mLayoutState.mNoRecycleSpace = 0;
    if (mAnchorInfo.mLayoutFromEnd) {
        // 先從錨點往上排布item,再從錨點往下排布item
        // fill towards start
        // 更新mLayoutState中頂部空間相關信息
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForStart;
        // 佈局item填充空間
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        final int firstElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForEnd += mLayoutState.mAvailable;
        }
        // fill towards end
        // 更新mLayoutState中底部空間相關信息
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForEnd;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        // 佈局item填充空間
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;

        // 判斷是否還有剩餘空間可以填充item
        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 {
        // 先從錨點往下排布item,再從錨點往上排布item
        // fill towards end
        // 更新mLayoutState中底部空間相關信息
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForEnd;
        // 佈局item填充空間
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
        final int lastElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForStart += mLayoutState.mAvailable;
        }
        // fill towards start
        // 更新mLayoutState中頂部空間相關信息
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        // 佈局item填充空間
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;

        // 判斷是否還有剩餘空間可以填充item
        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;
            // 佈局item填充空間
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
        }
    }
    
    // ···
}

這部分計算錨點距RecyclerView頂部和底部的空間,從錨點開始往兩端進行item的佈局填充,核心佈局方法在fill方法中。

[LinearLayoutManager.java]

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ···
    
    // changes may cause gaps on the UI, try to fix them.
    // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
    // changed
    if (getChildCount() > 0) {
        // because layout from end may be changed by scroll to position
        // we re-calculate it.
        // find which side we should check for gaps.
        // 計算gap,
        if (mShouldReverseLayout ^ mStackFromEnd) {
            int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
            startOffset += fixOffset;
            endOffset += fixOffset;
            fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
            startOffset += fixOffset;
            endOffset += fixOffset;
        } else {
            int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
            startOffset += fixOffset;
            endOffset += fixOffset;
            fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
            startOffset += fixOffset;
            endOffset += fixOffset;
        }
    }
    // 執行item動畫相關的佈局
    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    if (!state.isPreLayout()) {
        mOrientationHelper.onLayoutComplete();
    } else {
        mAnchorInfo.reset();
    }
    mLastStackFromEnd = mStackFromEnd;
    // DEBUG ···
}

接下來看fill方法:

[LinearLayoutManager.java]

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) {
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 用於存儲佈局執行結果
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    // 還有剩餘空間和item數據就不斷執行佈局
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.beginSection("LLM LayoutChunk");
        }
        // 佈局單個View
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (RecyclerView.VERBOSE_TRACING) {
            TraceCompat.endSection();
        }
        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;
        }

        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;
}

該方法中通過循環不斷調用layoutChunk方法一個一個View佈局。

[LinearLayoutManager.java]

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // 通過Recycler的getViewForPosition方法獲取View
    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;
    }
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    // mScrapList默認爲空,對於item動畫布局時會設置mScrapList
    if (layoutState.mScrapList == null) {
        // 判斷排布錨點頂部空間時是否反轉佈局或者錨點底部空間時不反轉佈局
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            // 通常情況下在底部空間往RecyclerView添加view時,是逐漸靠近下邊界,view添加在最後一個child位置
            addView(view);
        } else {
            // 通常情況下在頂部空間往RecyclerView添加view時,是逐漸靠近上邊界,view添加第一個child位置
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            // 添加要消失的view,在動畫完成後將會移除
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    // 若該child調用了requestLayout,或RecyclerView禁用測量緩存,或分配給child的空間
    // 小於之前測量的空間,則調用child.measure再次測量(佔用空間包括分隔線和margin)
    measureChildWithMargins(view, 0, 0);
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    int left, top, right, bottom;
    // 判斷橫向排列還是垂直排列
    if (mOrientation == VERTICAL) {
        // 計算分配給child的左上右下四邊界
        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.
    // 調用child.layout佈局(佔用空間包括分隔線和margin)
    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
    // 判斷該view對應的適配器中數據集的item是否刪除或者更改
    if (params.isItemRemoved() || params.isItemChanged()) {
        // 標記mIgnoreConsumed爲true,外層fill方法中不會減去該view消耗空間
        result.mIgnoreConsumed = true;
    }
    result.mFocusable = view.hasFocusable();
}

該方法首先通過Recycler獲取緩存的ViewHolder中的View或Adapter創建ViewHolder綁定View。

獲取到View後,通過addView或addDisappearingView添加view到RecyclerView中。通過addDisappearingView添加的view,若其對應的適配器中item數據已刪除,則在播放完消失動畫後也會被移除。addView或addDisappearingView中經過callback最終調用到ViewGroup的addView和attachViewToParent方法。

在處理完view添加後,會視情況再次調用child.measure進行測量,最後計算四邊界調用child.layout進行佈局。

總結

RecyclerView的佈局核心流程分爲三個階段,對應dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3三個方法,而在測量階段就會預先執行前兩個階段,Step2執行後RecyclerView會設置自身尺寸。
佈局第一階段主要是記錄當前狀態和ViewHolder索引位置以及item動畫等數據。第二階段進行真正的佈局操作,其具體佈局實現由LayoutManager的子類來完成。第三階段主要進行item動畫相關操作和緩存數據清理回收工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章