深入分析RecyclerView源碼——佈局流程(上)

開篇簡述

RecyclerView的源碼數量真的是太多啦,第一次解析打算按照分析view的標準方法,先從measure、layout和draw三大流程入手。本篇文章主要着眼於最核心的measure和layout過程。分析後會發現RecyclerView實際上是把兩個過程分由三個函數dispatchLayoutStep1,dispatchLayoutStep2和dispatchLayoutStep3去做了,onMeasure和onLayout的區別並不明顯。d1、d2、d3三函數主要負責處理預佈局、佈局和動畫,其中的佈局就是一般情況下ViewGroup的測量和佈局過程合二爲一,這部分RecyclerView交給了佈局管理器LayoutManager的onLayoutChildren方法完成,源碼中是mLayout,以下的分析均基LinearLayoutManager,它是LayoutManager的子類。除此之外,本篇還將努力理清預佈局和動畫的流程,還原RecyclerView佈局流程的全過程。

onMeasure

onMeasure函數很簡單,如果RecyclerView的width和height不都是EXACTLY,就在onMeasue裏執行佈局,並且有可能需要執行兩遍。如果width和height都是EXACTLY,跳過onMeasure直接進入onLayout。所以onMeasure和onLayout的僅僅是對於非EXACTLY和EXACTLY的不同策略,實質上都同時承擔了測量和佈局任務,對於ViewGroup來說測量和佈局往往是雜糅在一起的,之前分析過線性佈局LinearLayout的源碼對此深有體會。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    //mLayout就是我們設置的佈局管理器,如果沒有設置的話就走defaultOnMeasure
    //defaultOnMeasure只是簡單應用widthSpec和heightSpec,其他什麼都不做,等於放棄佈局
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    //如果設置了佈局管理器,返回值必爲true
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);
        //佈局管理器並不實現該方法,走的還是defaultOnMeasure,所以沒什麼用
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            
        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        //如果width和height都是絕對值直接結束onMeasure函數
        //初看可能比較懵,我開頭說過了RecyclerView的measure和layout區別不明顯
        //實際的過程是dispatchLayoutStep三兄弟實現的,所以延後到onLayout再調用也一樣
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
        //剛開始狀態必爲State.STEP_START,和dispatchLayoutStep三兄弟有關
        //有三個取值,分別代表開始、佈局、動畫
        //dispatchLayoutStep1執行完後變成STEP_LAYOUT,
        //dispatchLayoutStep2執行完後變成STEP_ANIMATIONS,
        //dispatchLayoutStep3執行完後變回STEP_START
        if (mState.mLayoutStep == State.STEP_START) {
            //三兄弟第一個出現了,d1是用來處理預佈局的,後面會詳細分析
            dispatchLayoutStep1();
        }
        //這句不是很懂,意思是SDK<23時不支持MeasureSpec.UNSPECIFIED
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        //三兄弟第二個出現了,d2是用來更新佈局的,d2完成後
        //佈局measure和layout過程就算完成了,後面會詳細分析
        dispatchLayoutStep2();
        //根據子view的大小設置自身大小,因爲自身的width或者height是不確定的
        //所以必須測完子view才能確定,當wrap_content的時候必須先知道content
        //多大才能wrap
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        //如果需要測量兩次,拿LinearLayoutManager舉例,當父view是wrap_content
        //子view是match_parent時,必須先測一遍子view,然後設定父view,最後設定
        //match_parent的子view
        if (mLayout.shouldMeasureTwice()) {
            mLayout.setMeasureSpecs(
                    MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
    } else {
        //如果設置了佈局管理器,是不可能走到這裏的,以下省略一大段代碼
        ......
    }
}

onLayout

onLayout只簡單地調用dispatchLayout,直接看dispatchLayout。

void dispatchLayout() {
    //沒有適配器和佈局管理器就不用往下走了
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        return;
    }
    mState.mIsMeasuring = false;
    //這裏判斷了mState.mLayoutStep,如果在onMeasure中因爲寬高是EXACTLY直接結束的話
    //就還是State.STEP_START,否則應該是State.STEP_ANIMATIONS
    if (mState.mLayoutStep == State.STEP_START) {
        //三兄弟之一,執行預佈局
        dispatchLayoutStep1();
        //此處不需要測量兩遍是因爲只有寬高固定纔會進來這
        mLayout.setExactMeasureSpecsFrom(this);
        //三兄弟之二,執行佈局
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        mLayout.setExactMeasureSpecsFrom(this);
    }
    //三兄弟之三,處理動畫
    dispatchLayoutStep3();
}

dispatchLayoutStep1

以下dispatchLayoutStep1爲刪減版,挑選出比較關鍵的步驟分析。d1的任務是是執行PreLayout預佈局過程。設想一個場景,對於線性佈局的RecyclerView,如果刪除了其中的某個子view,將導致後面的所有子view向上平移,並且會新加入一個子view在屏幕最後,默認情況下這個新加入的子view也是通過平移動畫進入,所以在進行動畫之前必須知道它的初始位置即從哪裏平移,但是此時該view並沒有初始位置,於是d1通過預佈局解決這個問題。當RecyclerView有n個子view發生變化或刪除時,會在d1額外佈局n個子view在屏幕外爲動畫做準備。詳細過程見源碼和註釋。

private void dispatchLayoutStep1() {
	startInterceptRequestLayout();
	//mViewInfoStore是存儲屏幕上view的動畫的,先清空
	mViewInfoStore.clear();
	//adapter執行notifyItemXXX方法時會給需要變化的view附加更新信息,
	//此時進行處理,具體見adapter和recyclerView的關係部分
	processAdapterUpdatesAndSetAnimationFlags();
	//此時mState.mInPreLayout變爲true,代表預佈局(PreLayout)正式開始
	mState.mInPreLayout = mState.mRunPredictiveAnimations;
	//第一個if是爲屏幕上顯示的view附加動畫信息
	if (mState.mRunSimpleAnimations) {
		int count = mChildHelper.getChildCount();
		for (int i = 0; i < count; ++i) {
			final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
			//只是附加了位置信息,真正添加動畫應該是在別的地方,
			//此處的位置是原始位置,即界面更新前的老位置,因爲目前還未執行佈局操作
			final ItemHolderInfo animationInfo = mItemAnimator
				.recordPreLayoutInformation(mState, holder,
				ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
				holder.getUnmodifiedPayloads());
			//開始佈局前的所有屏幕上的view添加舊位置信息到mViewInfoStore的mLayoutHolderMap容器中,flag爲FLAG_PRE
			mViewInfoStore.addToPreLayout(holder, animationInfo);
			if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                        && !holder.shouldIgnore() && !holder.isInvalid()) {
                long key = getChangedHolderKey(holder);
				//更新卻未刪除的view會把位置信息加入到mOldChangedHolders容器中
				mViewInfoStore.addToOldChangeHolders(key, holder);
            }
		}
	}
	//第二個if是爲在屏幕外的PreLayout的view附加動畫信息
	if (mState.mRunPredictiveAnimations) {
		//recyclerView一共會執行兩次onLayoutChildren,這是第一次,發生在PreLayout階段
		//主要任務是如果有n個view發生變化,則多layout n個view在屏幕外面,爲後面的進入動畫做準備
		mLayout.onLayoutChildren(mRecycler, mState);
		for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
			final View child = mChildHelper.getChildAt(i);
            final ViewHolder viewHolder = getChildViewHolderInt(child);
			if (!mViewInfoStore.isInPreLayout(viewHolder)) {
				//只有在屏幕外的PreLayout的view會進來,animationInfo和前一個if中的是一樣的,
				//也是隻附加位置信息,此處位置是在屏幕外的
                final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                    mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                if (wasHidden) {
                    recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                } else {
					//添加位置信息(屏幕外)到mViewInfoStore的mLayoutHolderMap容器中,flag爲FLAG_APPEAR
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
					//至此第一次onLayoutChildren的view全部加進了mLayoutHolderMap容器中
                }
            }
		}
	}
	stopInterceptRequestLayout(false);
	//dispatchLayoutStep1執行完代表State.STEP_START階段結束,進入State.STEP_LAYOUT階段
    mState.mLayoutStep = State.STEP_LAYOUT;
}

可以看到d1主要做了三件事:

一、調用processAdapterUpdatesAndSetAnimationFlags,這個涉及到Adapter和RecyclerView的緩存策略,後面會單獨開篇解析。

二、調用mLayout.onLayoutChildren進行預佈局(d1、d2各會調用該函數一次但目的不同),保存子view位置信息,爲動畫做準備。其中之所以經過mLayout.onLayoutChildren後子view會變多,正是因爲對於變化或刪除的子view不考慮他們佔的空間,從而能多列出n個子view到屏幕外。

三、d1還保存了所有子view的的原始位置信息,這些信息在d3中會再此用到,d3纔是真正確定動畫的地方。信息被保存到了mViewInfoStore的兩個容器中,更新卻不刪除的view的被保存到mOldChangedHolders,其他被保存到mLayoutHolderMap。之所以這樣區分是因爲更新卻不刪除的view有可能需要執行縮放動畫,其他view不需要。

dispatchLayoutStep2

由於dispatchLayoutStep2的源碼比較少,所以就全部放出來了。拋開mLayout.onLayoutChildren,d2應該是三兄弟中最簡單的,d2執行的就是ViewGroup常規的測量和佈局操作,並且是委託給佈局管理器實現的。經過d2整個界面就已經變成更新後的樣子了,但是因爲沒有執行draw操作,所以現實屏幕上暫時還看不到。onLayoutChildren是一個非常重點的方法,但是本篇着重從全局把握佈局流程,所以會單獨再開一篇解析onLayoutChildren和RecyclerView的緩存策略。

private void dispatchLayoutStep2() {
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
	//走到最後都是空方法,官方並未實現
    mAdapterHelper.consumeUpdatesInOnePass();
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

	//PreLayout預佈局階段結束,下面開始正式佈局
    mState.mInPreLayout = false;
	//第二次onLayoutChildren,執行完畢後子view已經到達所有操作結束後的位置和大小,
	//該刪除該變化的都刪除變化了,該新進入屏幕的也進來了,
	//但是這只是measure和layout完成,沒到draw所以現實屏幕上暫時看不到
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;

    mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
	//State.STEP_LAYOUT階段結束,下面進入State.STEP_ANIMATIONS階段,應該是要處理動畫相關了
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
}

dispatchLayoutStep3

d3只是處理動畫相關的一些操作,判斷每個子view(包括剛剛被刪除的view)是否需要動畫需要何種動畫(平移還是淡出)。在for循環中對每個子view在mViewInfoStore中存儲的舊位置信息做判斷,增加響應的flag以區分做何種動畫。具體操作見源碼和註釋。

private void dispatchLayoutStep3() {
    mState.assertLayoutStep(State.STEP_ANIMATIONS);
    startInterceptRequestLayout();
    onEnterLayoutOrScroll();
    mState.mLayoutStep = State.STEP_START;
    if (mState.mRunSimpleAnimations) {
		//經過第二次onLayoutChildren,此處的所有holder都是界面更新後應該在屏幕上的
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
			//key實際上就是position
            long key = getChangedHolderKey(holder);
			//這裏有兩個信息,animationInfo是新的位置信息,oldChangeViewHolder是舊的位置信息
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
			//從mViewInfoStore的mOldChangedHolders容器中按key找,在dispatchLayoutStep1中分析過
			//mOldChangedHolders存儲的是更新卻未刪除的view的舊位置信息
			//按position取出來
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
			//view更新卻不刪除if才成立
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                        oldChangeViewHolder);
                final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                if (oldDisappearing && oldChangeViewHolder == holder) {
                    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 {
						//如果view之前之後都在且有變化,直接就執行動畫,不等process方法了
                        //判斷是否變化的標準是oldChangeViewHolder和holder是否是同一個
                        //默認情況下變化的view會創建新的viewholder,所以肯定不是同一個
                        //但是如果採用局部刷新,會複用viewHolder,而不執行change動畫
                        animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                oldDisappearing, newDisappearing);
                    }
                }
            } else {
				//flag有四種:FLAG_PRE、FLAG_POST、FLAG_APPEAR、FLAG_DISAPPEARED
				//FLAG_DISAPPEARED是由addDisappearingView方法才能設定
                //用於預佈局列出,世紀佈局未進入屏幕的view
				//相當於flag增加一個FLAG_POST,只有更新後還在屏幕上且內容沒有變化的纔會執行此句
				//如果view之前之後都在且沒有變化,flag爲FLAG_PRE|FLAG_POST
				//如果view之前在之後不在,flag爲FLAG_PRE
				//如果view之前不在之後在,flag爲FLAG_APPEAR|FLAG_POST
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        //根據flag的不同確定不同的動畫
        mViewInfoStore.process(mViewInfoProcessCallback);
    }

    ......
	//做一些後續工作
    mLayout.onLayoutCompleted(mState);
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    mViewInfoStore.clear();
    if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
        dispatchOnScrolled(0, 0);
    }
    recoverFocusFromState();
    resetFocusInfo();
}

最後根據flag的不同在mViewInfoStore.process中執行相應的對話,我的源碼是androidx.recyclerview.weight包下的,所以最終執行的是屬性動畫,兼容包下的可能會有所不同。與常見方法不同,這裏的屬性動畫不是立即提交執行,而是延後到下一次屏幕刷新。

void process(ProcessCallback callback) {
	//以下動畫均是在下一次屏幕刷新時執行,而不是立即提交執行
    for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
        final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
        final InfoRecord record = mLayoutHolderMap.removeAt(index);
        if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
            //預佈局列出,實際佈局未進入屏幕,不需要動畫
            callback.unused(viewHolder);
        } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
            // Set as "disappeared" by the LayoutManager (addDisappearingView)
            //由於佈局變化(插入view或者view變大),導致view被擠出屏幕,執行平移動畫
			//FLAG_DISAPPEARED標記位是在LayoutManager的onLayoutChildren中設置的
            if (record.preInfo == null) {
                // similar to appear disappear but happened between different layout passes.
                // this can happen when the layout manager is using auto-measure
                callback.unused(viewHolder);
            } else {
                callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
            }
        } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
            // Appeared in the layout but not in the adapter (e.g. entered the viewport)
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
            //如果view之前之後都在且沒有變化,最終使用view.setTranslationX(-deltaX)
			//和view.setTranslationY(-deltaY);實現必要的平移
            callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_PRE) != 0) {
            //如果view之前在之後不在,實際執行的是透明度變0,即漸變退出animation.setDuration(getRemoveDuration()).alpha(0)
            callback.processDisappeared(viewHolder, record.preInfo, null);
        } else if ((record.flags & FLAG_POST) != 0) {
            //如果view之前不在之後在,也是執行平移動畫
            callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
        } else if ((record.flags & FLAG_APPEAR) != 0) {
            // Scrap view. RecyclerView will handle removing/recycling this.
        } else if (DEBUG) {
            throw new IllegalStateException("record without any reasonable flag combination:/");
        }
        InfoRecord.recycle(record);
    }
}

結語

通過閱讀d1、d2、d3的源碼能大致看到RecyclerView更新時的操作,如果是第一次顯示自然不會進行預佈局(相關條件爲false),不存在舊位置信息,d3的動畫也就無從談起。如果Adapter調用了notifyItemXXX系列方法引起數據變化,RecyclerView在進行新佈局前會先進行預佈局,把所有可能需要動畫的子view全部列出來,然後再執行新佈局,最後在d3中確定子view的動畫。分析完畢會發現並不複雜,當然RecyclerView的內容遠不止於此,後續還會開很多篇繼續深入解析RecyclerView源碼。

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