天天看點

android局部布局重新整理,Android RecyclerView 局部重新整理分析

前情回顧

之前寫的 PowerAdapter 和 SelectPowerAdapter 從建立到現在,已經兩年多,期間發生了翻天覆地的變化。一開始,我把 SwipeRefreshLayout 和 RecyclerView 直接耦合在一起,布局就寫一個控件,爽。因為那會兒業務場景是那樣,沒有考慮靈活性。後來改業務頭部不能直接用 SwipeRefreshLayout ,突然才意識到這樣局限性太大。于是乎 Android 打造專屬的下拉重新整理 加載更多 就這麼出現,的确也有實際的應用場景。甚至還狂熱地做了那會 UC 浏覽器下拉重新整理的效果

。在這之後,我将頭部拆分開,PowerAdapter 什麼的就用于簡化實作加載更多已經多布局填充。SelectPowerAdapter 繼承自 PowerAdapter ,寫的及其簡陋。用于實作簡單的單選和多選。

更新

這倆 Adapter 就是對于已有 Adapter 功能的裝飾,友善調用者實作一些常用功能。再這次之前,陸陸續續已經豐富了一些功能,比如說 SelectAdaper 中以前真是很簡陋,現在慢慢已經能實作 單選 多選 反選 選中删除 限制最大選中數量 等基本功能。PowerAdapter 則将 增删改查等基本功能完善。

因為沒有相關規範,一些方法那會兒很随意。現在一些方法被廢除,比如說之前寫的 adapter.attachRecyclerView() ,為什麼要廢除呢,因為 adapter 直接就有 onAttachedToRecyclerView() 的方法,是以,根本就不需要添加這個方法。那會疏(cai)忽

(ji)和想當然就直接加上。

還有最重要就是加入了局部重新整理這個好東西,一開始是沒有考慮到這個地方,後面看文檔,才發現這麼好一功能差點兒被遺忘。怎麼了解局部重新整理和使用,将是接下來文章的重點。

我們知道,RecyclerView 中已經添加了 notifyItemChange() notifyItemRemove() 等等單個條目更改的方法,大方向說,這個相對于 ListView 或者 notifyDataChange() 方法 , 它已經算是做到局部重新整理。小方向再說,這些添加的重新整理方法,其實預設都是帶有動畫效果,具體效果是 DefaultItemAnimator 來控制和處理,就是因為動畫效果,讓開發時會出現一些意料之外的狀況。

假設我們現在有一個上傳照片的場景,我們每一個 ViewHolder 帶有一張圖檔,然後上面有一個進度條,進度根據上傳進度實時傳回。如果你使用 notifyItemChange() 來更新動畫的話,那麼會有兩個問題:第一,你會發現每重新整理一次,整個布局都會閃動一下。第二,這個進度的數值我需要怎麼傳遞才好呢?在 ViewHolder 的 Bean 對象中添加一個 progress 臨時字段?

針對上面兩個問題,我其實還額外想問一個問題,也算前置問題。如果我們多次調用 notifyItemChange() 方法,條目會重新整理多次嗎?

另外針對局部重新整理,還有兩個問題,第一,notifyItemChange() 和 真正的局部重新整理 同一個位置,ViewHolder 是同一個對象嗎?第二,局部重新整理是沒有設定動畫效果嗎?

帶着這些問題,開始 RecyclerView 這一部分源碼探索,接下來的所有源碼基于 Android API 27 Platform。

notifyItemChange() 第二個參數

上面說了半天 RecyclerView 真正的局部重新整理,但是,到底怎麼就是局部重新整理呢?其實很簡單,看看 notifyItemChange(int position) 另外一個重載函數。

public final void notifyItemChanged(int position, @Nullable Object payload) {

mObservable.notifyItemRangeChanged(position, 1, payload);

}

同時,在 Adapter 中,與 onBindViewHolder(@NonNull VH holder, int position) 相對應,也有一個重載函數。預設實作就走上面這個方法。

public void onBindViewHolder(@NonNull VH holder, int position,

@NonNull List payloads) {

onBindViewHolder(holder, position);

}

好了,這就是 RecyclerView 局部重新整理相關 API 的差異。其實對于第一個額外問題(如果我們多次調用 notifyItemChange() 方法,條目會重新整理多次嗎?),從上面兩個方法中,我們就能猜到一些答案,多次調用應該隻會回調重新整理一次,你看傳入的 payload 是一個 Object,但是到 onBindViewHolder() 方法時參數卻成了一個集合,那應該就是有合并的操作。另外再從性能上說,連着 notify 多次,就重新 measure layout 多次的話,這個開銷也是很大并且沒有必要(RecyclerView 嚴格控制 requestLayout() 方法調用),真沒必要。結果真是這樣嗎,直接看相關源碼。

//RecyclerView

public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {

// fallback to onItemRangeChanged(positionStart, itemCount) if app

// does not override this method.

onItemRangeChanged(positionStart, itemCount);

}

//RecyclerViewDataObserver

@Override

public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {

assertNotInLayoutOrScroll(null);

if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {

triggerUpdateProcessor();

}

}

// AdapterHelper

void triggerUpdateProcessor() {

if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {

ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);

} else {

mAdapterUpdateDuringMeasure = true;

requestLayout();

}

}

在調用 notifyItemChange() 方法(不管一個參數還是兩個個參數)之後,最後都會走到 notifyItemRangeChanged(int positionStart, int itemCount,@Nullable Object payload) ,最後回調到 RecyclerViewDataObserver. onItemRangeChanged() 方法。在該方法中,有一個 triggerUpdateProcessor() 方法,它本質上說,就是去請求重新布局。那就是說,這裡隻有 if 條件成立,才會去 requestLayout() ,接下來,搞清楚什麼時候 if 成立,就能回答這個前置問題。

boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {

if (itemCount < 1) {

return false;

}

mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));

mExistingUpdateTypes |= UpdateOp.UPDATE;

return mPendingUpdates.size() == 1;

}

到這裡,兩個發現:第一,size==1 說明就第一次調用是才傳回 true 才觸發 requestLayout() ;第二,payload 參數在這裡被包裝為對象,放入 mPendingUpdates 這個集合中。第一個發現,徹底證明上訴猜測是正确的,即使你調用 notify 多次,其實隻有第一次會觸發 requestLayout() 。

逃不走的 measure layout

既然有 requestLayout() 調用,那麼就回到 onMeasure() 和 onLayout() 這些方法中。

@Override

protected void onMeasure(int widthSpec, int heightSpec) {

if (mLayout == null) {

defaultOnMeasure(widthSpec, heightSpec);

return;

}

if (mLayout.isAutoMeasureEnabled()) {

final int widthMode = MeasureSpec.getMode(widthSpec);

final int heightMode = MeasureSpec.getMode(heightSpec);

mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

final boolean measureSpecModeIsExactly =

widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;

if (measureSpecModeIsExactly || mAdapter == null) {

return;

}

...

}

...

}

假設我們 RecyclerView 布局就是兩個 match_parent 或者有一個精确值,那麼執行的代碼片段就是這樣。接着再看看 onLayout() 。

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

dispatchLayout();

mFirstLayoutComplete = true;

}

void dispatchLayout() {

...

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();

}

在 RecyclerView 的 onLayout() 方法中,一共執行三大步驟。從命名上已經能清楚看懂。對于三個 step ,每個方法上面都有詳細注釋。翻譯過來就是說,第一步時,處理 Adapter 更新,決定執行的動畫效果,儲存目前 Views 的資訊,最後,如果必要的話,預測布局并儲存相關資訊;第二步時,根據最終狀态執行布局,并且可能執行多次;第三步,儲存 View 資訊,執行動畫,最後做一些清除重置操作。

道理我都懂,但是還是過不好這一生。這是另外一個極簡翻譯

由于篇(neng)幅(li)有(bu)限(xing),接下來對 step 方法隻做本文相關及局部重新整理相關代碼的解析(隻關心上面提到的幾個問題),RecyclerView 代碼太 TM 多了,一次是啃不完,搞不好一輩子可能也啃不完。

dispatchLayoutStep1

處理 Adapter 更新,決定執行的動畫效果,儲存目前 Views 的資訊,最後,如果必要的話,預測布局并儲存相關資訊。

private void dispatchLayoutStep1() {

mState.assertLayoutStep(State.STEP_START);

startInterceptRequestLayout();

//1.更新 mRunSimpleAnimations 和 mRunPredictiveAnimations flag 其實還有其他一些騷操作

processAdapterUpdatesAndSetAnimationFlags();

//2.mInPreLayout 設定為 true 後面有用

mState.mInPreLayout = mState.mRunPredictiveAnimations;

...

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());

//5.儲存動畫資訊相關

mViewInfoStore.addToPreLayout(holder, animationInfo);

if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()

&& !holder.shouldIgnore() && !holder.isInvalid()) {

//3.如果holder确定要更新,就把它添加到 oldChangeHolders 集合中

long key = getChangedHolderKey(holder);

mViewInfoStore.addToOldChangeHolders(key, holder);

}

}

}

if (mState.mRunPredictiveAnimations) {

...

//4.很重要,LayoutManager 開始工作

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)) {

...

//5.儲存動畫資訊相關

mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);

}

}

...

} ...

onExitLayoutOrScroll();

stopInterceptRequestLayout(false);

mState.mLayoutStep = State.STEP_LAYOUT;

}

一共額外注釋五點,第一點,更新動畫相關辨別位 mRunSimpleAnimations ,mRunPredictiveAnimations,後面的操作都依賴它們。第二點,将 mInPreLayout 的狀态和 mRunPredictiveAnimations 同步。這個在後面的步驟中也需要使用。第三點,儲存需要更新的 ViewHolder 到 oldChangeHolder 集合中。第四點,調用 LayoutManager. onLayoutChildren() 。第五點,儲存相關動畫資訊。

腦子不能亂,我們現在就關心三個問題,第一個, payload 參數怎麼傳遞到 ViewHolder 中;第二個,動畫效果是否和 payload 有關系;第三個 ViewHolder 重新整理時到底是不是同一個對象。

//RecyclerView

private void processAdapterUpdatesAndSetAnimationFlags() {

...

// 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

...

mAdapterHelper.preProcess();

...

boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;

// 通常情況就是 ture

mState.mRunSimpleAnimations = mFirstLayoutComplete

&& mItemAnimator != null

&& (mDataSetHasChangedAfterLayout

|| animationTypeSupported

|| mLayout.mRequestedSimpleAnimations)

&& (!mDataSetHasChangedAfterLayout

|| mAdapter.hasStableIds());

// 通常情況就是 ture

mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations

&& animationTypeSupported

&& !mDataSetHasChangedAfterLayout

&& predictiveItemAnimationsEnabled();

}

//AdapterHelper

void preProcess() {

mOpReorderer.reorderOps(mPendingUpdates);

final int count = mPendingUpdates.size();

for (int i = 0; i < count; i++) {

UpdateOp op = mPendingUpdates.get(i);

switch (op.cmd) {

...

case UpdateOp.UPDATE:

applyUpdate(op);

break;

}

...

}

mPendingUpdates.clear();

}

//AdapterHelper

private void postponeAndUpdateViewHolders(UpdateOp op) {

if (DEBUG) {

Log.d(TAG, "postponing " + op);

}

mPostponedList.add(op);

switch (op.cmd) {

...

case UpdateOp.UPDATE:

mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);

break;

default:

throw new IllegalArgumentException("Unknown update op type for " + op);

}

}

上面幾個方法,涉及到 RecyclerView 和 Adapter 互動,首先執行 mAdapterHelper.preProcess() 後,會将剛剛上文說到的onItemRangeChanged() 方法中的 payload 包裝成 UpdateOp 對象,到這裡,要開始處理這個對象。

UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {

this.cmd = cmd;

this.positionStart = positionStart;

this.itemCount = itemCount;

this.payload = payload;

}

cmd 對應我們的操作,這裡就是 update,後面就是 notifyItemRangeChange() 方法中對應的參數。AdapterHelper 最後會使用 callback 回調到 RecyclerView 中,在 RecyclerView 中執行 viewRangeUpdate() 方法。這個 callback 是 RecyclerView 在建立時就已經設定。

//RecyclerView 初始化是調用

void initAdapterManager() {

mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {

....

@Override

public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {

viewRangeUpdate(positionStart, itemCount, payload);

mItemsChanged = true;

}

});

}

//RecyclerView

void viewRangeUpdate(int positionStart, int itemCount, Object payload) {

final int childCount = mChildHelper.getUnfilteredChildCount();

final int positionEnd = positionStart + itemCount;

for (int i = 0; i < childCount; i++) {

final View child = mChildHelper.getUnfilteredChildAt(i);

final ViewHolder holder = getChildViewHolderInt(child);

if (holder == null || holder.shouldIgnore()) {

continue;

}

if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {

// 很重要,在這裡更新了 Flag 然後将 payload 傳遞到 Viewholder 中

holder.addFlags(ViewHolder.FLAG_UPDATE);

holder.addChangePayload(payload);

// lp cannot be null since we get ViewHolder from it.

((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;

}

}

mRecycler.viewRangeUpdate(positionStart, itemCount);

}

到這裡,我們可以看到,在 viewRangeUpdate() 方法中,** holder 加上 FLAG_UPDATE 辨別,請先記住,這個辨別很重要。然後,關鍵問題之一來了,payload 通過 addChangePayload() 方法直接加到對應 holder 中。**一個核心問題得到解決。

接着回到 processAdapterUpdatesAndSetAnimationFlags() 後部分,設定 mRunSimpleAnimations 和 mRunPredictiveAnimations 兩個辨別為 true。再傳回 dispatchLayoutStep1() 方法中第三點。

if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()

&& !holder.shouldIgnore() && !holder.isInvalid()) {

//3.如果holder确定要更新,就把它添加到 oldChangeHolders 集合中

long key = getChangedHolderKey(holder);

mViewInfoStore.addToOldChangeHolders(key, holder);

}

上面說了,** holder 如果需要被更新,那麼 FLAG_UPDATE 就會被添加,然後 holder.isUpdated() 方法就會傳回 true 。是以第三點條件符合,就會執行。**

接着是第四點:

if (mState.mRunPredictiveAnimations) {

...

//4.很重要,LayoutManager 開始工作

mLayout.onLayoutChildren(mRecycler, mState);

在 LayoutManger 的 onLayoutChildren() 中有大量代碼,這裡就看我們關心的兩行代碼,其實就是兩個方法,detachAndScrapAttachedViews() 和 fill() 方法。

//LinearLayoutManger

@Override

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

...

detachAndScrapAttachedViews(recycler);

fill(recycler, mLayoutState, state, false);

...

}

detachAndScrapAttachedViews() 這個方法最後會反向周遊所有 View 依次調用 Recycler 的 scrapView() 方法。關于 Recycler ,可以說是RecyclerView 的核心之一,單獨開一篇文章講緩存複用 Recycler 機制都不過分,這裡我們關心局部重新整理相關就好。

void scrapView(View view) {

final ViewHolder holder = getChildViewHolderInt(view);

// 如果不需要更新 放到 mAttachedScrap 中

if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)

|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {

...

holder.setScrapContainer(this, false);

mAttachedScrap.add(holder);

} else {

// 需要更新 放到 mChangedScrap 中

if (mChangedScrap == null) {

mChangedScrap = new ArrayList();

}

holder.setScrapContainer(this, true);

mChangedScrap.add(holder);

}

}

這個方法中,也要注意,如果不需要更新,會加到 mAttachedScrap 全家桶中,需要更新的,就會放到 mChangedScrap 中。為什麼要加入到這些集合中呢,因為後面 fill() 的時候會通過這些集合去找對應的 holder,生産對應的 View 最後真正添加到 RecyclerView 控件中。

在 fill() 方法中,最終會調用 tryGetViewHolderForPositionByDeadline() 方法找到 ViewHolder,拿到對應 View,然後 addView()。

//Recycler

@Nullable

ViewHolder tryGetViewHolderForPositionByDeadline(int position,

boolean dryRun, long deadlineNs) {

...

boolean fromScrapOrHiddenOrCache = false;

ViewHolder holder = null;

// 在 dispatchLayoutStep1 中 設定為 mState.mInPreLayout = mState.mRunPredictiveAnimations

// 0. 從 mChangedScrap 集合中尋找

if (mState.isPreLayout()) {

holder = getChangedScrapViewForPosition(position);

fromScrapOrHiddenOrCache = holder != null;

}

// 1. Find by position from scrap/hidden list/cache

if (holder == null) {

holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

...

}

if (holder == null) {

final int offsetPosition = mAdapterHelper.findPositionOffset(position);

...

final int type = mAdapter.getItemViewType(offsetPosition);

// 2. Find from scrap/cache via stable ids, if exists

if (mAdapter.hasStableIds()) {

holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),

type, dryRun);

if (holder != null) {

// update position

holder.mPosition = offsetPosition;

fromScrapOrHiddenOrCache = true;

}

}

// 3. 從 mViewCacheExtension 查找,mViewCacheExtension 預設為 null

if (holder == null && mViewCacheExtension != null) {

// We are NOT sending the offsetPosition because LayoutManager does not

// know it.

final View view = mViewCacheExtension

.getViewForPositionAndType(this, position, type);

if (view != null) {

holder = getChildViewHolder(view);

...

}

}

if (holder == null) { // fallback to pool

...

//4. 從 RecycledViewPool 中查找

holder = getRecycledViewPool().getRecycledView(type);

...

}

if (holder == null) {

...

//5. 老實建立

holder = mAdapter.createViewHolder(RecyclerView.this, type);

...

}

}

holder 的查找是一個漫長過程,注意這裡第 0 步,隻有 isPreLayout() 為 true 才會從 mChangedScrap 集合中查找 ViewHolder ,在 dispatchLayoutStep1() 中,mState.mInPreLayout = mState.mRunPredictiveAnimations 預設會設定為 true ,是以會執行到這裡。第一步從 mAttachedScrap mHiddenViews mCachedViews 這些集合中查找;第二步通過 獨立 id 再次查找;第三步,可能的話,通過 mViewCacheExtension 拓展查找,這個可以通過 RecyclerView 設定;第四部,從 RecycledViewPool 中查找;最後,通過 adapter 建立。

到這裡,dispatchLayoutStep1() 方法差不多結束。

dispatchLayoutStep2

根據最終狀态執行布局,并且可能執行多次。

private void dispatchLayoutStep2() {

...

// 注意,這裡 mInPreLayout 設定為 false

mState.mInPreLayout = false;

mLayout.onLayoutChildren(mRecycler, mState);

mState.mStructureChanged = false;

mPendingSavedState = null;

...

}

相比第一步預備,第二步其實來得簡單得多,核心就是将 mInPreLayout 設定為 false 然後重新調用 LayoutManager 的 onLayoutChildren() 方法。過程就如上分析,但是,因為這裡 mInPreLayout 字段為 false ,而我們之前修改的 ViewHolder 是被添加到 mChangedScrap 集合中,但是因為 mInPreLayout 為 false, 它不會再去 mChangedScrap 查找ViewHolder( tryGetViewHolderForPositionByDeadline() 方法中第 0 步操作将不在執行,是以 舊的 ViewHolder 無法被複用,它會從下面步驟中擷取ViewHolder 傳回。也就是說,當我們通常使用 notifyItemChangge(pisition) 一個參數的方法之後,重新整理時它使用的不是同一個 ViewHolder 。

dispatchLayoutStep3

儲存 View 資訊,執行動畫效果,最後做一些清除操作。

private void dispatchLayoutStep3() {

...

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

...

long key = getChangedHolderKey(holder);

//擷取目前 holder 動畫資訊

final ItemHolderInfo animationInfo = mItemAnimator

.recordPostLayoutInformation(mState, holder);

//擷取 olderViewHolder

ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);

if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {

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

// 這裡一存一取 完成資訊覆寫

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

}

}

// 重點,真正開始執行添加的動畫效果

mViewInfoStore.process(mViewInfoProcessCallback);

}

//回收 和 重置

...

}

在 dispatchLayoutStep1() 中,我們儲存了一些資訊,現在終于要派上用場。關于動畫相關邏輯,已經添加相關注釋。最後需要注意,如果需要執行動畫,将會執行 animateChange() 方法,該方法會完成動畫相關的建立,并不會直接執行,而是到最後 mViewInfoStore.process(mViewInfoProcessCallback) 調用,才開始真正的擷取相關動畫資訊,并執行。

// RecyclerView

private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,

@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,

boolean oldHolderDisappearing, boolean newHolderDisappearing) {

oldHolder.setIsRecyclable(false);

if (oldHolderDisappearing) {

addAnimatingView(oldHolder);

}

if (oldHolder != newHolder) {

if (newHolderDisappearing) {

addAnimatingView(newHolder);

}

// 這裡很有意思,有點兒像繞密碼,這種設定是為了後面做相關釋放

oldHolder.mShadowedHolder = newHolder;

addAnimatingView(oldHolder);

mRecycler.unscrapView(oldHolder);

newHolder.setIsRecyclable(false);

newHolder.mShadowingHolder = oldHolder;

}

if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {

// 到這裡真正執行相關動畫

postAnimationRunner();

}

}

// DefaultItemAnimator

@Override

public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,

int fromX, int fromY, int toX, int toY) {

if (oldHolder == newHolder) {

// 如果新舊 holder 相同時,就添加一個 move 動畫效果

// 如果偏移量為 0 直接傳回 false 不執行動畫效果

return animateMove(oldHolder, fromX, fromY, toX, toY);

}

// 如果新舊不一樣 那麼就需要 計算出偏移量,然後建立一個 ChangeInfo

final float prevTranslationX = oldHolder.itemView.getTranslationX();

final float prevTranslationY = oldHolder.itemView.getTranslationY();

final float prevAlpha = oldHolder.itemView.getAlpha();

resetAnimation(oldHolder);

int deltaX = (int) (toX - fromX - prevTranslationX);

int deltaY = (int) (toY - fromY - prevTranslationY);

// recover prev translation state after ending animation

oldHolder.itemView.setTranslationX(prevTranslationX);

oldHolder.itemView.setTranslationY(prevTranslationY);

oldHolder.itemView.setAlpha(prevAlpha);

if (newHolder != null) {

// carry over translation values

resetAnimation(newHolder);

newHolder.itemView.setTranslationX(-deltaX);

newHolder.itemView.setTranslationY(-deltaY);

newHolder.itemView.setAlpha(0);

}

// 添加到 動畫 集合中,等待接下來真正運作

mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));

return true;

}

@Override

public boolean animateMove(final ViewHolder holder, int fromX, int fromY,

int toX, int toY) {

final View view = holder.itemView;

fromX += (int) holder.itemView.getTranslationX();

fromY += (int) holder.itemView.getTranslationY();

resetAnimation(holder);

int deltaX = toX - fromX;

int deltaY = toY - fromY;

// 如果平移偏移量都是 0 那麼就不執行動畫效果

if (deltaX == 0 && deltaY == 0) {

dispatchMoveFinished(holder);

//false 不會觸發動畫效果

return false;

}

if (deltaX != 0) {

view.setTranslationX(-deltaX);

}

if (deltaY != 0) {

view.setTranslationY(-deltaY);

}

// 添加動畫效果 等待執行

mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));

return true;

}

上面三個方法,分别是具體的動畫添加過程,其中有些注意點,如果 oldHolder 和 newHolder 相等,并且相關偏移量為零,那麼不會添加和執行相關動畫效果。

如果有相關動畫效果,會建立加入到 DefaultItemAnimator 的集合中,然後 mItemAnimator.animateChange() 方法傳回 true ,最後調用 postAnimationRunner() 方法執行。

在 postAnimationRunner() 方法的 Runnable 中最後會調用 mItemAnimator.runPendingAnimations() ,最後将會執行到 animateChangeImpl() 方法。

//DefaultItemAnimator

void animateChangeImpl(final ChangeInfo changeInfo) {

final ViewHolder holder = changeInfo.oldHolder;

final View view = holder == null ? null : holder.itemView;

final ViewHolder newHolder = changeInfo.newHolder;

final View newView = newHolder != null ? newHolder.itemView : null;

//舊View存在的話,執行相關動畫

if (view != null) {

final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(

getChangeDuration());

mChangeAnimations.add(changeInfo.oldHolder);

oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);

oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);

oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {

@Override

public void onAnimationStart(Animator animator) {

dispatchChangeStarting(changeInfo.oldHolder, true);

}

@Override

public void onAnimationEnd(Animator animator) {

//釋放

oldViewAnim.setListener(null);

view.setAlpha(1);

view.setTranslationX(0);

view.setTranslationY(0);

//回調 RecyclerView

dispatchChangeFinished(changeInfo.oldHolder, true);

mChangeAnimations.remove(changeInfo.oldHolder);

dispatchFinishedWhenDone();

}

}).start();

}

//新 View 執行相關動畫

if (newView != null) {

final ViewPropertyAnimator newViewAnimation = newView.animate();

mChangeAnimations.add(changeInfo.newHolder);

newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())

.alpha(1).setListener(new AnimatorListenerAdapter() {

@Override

public void onAnimationStart(Animator animator) {

dispatchChangeStarting(changeInfo.newHolder, false);

}

@Override

public void onAnimationEnd(Animator animator) {

//回收釋放

newViewAnimation.setListener(null);

newView.setAlpha(1);

newView.setTranslationX(0);

newView.setTranslationY(0);

//回調 RecyclerView

dispatchChangeFinished(changeInfo.newHolder, false);

mChangeAnimations.remove(changeInfo.newHolder);

dispatchFinishedWhenDone();

}

}).start();

}

}

// RecyclerView inner

private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {

ItemAnimatorRestoreListener() {

}

@Override

public void onAnimationFinished(ViewHolder item) {

item.setIsRecyclable(true);

//前面 ViewHolder 繞密碼式設定,在這裡做最後釋放

if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh

item.mShadowedHolder = null;

}

// always null this because an OldViewHolder can never become NewViewHolder w/o being

// recycled.

item.mShadowingHolder = null;

if (!item.shouldBeKeptAsChild()) {

if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {

removeDetachedView(item.itemView, false);

}

}

}

}

在 animateChangeImpl() 方法中,分别為 oldView 和 newView 執行相關動畫,最後回調到 RecyclerView 中的 onAnimationFinished() 方法中,完成對 ViewHolder 之前動畫相關聯 holder 的釋放。

到這裡,第三步分析基本完成。再回頭看之前提出的局部重新整理的兩個問題:

第一,notifyItemChange() 和 局部重新整理 ViewHolder 是同一個對象嗎?第二,局部重新整理是沒有設定動畫效果嗎?

基于上面第三步分析的結論,如果 oldHolder 和 newHolder 相等,并且偏移量為零,那麼不會添加和執行相關動畫效果,再結合實際情況,我們不妨大膽猜測第一個問題的答案,它們使用的是同一個 ViewHolder 對象。接着針對第二個問題,如果使用同一個對象,并且偏移值為 0 ,那麼就不會執行相關動畫效果。

但是,這個結論似乎和我們分析第二步時得出的結論有出入:而我們之前修改的 ViewHolder 是被添加到 mChangedScrap 集合中,但是因為 mInPreLayout 此時設定為 false, 它不會再去 mChangedScrap 查找ViewHolder( tryGetViewHolderForPositionByDeadline() 方法中第 0 步操作,是以 舊的 ViewHolder 無法被複用,它會從下面步驟中擷取 ViewHolder 傳回。也就是說,當我們通常使用 notifyItemChangge() 一個參數的方法之後,它使用的不是同一個 ViewHolder。

到底是哪裡錯了呢?其實都沒錯,上面說的是使用 notifyItemChangge(position) 一個參數的方法時的情況,完全正确。局部重新整理時,我們使用的可是兩個參數的方法。

void scrapView(View view) {

final ViewHolder holder = getChildViewHolderInt(view);

// 如果不需要更新 放到 mAttachedScrap 中

if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)

// 沒有更新 或者 可以被複用

|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {

...

holder.setScrapContainer(this, false);

mAttachedScrap.add(holder);

} else {

// 需要更新 放到 mChangedScrap 中

if (mChangedScrap == null) {

mChangedScrap = new ArrayList();

}

holder.setScrapContainer(this, true);

mChangedScrap.add(holder);

}

}

針對第二步中,調用的 scrapView() 方法我們再看一次,我們可以看到,在 if 判斷中,如果 holder 确實需要被更新,那麼它也可能被添加到 mAttachedScrap 集合中,隻要 anReuseUpdatedViewHolder(holder) 這個方法能傳回 true 。

//RecyclerView

boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {

return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,

viewHolder.getUnmodifiedPayloads());

}

RecyclerView 最後會調用 ItemAnimator 中的 canReuseUpdatedViewHolder() 方法,我們看看具體實作:

@Override

public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,

@NonNull List payloads) {

return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);

}

所噶,當我們使用局部重新整理,payload 不為空 ,這個時候,如果 ViewHolder 需要更新,它的 更新辨別 的确會被加入,但是同時canReuseUpdatedViewHolder() 也會傳回 true ,是以,這個時候 ViewHolder 不會被添加到 mChangedScrap 集合中,而是加入 mAttachedScrap 集合中,真是程式員的小巧思。

是以,當你使用局部重新整理時,前後都是同一個 ViewHolder ,如果位置沒有變化,就不會執行動畫效果;而當你不使用局部重新整理時,使用的不是同一個 ViewHolder ,不管位置是否變化,都會執行相關動畫,是以你看到的 itemView 會閃爍一下。當我們多次調用 notifyItemChange() 方法時,也不會多次觸發 requestLayout() 和回調 bindViewHolder()

到此,上面提到的疑問都解決,至于提到那個場景,就可以使用 局部重新整理 來處理,首先不會再有閃爍,那是在執行動畫,其次,是那個傳值問題,完全就可以使用 payload 參數來傳遞。每次拿取出 payloads 集合中最後一個值(最新的進度),然後更新到進度條,這個設計也算得上程式員的小巧思嘛(狗頭)。