RecyclerView源碼分析 — LayoutManager

類名 功能
RecyclerView.LayoutManager 負責Item視圖的佈局的顯示
RecyclerView.ItemDecoration 繪製Item的分割樣式
RecyclerView.ItemAnimator 負責處理數據添加刪除時的動畫效果
RecyclerView.Adapter 創建每一項Item視圖
RecyclerView.ViewHolder 承載Item視圖的子佈局
RecyclerView.Recycler 處理View的緩存

RecyclerView 把子 View 的 measure 和 layout 委託給了 LayoutManager,我們可以通過自定義 LayoutManager 來自定義 RecyclerView 的子 View 排列規則。Google 官方給我提供了3個實現類,LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager.
下面我們根據源碼一步步來看 RecyclerView 是怎麼實現的。

1、setLayoutManager(RecyclerView.LayoutManager layout):

設置LayoutManager的方法

public void setLayoutManager(@Nullable RecyclerView.LayoutManager layout) {
    if (layout != this.mLayout) {
        //停止滾動
        this.stopScroll();
        if (this.mLayout != null) {
            if (this.mItemAnimator != null) {
                //結束動畫
                this.mItemAnimator.endAnimations();
            }
            //移除並回收視圖
            this.mLayout.removeAndRecycleAllViews(this.mRecycler);
            //回收廢棄視圖
            this.mLayout.removeAndRecycleScrapInt(this.mRecycler);
            this.mRecycler.clear();
            if (this.mIsAttached) {
                this.mLayout.dispatchDetachedFromWindow(this, this.mRecycler);
            }

            this.mLayout.setRecyclerView((RecyclerView)null);
            this.mLayout = null;
        } else {
            this.mRecycler.clear();
        }

        this.mChildHelper.removeAllViewsUnfiltered();
        //更新layout
        this.mLayout = layout;
        ...

        //更新緩存的View數量
        this.mRecycler.updateViewCacheSize();
        //請求measure layout draw
        this.requestLayout();
    }

}

2、setAdapter(RecyclerView.Adapter adapter)\

setAdapterInternal(RecyclerView.Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews):
設置Adapter的方法

public void setAdapter(@Nullable RecyclerView.Adapter adapter) {
        this.setLayoutFrozen(false);
        this.setAdapterInternal(adapter, false, true);
        this.processDataSetCompletelyChanged(false);
        this.requestLayout();
    }
    
private void setAdapterInternal(@Nullable RecyclerView.Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) {
        ...
        this.mAdapter = adapter;
        ...
    }

setAdapter()方法中調用了setAdapterInternal()方法,在setAdapterInternal()方法中初始化,然後調用 requestLayout()方法。

public void requestLayout() {
    if (this.mInterceptRequestLayoutDepth == 0 && !this.mLayoutFrozen) {
        super.requestLayout();
    } else {
        this.mLayoutWasDefered = true;
    }

}

requestLayout()方法其實調用的是View的requestLayout()方法。

public void requestLayout() {
    ...
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    ...
}

其中mParent.requestLayout()方法調用的是ViewRootImpl的requestLayout()方法

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

scheduleTraversals()方法被執行之後會走RecyclerView的onMeasure()、onLayout()、onDraw()等繪製方法。

3、onMeasure()、onLayout()

先來看一下onMeasure()方法的源碼:

protected void onMeasure(int widthSpec, int heightSpec) {
        ...
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            //當 RecyclerView 當 Mode 是 EXACTLY 的時候大小是確定的,所以 return 退出 measure 
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                //記錄 layout 之前,view 的信息
                dispatchLayoutStep1();
            }
            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
            // consistency
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();

            // now we can get the width and height from the children.
            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.
            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 {
            ...
        }

    }
}

mAutoMeasure的值是用來標記是否自動測量的,否則就必須在 LayoutManager 中自己實現 onMeasure 來進行測量,其中Google提共的三個LayoutManager實現類的mAutoMeasure返回值都是true。

public boolean isAutoMeasureEnabled() {
    return true;
}

所以我們看 else 中的代碼就可以了,首先如果RecyclerView 的 Mode 是 EXACTLY 的時候大小是確定的,所以就 return 退出 measure。然後會調用dispatchLayoutStep1()方法,它的作用我們看一下注釋:

The first step of a layout where we;
- process adapter updates
- decide which animation should run
- save information about current views
- If necessary, run predictive layout and save its information

佈局的第一步,進程適配器更新,決定應運行哪個動畫,保存有關當前 View 的信息,如果有必要,運行 predictive layout。然後調用了dispatchLayoutStep2()方法。

/**
 * The second layout step where we do the actual layout of the views for the final state.This step * might be run multiple times if necessary (e.g. measure).
 */
private void dispatchLayoutStep2() {
    ...
    this.mLayout.onLayoutChildren(this.mRecycler, this.mState);
    ...
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    ...
}

其中都核心代碼就是調用了mLayout的onLayoutChildren()方法,這裏的 mLayout 就是RecyclerView.LayourManager對象,用來規定放置子 view 的算法,尋找錨點填充 view。所以到這裏我們可以知道RecyclerView的measure過程委託給了RecyclerView.LayourManager的onLayoutChildren()方法來處理。
再看看onLayout()方法:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...
    this.dispatchLayout();
    ...
}

其中 dispatchLayout() 方法的源碼如下:

void dispatchLayout() {
    ...
    this.mState.mIsMeasuring = false;
    if (this.mState.mLayoutStep == State.STEP_START) {
        this.dispatchLayoutStep1();
        this.mLayout.setExactMeasureSpecsFrom(this);
        this.dispatchLayoutStep2();
    } else if (!this.mAdapterHelper.hasUpdates() && this.mLayout.getWidth() == this.getWidth() && this.mLayout.getHeight() == this.getHeight()) {
        this.mLayout.setExactMeasureSpecsFrom(this);
    } else {
        this.mLayout.setExactMeasureSpecsFrom(this);
        this.dispatchLayoutStep2();
    }
    //保存 view 動畫的信息,執行動畫,狀態清理。
    this.dispatchLayoutStep3();
    
}

dispatchLayout()裏面調用了 dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3()方法,前兩個方法的作用我們之前已經說過,現在來看看 dispatchLayoutStep3() 方法,源碼太長看一下注釋:

/**
 * The final step of the layout where we save the information about views for animations,
 * trigger animations and do any necessary cleanup.
 */
 private void dispatchLayoutStep3() {
    ...
    mState.mLayoutStep = State.STEP_START;
    ...
}

所以它的功能是保存 view 動畫的信息,執行動畫,狀態清理。
這裏有個重要點就是狀態類 State,它是標記 layout 過程進行到哪一步了。在onMeasure()中當RecyclerView當Mode不是EXACTLY的時候,會執行dispatchLayoutStep1()、dispatchLayoutStep2()方法來測量子View的寬高來確定 RecyclerView自己的寬高,在onlayout()中就不需要執行了。當RecyclerView當Mode是EXACTLY的時候的時候不會執行dispatchLayoutStep1()、dispatchLayoutStep2()方法,在onlayout()中就需要執行dispatchLayoutStep1()、dispatchLayoutStep2()方法。

所以 RecyclerView的layout過程也委託給了RecyclerView.LayourManager的onLayoutChildren()方法來處理。
那麼下面以LinearLayoutmanager爲例:

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.
    // 2) fill towards start, stacking from bottom
    // 3) fill towards end, stacking from top
    // 4) scroll to fulfill requirements like stack from bottom.
    // create layout state
    ...
    final View focused = getFocusedChild();
    if (!mAnchorInfo.mValid || mPendingScrollPosition != 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));
    }
    ...
    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);
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mInfinite = resolveIsInfinite();
    mLayoutState.mIsPreLayout = state.isPreLayout();
    if (mAnchorInfo.mLayoutFromEnd) {
        // fill towards start
        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;
        }
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = 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.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
    } else {
        // fill towards end
        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;
        }
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = 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.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
        }
    }
    ...
}

根據方法中的註釋可以知道,首先確定錨點mAnchorInfo,mAnchorInfo包含了子控件在Y軸上起始繪製偏移量(coordinate),ItemView在Adapter中的索引位置(position)和佈局方向(mLayoutFromEnd)——這裏是指start、end方向

static class AnchorInfo {
    OrientationHelper mOrientationHelper;
    int mPosition;
    int mCoordinate;
    boolean mLayoutFromEnd;
    boolean mValid;
    ...
 }

然後以錨點爲起點向着開始和結束的方向填充ItemView。其中真正的填充方法是 fill() 方法:

/**
 * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
 * independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager}
 * and with little change, can be made publicly available as a helper class.
 */
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        ...
        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 || mLayoutState.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);
        }
        ...
    }
    ...
    return start - layoutState.mAvailable;
}

由上面的僞代碼可知,fill() 方法通過while循環調用 layoutChunk() 方法來填充子 View。下面看看 layoutChunk()方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    ...
    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);
        }
    }
    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;
        }
    }
    // We calculate everything with View's bounding box (which includes decor and margins)
    // To calculate correct layout position, we subtract margins.
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    ...
    result.mFocusable = view.hasFocusable();
}

其中 View 通過 next()方法取出:

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

在 next()方法中通過 recycler.getViewForPosition(mCurrentPosition) 方法獲取的 View,這裏是RecyclerView的緩存機制來,先不說。回到上面layoutChunk()方法,取到View之後通過addView()方法添加。

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