結合例項講解RecyclerView的佈局過程
本文主要結合例項講解以下幾個方面:
- 預佈局&預測動畫
- Recycler 的快取的複用過程
- RecyclerView的佈局過程
- Step1 預佈局
- Step2 佈局
- Step3 執行動畫
- Q&A
起因:
在試用重構後的播放器時,因為VideoView響應了 onVideoViewAttachToWindow 、 onVideoViewDetachFromWindow、onWindowVisibilityChanged 等事件,發現在點選VideoFeed時,傳送PayLoad 為 FULL_SCREEN等事件的notifyItemChanged時,RecyclerView會先新增一個View,然後隨之刪去,雖然不影響頁面的展示,但是卻出發了VideoView裡面的邏輯,造成了一些不可預期的結果。
預佈局和預測動畫
使用者有A、B、C三個item,A、B正顯示在螢幕中,這時,使用者把B刪除了,最終C會顯示在原先B的位置。
僅僅知道原狀態和最終狀態是無法執行合理的動畫的。因為知道C的最終位置,但是不知道C的起始位置。
針對這個問題,谷歌給出的答案是,通過預佈局把C先 預佈局 出來,然後再觸發動畫。
預佈局:
第一次是預佈局,將之前原狀態下的item都佈局出來。並且根據Adapter的notify資訊,知道哪些item即將變化,所以可以加載出另外的View來備用(可能有多個)。上述例子中,因為知道B已經被刪除(寬高變化也可觸發),所以把螢幕之外的C也預載入進來。
預佈局:
普通動畫:
預測動畫:
https://tylerliu.top/2020/05/15/Android-RecyclerView的快取機制/171d8e01b6b0ba6c
Recycler 的四級快取的複用過程
從adapter的notify到RecyclerView的 layout
從略~
第一步: RecyclerView.dispatchLayoutStep1()
當LayoutManage
開始佈局的時候(預佈局或者是最終佈局),當前佈局中的所有view,都會被dump到scrap中(具體可見LinearLayoutManage#onLayoutChildren
detachAndScrap
),然後LayoutManager
挨個取回view,除非view發生了什麼變化,否則它會馬上從scrap中回到原來的位置。
為什麼LayoutManager
需要先執行detach
,然後再重新attach
這些view,而不是隻移除那些變化的子view呢?Scrap快取列表的存在,是為了隔離LayoutManager
和RecyclerView.Recycler
之間的關注點/職責。LayoutManager
不需要知道哪些子view需要保留或者被回收到RecyclerViewPool
或者其他地方。這是Recycler
的職責。
除了在佈局時不為空之外,還有另外一個與scrap有關的規律:所有scrap的view都會跟RecyclerView
分離。ViewGroup
中的attachView
和detachView
方法跟addView
和removeView
很像,但是不會觸發請求佈局重繪的事件。它們只是從ViewGroup
的子view列表中刪除對應的子view。,並將該子view的parent設定為null。detached
狀態必須是臨時的,後面緊隨著attach
或remove
事件。
下面我們看下在預佈局時候新增View的兩種情況:
第一種情況:
- 先從
mAttachedScrap
快取中移除。再呼叫mChildHelper的attachViewToParent()
方法,進而通過其 mCallback 呼叫到 RecyclerView的attachViewToParent
方法。正如上文所述,該方法不會觸發子View 的onAttachedToWindow()
和 RecyclerView 中註冊的 OnChildAttachStateChangeListener 中的方法。
第二種情況:
layout 時載入 額外的一個View到RecyclerView中。
此時,因為該ViewHolder為通過 onCreateViewHolder新生成的,所以呼叫了ChildHelper 的 addView()
方法,該方法最終會呼叫到RecyclerView中的 dispatchChildAttached()
方法,使OnChildAttachStateChangeListener 進行相應。子View的 onAttachedToWindow()
也會被呼叫。
佈局第二步 dispatchLayoutStep2()
主要也是呼叫 mLayout.onLayoutChildren(mRecycler, mState);
。
因為在第一步中,我們已經在RecyclerView中多加入了一個Item,所以mAttachedScrap
共蒐集到 0、1、2、3、4、5、6 七個,比原來的多一個 0,1,2,3,4,5,6。
A
B
C
D ABCD B (notify 1) prelayout
A
B '
C
D
佈局和第一步類似,在fill()中 ,但是改變的第0個ViewHolder 現在的 layoutChunkResult.mIgnoreConsumed 也是false了,所以也需要消耗剩餘空間了。
所以 fill()方法執行完成後, 最後一個(即第 6 個)依然存在於 mAttachedScrap
中。
最後執行到 layoutForPredictiveAnimations
方法,其中會取出其中還未使用完的 mAttachedScrap
:
final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
final int scrapSize = scrapList.size();
final int firstChildPos = getPosition(getChildAt(0));
for (int i = 0; i < scrapSize; i++) {
RecyclerView.ViewHolder scrap = scrapList.get(i);
if (scrap.isRemoved()) {
continue;
}
final int position = scrap.getLayoutPosition();
final int direction = position < firstChildPos != mShouldReverseLayout
? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
//為scrap新增 Extra空間
if (direction == LayoutState.LAYOUT_START) {
scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
} else {
scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
}
}
mLayoutState.mScrapList = scrapList;
if (scrapExtraEnd > 0) {
View anchor = getChildClosestToEnd();
updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
mLayoutState.mExtraFillSpace = scrapExtraEnd;
mLayoutState.mAvailable = 0;
mLayoutState.assignPositionFromScrapList();
//續命後再次fill
fill(recycler, mLayoutState, state, false);
}
再次進入到 layoutChunk 時,因為 layoutState.mScrapList 不為 null,此時將 呼叫 addDisappearingView(view);
佈局第三步dispatchLayoutStep3()
A
B '
C
D
根據 mChildHelper 中記錄的數量, 遍歷子獲取holder,統計需要執行的動畫。
統計完成後,執行動畫:
mViewInfoStore.process(mViewInfoProcessCallback);
遍歷 mLayoutHolderMap,然後根據 record的Flag,執行相應的動畫。
本例項中,因為addDisappearingView(view)
新增View時, 會將record的Flag 加入 FLAG_APPEAR_AND_DISAPPEAR ,所以會據此執行callback.unused(viewHolder); 之後通過ChildHelper的 removeView
方法 ,刪除上一步加入的待刪除 View。然後會呼叫 RecyclerView的 dispatchChildDetached方法,通知 mOnChildAttachStateListeners
響應。子View的 onAttachedToWindow()
也將響應。
Questions
Q:為什麼Update 也需要多新增一個View?
A:因為update也有可能隱藏部分佈局,導致漏出下一條內容。
Q:在什麼時候判斷,新加入的View是該保留還是該刪除?
A:在第二步中,fill時,每新增一個新的View,會判斷 他的高度,高度變化(或者甚至是其中一個唄刪除了),會計算消耗高度 layoutChunkResult.mConsumed
,如果空出了空間 ,則會正常呼叫 addView
而不是 addDisappearingView(view);
這樣,在第三步執行動畫時,也不會 走到 unused() 方法中。但是,如果,在fill時,如果前面的所有View高度都沒有發生變化,還是會走到 addDisappearingView(view);
中。
所以,可以說,最後需要被刪除,在第二步時已經 註定了。
Q:既然第二步已經決定最後這個View會被刪除,那麼直接不新增不就行了嗎?
A:新增是在第一步預佈局時候加入進去的,所以第二步 真正佈局 時只能進行標記,讓他在第三步中刪除掉。
Q:既然第二步中,可以得到變化後的 layoutChunkResult.mConsumed
為變化後的值: newHeight,那麼為什麼第一步時候,為何還要在layoutChunk時:
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
然後再fill方法中根據此值忽略掉該View所耗費高度(其實第一步計算時高度為變化前高度),而是直接無腦預先多加入一個View呢? 這要看 RecyclerView的快取相關的邏輯了:
獲取Holder:
結論:第一步為 preload 階段,該階段認為沒有必要對 bind過得 View 重新執行 tryBindViewHolderByDeadline
和業務中 onBindViewHolder
方法的呼叫。所以,預佈局階段,一切都是老佈局時的View和屬性。
Q:那麼這一切 和 LinearLayoutManager 中 的 supportsPredictiveItemAnimations()
有什麼關係呢?
A:簡單的說,就是 如果 這個地方返回 false,則 在第一步dispatchLayoutStep1()
中就不會呼叫 LinearLayoutManager 的 onLayoutChildren()
方法了。
mState.mRunPredictiveAnimations 決定 dispatchLayoutStep1()
中是否進行預佈局,而該值在第一步開始時即在 processAdapterUpdatesAndSetAnimationFlags()
方法中賦值如下:
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
private boolean predictiveItemAnimationsEnabled() {
return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
}
所以說,整體結論是:
第一步佈局時,如果預佈局開關開啟,如果有Item發生了變化或刪除,不管三七二十一,先多佈局一個再說。
然後第二步進行佈局時,根據重新Bind後的ViewHolder和其中的View的寬高,進行真正的佈局。
第三步,佈局完成後,按需執行動畫,該刪除的刪除,該移動的移動。