開篇簡述
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源碼。