前情回顧
Android RecycleView輕鬆實現下拉重新整理、載入更多
SwipeRefreshLayout 在 RecyclerView 空白時下拉失效分析
之前寫的 PowerAdapter
和 SelectPowerAdapter
從建立到現在,已經兩年多,期間發生了翻天覆地的變化。一開始,我把 SwipeRefreshLayout
和 RecyclerView
直接耦合在一起,佈局就寫一個控制元件,爽。因為那會兒業務場景是那樣,沒有考慮靈活性。後來改業務頭部不能直接用 SwipeRefreshLayout
,突然才意識到這樣侷限性太大。於是乎 Android 打造專屬的下拉重新整理 載入更多 就這麼出現,的確也有實際的應用場景。甚至還狂熱地做了那會 UC 瀏覽器下拉重新整理的效果
。在這之後,我將頭部拆分開,PowerAdapter
什麼的就用於簡化實現載入更多已經多佈局填充。SelectPowerAdapter
繼承自 PowerAdapter
,寫的及其簡陋。用於實現簡單的單選和多選。
更新
倉庫請戳 PowerRecyclerView
這倆 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<Object> 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 成立,就能回答這個前置問題。
/**
* @return True if updates should be processed.
*/
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<ViewHolder>();
}
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<ViewHolder>();
}
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<Object> payloads) {
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
複製程式碼
所噶,當我們使用區域性重新整理,payload
不為空 ,這個時候,如果 ViewHolder
需要更新,它的 更新標識 的確會被加入,但是同時canReuseUpdatedViewHolder()
也會返回 true
,所以,這個時候 ViewHolder
不會被新增到 mChangedScrap
集合中,而是加入 mAttachedScrap
集合中,真是程式設計師的小巧思。
所以,當你使用區域性重新整理時,前後都是同一個 ViewHolder
,如果位置沒有變化,就不會執行動畫效果;而當你不使用區域性重新整理時,使用的不是同一個 ViewHolder
,不管位置是否變化,都會執行相關動畫,所以你看到的 itemView
會閃爍一下。當我們多次呼叫 notifyItemChange()
方法時,也不會多次觸發 requestLayout()
和回撥 bindViewHolder()
到此,上面提到的疑問都解決,至於提到那個場景,就可以使用 區域性重新整理 來處理,首先不會再有閃爍,那是在執行動畫,其次,是那個傳值問題,完全就可以使用 payload
引數來傳遞。每次拿取出 payloads
集合中最後一個值(最新的進度),然後更新到進度條,這個設計也算得上程式設計師的小巧思嘛(狗頭)。