RecyclerView 原始碼分析(一) —— 繪製流程解析

huansky發表於2021-01-17

概述

對於 RecyclerView 是那麼熟悉又那麼陌生。熟悉是因為作為一名 Android 開發者,RecyclerView 是經常會在專案裡面用到的,陌生是因為只是知道怎麼用,但是卻不知道 RecyclerView 的內部實現機制。

但凡是一位有所追求的開發者,都不會只讓自己停留在只會使用上,而是會研讀相關原始碼,知其然知其所以然。

對於 RecyclerView 的原始碼解析一篇文章肯定是不夠的,後續有時間會繼續更新。每一篇都會有自己的主題。RecyclerView 的使用,本文也就不講了,具體可以檢視之前的文章:RecyclerView 使用指南

對於用過 RecyclerView 的開發者來說,這個 View 的功能確實強大,可以在日常開發的很多場景都可以使用。在講解 RecyclerView 繪製原始碼的時候,我希望大家去思考一些問題:

  1. 如果是你,你會怎麼來設計 RecyclerView 的繪製過程,和普通的 View 一樣?

  2. RecyclerView 可以支援不同的流式佈局,一列,多列,所以裡面的繪製邏輯它是如何設計的?

  3. 分割線是可以定製的,那我要如何設計這塊的程式碼?

其實也還有其他的問題,但是本文只講繪製流程,因此,其他問題就在其他模組去思考。要是在以前呢,我也是為了分析原始碼而分析原始碼,然後把文章發出去。很少去思考原始碼背後的一些東西。直到最近自己需要去重構一個模組的時候,發現設計一個技術方案是多麼的難。

本文原始碼版本:androidx.recyclerView:1.1.0 

measure 測量

對於 view 來說,必有的三大流程:測量,佈局,繪製。因此 RecyclerView 也是一樣。如果你現在還是對 View 的繪製流程,不瞭解可以推薦看文章:

 下面進入正題,首先來看下 RecyclerView 類的定義:

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
     // ......  
}

 可以看到 RecyclerView 是一個 ViewGroup,也就是說,RecyclerView 本質是一個自定義 view,需要自己去管理繪製流程。對於瞭解自定義 View 來說,其實就是需要重寫 onMeasure 方法。

在 Android 自定義 View 詳解 一文中總結了 onMeausre 的具體邏輯,到這裡,依然可以做個參考:

  1. super.onMeasure 會先計算自定義 view 的大小;

  2. 呼叫 measureChild 對子 View 進行測量;
  3. 自定義 view 設定的寬高引數不是 MeasureSpec.EXACTLY 的話,對於子 View 是 match_parent 需要額外處理,同時也需要對 MeasureSpec.AT_MOST 情況進行額外處理。

  4.  當自定義View 的大小確定後,在對子 View 是 match_parent 重新測量;

下面來看下 RecyclerView 的 onMeausre 程式碼:

    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            // 第一種情況
        }
        if (mLayout.isAutoMeasureEnabled()) {
            // 第二種情況
        } else {
            // 第三種情況
        }
    }

onMeasure方法還是有點長,這裡我將它分為3種情況,我將簡單解釋這三種情況:

  1. mLayout 即 LayoutManager 的物件。我們知道,當 RecyclerView 的 LayoutManager 為空時,RecyclerView 不能顯示任何的資料,在這裡我們找到答案。

  2. LayoutManager 開啟了自動測量時,這是一種情況。在這種情況下,有可能會測量兩次。

  3. 第三種情況就是沒有開啟自動測量的情況,這種情況比較少,因為 RecyclerView 為了支援 warp_content 屬性,系統提供的 LayoutManager 都開啟自動測量的,不過還是要分析的。

首先我們來第一種情況。

1、LayoutManager == null

這種情況下比較簡單,我們來看看原始碼:

        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }

這裡是呼叫了 defaultOnMeasure 方法,

    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.
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));

        setMeasuredDimension(width, height);
    }

在 defaultOnMeasure 方法裡面,主要是通過 LayoutManager 的 chooseSize 方法來計算寬高,最後呼叫 setMeasuredDimension 方法來設定寬高。下面來看下 chooseSize 的具體邏輯:

        public static int chooseSize(int spec, int desired, int min) {
            final int mode = View.MeasureSpec.getMode(spec);
            final int size = View.MeasureSpec.getSize(spec);
            switch (mode) {
                case View.MeasureSpec.EXACTLY:
                    return size;
                case View.MeasureSpec.AT_MOST:
                    return Math.min(size, Math.max(desired, min));
                case View.MeasureSpec.UNSPECIFIED:
                default:
                    return Math.max(desired, min);
            }
        }

 

這裡主要是根據不同的設定,來返回最終的大小。這塊邏輯不是很懂的讀者可以閱讀前面提到的文章,裡面詳細解讀了。但是這裡有個問題需要指出來的就是沒有測量子 view 的大小,這也是白屏的原因。因為 RecyclerView 的繪製其實是委託給 LayoutManager 來管理呢,LayoutManager = null 的情況下測量子 view 沒有任何的意義。

2、LayoutManager 開啟了自動測量

在分析這種情況之前,我們先對了解幾個東西。

  RecyclerView 的測量分為兩步,分別呼叫 dispatchLayoutStep1 和 dispatchLayoutStep2。同時,瞭解過 RecyclerView 原始碼的同學應該知道在 RecyclerView 的原始碼裡面還一個dispatchLayoutStep3 方法。這三個方法的方法名比較接近,所以容易讓人搞混淆。本文會詳細的講解這三個方法的作用。

  由於在這種情況下,只會呼叫 dispatchLayoutStep1 和 dispatchLayoutStep2 這兩個方法,所以這裡會重點的講解這兩個方法。而 dispatchLayoutStep3 方法的呼叫在RecyclerView 的 onLayout 方法裡面,所以在後面分析 onLayout 方法時再來看 dispatchLayoutStep3 方法。

  我們在分析之前,先來看一個東西 —— mState.mLayoutStep。這個變數有幾個取值情況。我們分別來看看:

取值含義

State.STEP_START

mState.mLayoutStep 的預設值,這種情況下,表示 RecyclerView 還未經歷 dispatchLayoutStep1,因為 dispatchLayoutStep1 呼叫之後mState.mLayoutStep 會變為 State.STEP_LAYOUT

State.STEP_LAYOUT

當 mState.mLayoutStep 為 State.STEP_LAYOUT 時,表示此時處於 layout 階段,這個階段會呼叫 dispatchLayoutStep2 方法 layout RecyclerView children。呼叫 dispatchLayoutStep2 方法之後,此時 mState.mLayoutStep 變為了 State.STEP_ANIMATIONS

State.STEP_ANIMATIONS

當 mState.mLayoutStep為 State.STEP_ANIMATIONS 時,表示 RecyclerView 處於第三個階段,也就是執行動畫的階段,也就是呼叫 dispatchLayoutStep3方法。當 dispatchLayoutStep3 方法執行完畢之後,mState.mLayoutStep 又變為了 State.STEP_START

從上表中,我們瞭解到 mState.mLayoutStep 的三個狀態對應著不同的 dispatchLayoutStep 方法。這一點,我們必須清楚,否則接下來的程式碼將難以理解。

        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.
             */
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

            final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }
       // 開始測量
            if (mState.mLayoutStep == State.STEP_START) {
                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); } }

 首先,我們來看看 onMeasure 方法。

        public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,
                int heightSpec) {
            mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
        }
發現呼叫的 RecyclerView 的 defaultOnMeasure 方法,其實就是前面我們介紹過的自定義 View 的三個步驟:先是測量自己本身的大小。

dispatchLayoutStep1

我們們再接著往下走,看看 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
     */
    private void dispatchLayoutStep1() {
     // 這裡還用到了斷言 mState.assertLayoutStep(State.STEP_START); fillRemainingScrollValues(mState); mState.mIsMeasuring
= false; startInterceptRequestLayout(); mViewInfoStore.clear(); onEnterLayoutOrScroll();
     // 處理 adapter 更新 processAdapterUpdatesAndSetAnimationFlags(); saveFocusInfo(); mState.mTrackOldChangeHolders
= mState.mRunSimpleAnimations && mItemsChanged; mItemsAddedOrRemoved = mItemsChanged = false; mState.mInPreLayout = mState.mRunPredictiveAnimations; mState.mItemCount = mAdapter.getItemCount(); findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);      // 是否要執行動畫 if (mState.mRunSimpleAnimations) { // Step 0: Find out where all non-removed items are, pre-layout int count = mChildHelper.getChildCount(); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) { continue; } final ItemHolderInfo animationInfo = mItemAnimator .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); mViewInfoStore.addToPreLayout(holder, animationInfo); if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved() && !holder.shouldIgnore() && !holder.isInvalid()) { long key = getChangedHolderKey(holder); // This is NOT the only place where a ViewHolder is added to old change holders // list. There is another case where: // * A VH is currently hidden but not deleted // * The hidden item is changed in the adapter // * Layout manager decides to layout the item in the pre-Layout pass (step1) // When this case is detected, RV will un-hide that view and add to the old // change holders list. mViewInfoStore.addToOldChangeHolders(key, holder); } } } 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. saveOldPositions(); final boolean didStructureChange = mState.mStructureChanged; mState.mStructureChanged = false; // temporarily disable flag because we are asking for previous layout mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = didStructureChange; for (int i = 0; i < mChildHelper.getChildCount(); ++i) { final View child = mChildHelper.getChildAt(i); final ViewHolder viewHolder = getChildViewHolderInt(child); if (viewHolder.shouldIgnore()) { continue; } if (!mViewInfoStore.isInPreLayout(viewHolder)) { int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder); boolean wasHidden = viewHolder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (!wasHidden) { flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; } final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation( mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads()); if (wasHidden) { recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo); } else { mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); } } } // we don't process disappearing list because they may re-appear in post layout pass. clearOldPositions(); } else { clearOldPositions(); } onExitLayoutOrScroll(); stopInterceptRequestLayout(false);
     // 上面的執行完以後,改變狀態 mState.mLayoutStep
= State.STEP_LAYOUT; }

 其實最上面的方法註釋,已經把這個方法所做的事情都總結好了,該方法主要工作如下:

  1. 處理 Adapter 更新;

  2. 決定執行哪一種動畫

  3. 儲存每個 ItemView 的資訊

  4. 有必要的話,會進行預佈局,並把相關資訊儲存下來。

可以看到整個方法內部呼叫的方法還是很多,導致你會覺得這個方法的邏輯很複雜。不過既然是原始碼閱讀,我們們只關注一些重要的點,在眾多被呼叫的方法中 processAdapterUpdatesAndSetAnimationFlags 是需要點進去看看裡面的邏輯的,後續的 if else 邏輯其實都是在該方法裡面決定的。

    /**
     * Consumes adapter updates and calculates which type of animations we want to run.
     * Called in onMeasure and dispatchLayout.
     * <p>
     * This method may process only the pre-layout state of updates or all of them.
     */
    private void processAdapterUpdatesAndSetAnimationFlags() {
        if (mDataSetHasChangedAfterLayout) {
            // Processing these items have no value since data set changed unexpectedly.
            // Instead, we just reset it.
            mAdapterHelper.reset();
            if (mDispatchItemsChangedEvent) {
                mLayout.onItemsChanged(this);
            }
        }
        // simple animations are a subset of advanced animations (which will cause a
        // pre-layout step)
        // If layout supports predictive animations, pre-process to decide if we want to run them
        if (predictiveItemAnimationsEnabled()) {
            mAdapterHelper.preProcess();
        } else {
            mAdapterHelper.consumeUpdatesInOnePass();
        }
        boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
        mState.mRunSimpleAnimations = mFirstLayoutComplete
                && mItemAnimator != null
                && (mDataSetHasChangedAfterLayout
                || animationTypeSupported
                || mLayout.mRequestedSimpleAnimations)
                && (!mDataSetHasChangedAfterLayout
                || mAdapter.hasStableIds());
        mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                && animationTypeSupported
                && !mDataSetHasChangedAfterLayout
                && predictiveItemAnimationsEnabled();
    } 
這裡我們的重心放在 mFirstLayoutComplete 變數裡面,我們發現 mRunSimpleAnimations 的值與mFirstLayoutComplete有關,mRunPredictiveAnimations同時跟mRunSimpleAnimations有關。所以這裡我們可以得出一個結論,當RecyclerView第一次載入資料時,是不會執行的動畫?那到底會不會呢,這裡先賣個關子。

dispatchLayoutStep2

接下來我們看看 dispatchLayoutStep2 方法,這個方法是真正佈局 children。上程式碼:
    /**
     * 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() {
        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;
        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);
    }

可以看到的是,這裡的邏輯似乎簡單很多,那是因為這裡把對子 view 的繪製邏輯放到 LayoutManager 中去了。到這裡,state 的狀態已經改變了,變成了 State.STEP_LAYOUT | State.STEP_ANIMATIONS。

系統的 LayoutManager 的 onLayoutChildren 方法是一個空方法,所以需要 LayoutManager 的子類自己來實現。

這裡先不做過多介紹,不同的 LayoutManager 有不同的實現。

3、沒有開啟自動測量

還是先來看看這一塊的程式碼:

 {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            // custom onMeasure
            if (mAdapterUpdateDuringMeasure) {
                startInterceptRequestLayout();
                onEnterLayoutOrScroll();
                processAdapterUpdatesAndSetAnimationFlags();
                onExitLayoutOrScroll();

                if (mState.mRunPredictiveAnimations) {
                    mState.mInPreLayout = true;
                } else {
                    // consume remaining updates to provide a consistent state with the layout pass.
                    mAdapterHelper.consumeUpdatesInOnePass();
                    mState.mInPreLayout = false;
                }
                mAdapterUpdateDuringMeasure = false;
                stopInterceptRequestLayout(false);
            } else if (mState.mRunPredictiveAnimations) {
                // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
                // this means there is already an onMeasure() call performed to handle the pending
                // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout
                // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time
                // because getViewForPosition() will crash when LM uses a child to measure.
                setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
                return;
            }

            if (mAdapter != null) {
                mState.mItemCount = mAdapter.getItemCount();
            } else {
                mState.mItemCount = 0;
            }
            startInterceptRequestLayout();
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            stopInterceptRequestLayout(false);
            mState.mInPreLayout = false; // clear

 這裡主要做了兩件事,其實跟第二個步驟很像,最終都會呼叫 LayoutManager 的 onMeasure 方法來進行測量。

  • 如果mHasFixedSize為true(也就是呼叫了setHasFixedSize方法),將直接呼叫LayoutManageronMeasure方法進行測量。

  • 如果mHasFixedSize為false,同時此時如果有資料更新,先處理資料更新的事務,然後呼叫LayoutManageronMeasure方法進行測量

onLayout 佈局

到這裡,關於測量的邏輯就講完了,接下去開始看 layout 邏輯:

    @Override
    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 進行 layout 操作,可以看到前面關注過的一個變數 mFirstLayoutComplete 賦值變為 true 。

下面主要看 dispatchLayout 方法:

    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;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            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.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

dispatchLayout 方法也是非常的簡單,這個方法保證 RecyclerView 必須經歷三個過程 —— dispatchLayoutStep1dispatchLayoutStep2dispatchLayoutStep3。同時,如果在這時候,發現子 view 寬高引數發生變化後,會再次呼叫 dispatchLayoutStep2() 方法。

最後,來看下千呼萬喚使出來的 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.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 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); 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 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 做動畫 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(); } mLayout.onLayoutCompleted(mState); onExitLayoutOrScroll(); stopInterceptRequestLayout(false); mViewInfoStore.clear();
//
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) { dispatchOnScrolled(0, 0); } recoverFocusFromState(); resetFocusInfo(); }

從上面的邏輯可以看出 dispatchLayoutStep3 主要是做 Item 的動畫,本文不對動畫進行展開,所以先省略動畫部分。然後就是對一些標誌位復位。清除一些狀態。

小結

這裡對這三個方法做一個小結,方便大家記住這幾個方法的作用:

方法名作用
dispatchLayoutStep1

本方法的作用主要有三點:

  1. 處理 Adapter 更新;

  2. 決定執行哪一種動畫

  3. 儲存每個 ItemView 的資訊

  4. 有必要的話,會進行預佈局,並把相關資訊儲存下來。

dispatchLayoutStep2 在這個方法裡面,真正進行 children 的測量和佈局。
dispatchLayoutStep3 這個方法的作用執行在 dispatchLayoutStep1 方法裡面儲存的動畫資訊。本方法不是本文的介紹重點


 3、Draw 繪製

接下來,我們來分析三大流程的最後一個階段 —— draw。

下面來看看 RecyclerView 的 draw() 和 onDraw() 方法:

    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        // ......
}    

 真是考慮周到啊。

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

發現這裡並沒有做太多,只是呼叫 ItemDecoration 的 onDraw 和 onDrawOver 方法。這樣就將分割線新增到其中。

4、 LinearLayoutManager

前面在介紹 dispatchLayoutStep2 方法時,只是簡單的介紹了,RecyclerView 通過呼叫 LayoutManager 的 onLayoutChildren 方法。LayoutManager 本身對這個方法沒有進行實現,所以必須得看看它的子類,這裡以 LinearLayoutManager 來舉例說明:

onLayoutChildren

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // ......

        ensureLayoutState();
        mLayoutState.mRecycle = false;
        // resolve layout direction
        resolveShouldLayoutReverse();
     // ......
     // calculate anchor position and coordinate
     updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
     mAnchorInfo.mValid = true;
// 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) {
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = 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.mExtraFillSpace = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }

       
        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
        if (!state.isPreLayout()) {
            mOrientationHelper.onLayoutComplete();
        } else {
            mAnchorInfo.reset();
        }
        mLastStackFromEnd = mStackFromEnd;
        if (DEBUG) {
            validateChildOrder();
        }
    }

onLayoutChildren 方法很長,因此省略一些無關的程式碼。其實主要是做兩件事確定錨點的資訊,這裡面的資訊包括:

  1. 1Children 的佈局方向,有 start 和 end 兩個方向;

  2. mPosition mCoordinate,分別表示 Children 開始填充的 position 和座標。

根據錨點資訊,呼叫 fill 方法進行 Children 的填充。這個過程中根據錨點資訊的不同,可能會呼叫兩次 fill 方法。

updateAnchorInfoForLayout

要想看錨點資訊的計算過程,我們可以從 updateAnchorInfoForLayout 方法裡面來找出答案,我們來看看 updateAnchorInfoForLayout 方法:

    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
            AnchorInfo anchorInfo) {
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from pending information");
            }
            return;
        }

        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            if (DEBUG) {
                Log.d(TAG, "updated anchor info from existing children");
            }
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "deciding anchor info for fresh state");
        }
        anchorInfo.assignCoordinateFromPadding();
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }

我相信通過上面的程式碼註釋,大家都能明白 updateAnchorInfoForLayout 方法到底幹了嘛,這裡我簡單分析一下這三種確定所做的含義,具體是怎麼做的,這裡就不討論。

  1. 第一種計算方式,表示含義有兩種:1. RecyclerView 被重建,期間回撥了 onSaveInstanceState 方法,所以目的是為了恢復上次的佈局;2. RecyclerView 呼叫了scrollToPosition 之類的方法,所以目的是讓

  2. RecyclerView 滾到準確的位置上去。所以,錨點的資訊根據上面的兩種情況來計算。

  3. 第二種計算方法,從C hildren 上面來計算錨點資訊。這種計算方式也有兩種情況:1. 如果當前有擁有焦點的 Child,那麼有當前有焦點的 Child 的位置來計算錨點;2. 如果沒有 child 擁有焦點,那麼根據佈局方向(此時佈局方向由 mLayoutFromEnd 來決定)獲取可見的第一個 ItemView 或者最後一個 ItemView

  4. 如果前面兩種方式都計算失敗了,那麼採用第三種計算方式,也就是預設的計算方式。

fill 填充佈局

然後就是呼叫 fill 方法來填充 Children。在正式分析填充過程時,我們先來看一張圖片:

 

上圖形象的展現出三種fill的情況。其中,我們可以看到第三種情況,fill 方法被呼叫了兩次。

我們看看 fill 方法:

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // ······
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // ······
            layoutChunk(recycler, state, layoutState, layoutChunkResult);

        }
         // ······
    }

fill 方法的程式碼比較長,其實都是來計算可填充的空間,真正填充 Child 的地方是 layoutChunk 方法。我們來看看 layoutChunk 方法。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    ...
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    }
    ...
    measureChildWithMargins(view, 0, 0);
    ...
    // We calculate everything with View's bounding box (which includes decor and margins)
    // To calculate correct layout position, we subtract margins.
    layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
            right - params.rightMargin, bottom - params.bottomMargin);
    ...
}

提醒下別小看這個 next 方法,RecyclerView 快取機制的起點就是從這個方法開始,可想而知,這個方法到底為我們做了多少事情。

這裡的 addView() 方法,其實就是 ViewGroup 的 addView() 方法;measureChildWithMargins() 方法看名字就知道是用於測量子控制元件大小的,這裡我先跳過這個方法的解釋,放在後面來做,目前就簡單地理解為測量子控制元件大小就好了。下面是 layoutDecoreated() 方法:

        public void layoutDecorated(@NonNull View child, int left, int top, int right, int bottom) {
       // 將分割線考慮進去
final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; child.layout(left + insets.left, top + insets.top, right - insets.right, bottom - insets.bottom); }

 

總結上面程式碼,在 RecyclerView 的 measure 及 layout 階段,填充 ItemView 的演算法為:向父容器增加子控制元件,測量子控制元件大小,佈局子控制元件,佈局錨點向當前佈局方向平移子控制元件大小,重複上訴步驟至 RecyclerView 可繪製空間消耗完畢或子控制元件已全部填充。

這樣所有的子控制元件的 measure 及 layout 過程就完成了。回到 RecyclerView 的 onMeasure 方法,執行 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec) 這行程式碼的作用就是根據子控制元件的大小,設定 RecyclerView 的大小。至此,RecyclerView 的 measure 和 layout 實際上已經完成了。

但是,你有可能已經發現上面過程中的問題了:如何確定 RecyclerView 的可繪製空間?不過,如果你熟悉 android 控制元件的繪製機制的話,這就不是問題。其實,這裡的可繪製空間,可以簡單地理解為父容器的大小;更準確的描述是,父容器對 RecyclerView 的佈局大小的要求,可以通過 MeasureSpec.getSize() 方法獲得。

總結

到這裡,關於 RecyclerView 的繪製流程就講完了,由於主打繪製流程,沒有分析其他,可能會導致整個邏輯有些跳躍,但不妨礙理解整個繪製過程。

最後回到文章前面的問題上,可以發現 RecyclerView 將繪製過程其實是委託給 layoutManager 來操作,這和普通自定義 view 是很不一樣的。這樣的靈活操作,可以讓使用者自定義各種樣式,使得 RecyclerView 使用場景變得更加豐富。

其次在於分割線的處理上,它並不把分割線當做是子 view 來處理,而是在佈局子 view 的時候,將分割線考慮進去給留下間隙。

 

參考文章

RecyclerView 原始碼分析(一) - RecyclerView的三大流程

Android中RecyclerView原始碼解析

相關文章