老大爺都能看懂的RecyclerView動畫原理
如何閱讀本篇文章
本文主要講解RecyclerView Layout變化觸發動畫執行的原理。前半部分偏重原理和程式碼的講解,後半部分通過圖文結合場景講解各個階段的執行過程。
建議先粗略閱讀前半部分的原理和程式碼篇,做到心中有概念,帶著理論知識去閱讀後半部分的場景篇。最後結合全文學到的知識,帶著問題去閱讀原始碼,效果會更好。
原理篇
1. Adapter的notify方法
用過RecyclerView的同學大概都應該知道Adapter有幾個notify相關的方法,它們分別是:
- notifyDataSetChanged()
- notifyItemChanged(int)
- notifyItemInserted(int)
- notifyItemRemoved(int)
- notifyItemRangeChanged(int, int)
- notifyItemRangeInserted(int, int)
- notifyItemRangeRemoved(int, int)
- notifyItemMoved(int, int)
稍微有點開發經驗的同學都知道,notifyDataSetChanged()方法比其它的幾個方法更重量級一點,它會導致整個列表重新整理,其它幾個方法則不會。有更多開發經驗的同學可能還知道notifyDataSetChanged()方法不會觸發RecyclerView的動畫機制,其它幾個方法則會觸發各種不同型別的動畫。
2. RecyclerView的佈局邏輯
2.1 RecyclerView的dispatchLayout
dispatchLayout顧名思義,當然是把子View佈局(新增並放置到合適的位置)到RecyclerView上面了。開啟它的原始碼我們可以看到這樣一段註釋。
Wrapper around layoutChildren() that handles animating changes caused by layout. Animations work on the assumption that there are five different kinds of items in play:
PERSISTENT: items are visible before and after layout
REMOVED: items were visible before layout and were removed by the app
ADDED: items did not exist before layout and were added by the app
DISAPPEARING: items exist in the data set before/after, but changed from visible to non-visible in the process of layout (they were moved off screen as a side-effect of other changes)
APPEARING: items exist in the data set before/after, but changed from non-visible to visible in the process of layout (they were moved on screen as a side-effect of other changes)
從註釋我們可以知道。dispatchLayout方法不僅有給子View佈局的功能,而且可以處理動畫。動畫主要分為五種:
- PERSISTENT:針對佈局前和佈局後都在手機介面上的View所做的動畫
- REMOVED:在佈局前對使用者可見,但是資料已經從資料來源中刪除掉了
- ADDED:新增資料到資料來源中,並且在佈局後對使用者可見
- DISAPPEARING:資料一直都存在於資料來源中,但是佈局後從可見變成不可見狀態
- APPEARING:資料一直都存在於資料來源中,但是佈局後從不可見變成可見狀態
到目前為止,我們還不能完全理解這五種型別的動畫有什麼具體的區別,分別在什麼樣的場景下會觸發這些型別的動畫。但是給我們提供了很好的研究思路。目前我們只需要簡單瞭解有這五種動畫,接著往下,我們這裡看下dispatchLayout的原始碼,為了響應文章標題,這裡貼出精簡過的原始碼:
void dispatchLayout(){
...
dispatchLayoutStep1();
dispatchLayoutStep2();
dispatchLayoutStep3();
...
}
關於dispatchLayoutStepX方法,相信很多人都聽說或者瞭解過,文章後面我會做詳細的介紹,簡單介紹如下:
從dispatchLayout的註釋中,我們注意到before和after兩個單詞,分別表示佈局前和佈局後。這麼說來那就簡單了。dispatchLayoutStep1對應的是before(佈局前),dispatchLayoutStep2的意思是佈局中,dispatchLayoutStep3對應的是after(佈局後)。它們的作用描述如下:
- dispatchLayoutStep1
-
判斷是否需要開啟動畫功能
-
如果開啟動畫,將當前螢幕上的Item相關資訊儲存起來供後續動畫使用
-
如果開啟動畫,呼叫mLayout.onLayoutChildren方法預佈局
-
預佈局後,與第二步儲存的資訊對比,將新出現的Item資訊儲存到Appeared中
-
精簡後的程式碼如下:
private void dispatchLayoutStep1() {
...
//第一步 判斷是否需要開啟動畫功能
processAdapterUpdatesAndSetAnimationFlags();
...
if (mState.mRunSimpleAnimations) {
...
//第二步 將當前螢幕上的Item相關資訊儲存起來供後續動畫使用
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());
mViewInfoStore.addToPreLayout(holder, animationInfo);
}
...
if (mState.mRunPredictiveAnimations) {
saveOldPositions();
//第三步 呼叫onLayoutChildren方法預佈局
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;
}
//第四步 預佈局後,對比預佈局前後,哪些item需要放入到Appeared中
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
clearOldPositions();
} else {
clearOldPositions();
}
}
}
- dispatchLayoutStep2 根據資料來源中的資料進行佈局,真正展示給使用者看的最終介面
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mState.mInPreLayout = false;//此處關閉預佈局模式
mLayout.onLayoutChildren(mRecycler, mState);
...
}
- dispatchLayoutStep3 觸發動畫
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));
if (holder.shouldIgnore()) {
continue;
}
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
...
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// Step 4: Process view info lists and trigger animations
//觸發動畫
mViewInfoStore.process(mViewInfoProcessCallback);
}
...
}
從程式碼我們可以看出dispatchLayoutStep1和dispatchLayoutStep2方法中呼叫了onLayoutChildren方法,而dispatchLayoutStep3沒有呼叫。
2.2 LinearLayoutManager的onLayoutChildren方法
以垂直方向的RecyclerView為例子,我們填充RecyclerView的方向有兩種,從上往下填充和從下往上填充。開始填充的位置不是固定的,可以從RecyclerView的任意位置處開始填充。該方法的功能我精簡為以下幾個步驟:
- 尋找填充的錨點(最終呼叫findReferenceChild方法)
- 移除螢幕上的Views(最終呼叫detachAndScrapAttachedViews方法)
- 從錨點處從上往下填充(呼叫fill和layoutChunk方法)
- 從錨點處從下往上填充(呼叫fill和layoutChunk方法)
- 如果還有多餘的空間,繼續填充(呼叫fill和layoutChunk方法)
- 非預佈局,將scrapList中多餘的ViewHolder填充(呼叫layoutForPredictiveAnimations)
本文只講解onLayoutChildren的主流程,具體的填充邏輯請參考RecyclerView填充邏輯一文
LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
//1\. 尋找填充的錨點
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
...
//2\. 移除螢幕上的Views
detachAndScrapAttachedViews(recycler);
...
//3\. 從錨點處從上往下填充
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
...
//4\. 從錨點處從下往上填充
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
...
//5\. 如果還有多餘的空間,繼續填充
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
...
//6\. 非預佈局,將scrapList中多餘的ViewHolder填充
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
...
LinearLayoutManager#layoutForPredictiveAnimations
private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,
RecyclerView.State state, int startOffset,
int endOffset) {
//判斷是否滿足條件,如果是預佈局直接返回
if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout()
|| !supportsPredictiveItemAnimations()) {
return;
}
// 遍歷scrapList,步驟2中螢幕中被移除的View
int scrapExtraStart = 0, scrapExtraEnd = 0;
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);
//如果被remove掉了,跳過
if (scrap.isRemoved()) {
continue;
}
//計算額外的控制元件
scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
}
mLayoutState.mScrapList = scrapList;
...
// 步驟6 繼續填充
if (scrapExtraEnd > 0) {
View anchor = getChildClosestToEnd();
updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
mLayoutState.mExtraFillSpace = scrapExtraEnd;
mLayoutState.mAvailable = 0;
mLayoutState.assignPositionFromScrapList();
fill(recycler, mLayoutState, state, false);
}
mLayoutState.mScrapList = null;
}
至此,佈局的邏輯已經講解完畢。關於具體的動畫執行邏輯,由於篇幅有限。不在本文中講解
場景篇
1. notifyItemRemoved
我們來測試從螢幕中刪除View,呼叫notifyItemRemoved相關的方法,dispatchLayout是如何重新佈局的。假設初始狀態如下圖,假設Adapter資料有100條,螢幕上有Item1~Item6 6個View,刪除Item1和Item2。
- 將Item1 Item2對應的ViewHolder設定為REMOVE狀態
- 將所有的Item對應的ViewHolder的mPreLayoutPosition欄位賦值為當前的position
我們回顧以下onLayoutChildren的幾個步驟
- 尋找填充的錨點(最終呼叫findReferenceChild方法)
- 移除螢幕上的Views(最終呼叫detachAndScrapAttachedViews方法)
- 從錨點處從上往下填充(呼叫fill和layoutChunk方法)
- 從錨點處從下往上填充(呼叫fill和layoutChunk方法)
- 如果還有多餘的空間,繼續填充(呼叫fill和layoutChunk方法)
- 非預佈局,將scrapList中多餘的ViewHolder填充(呼叫layoutForPredictiveAnimations)
1.1 dispatchLayoutStep1階段
- 尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item。在本Case中,找到Item3
- 移除螢幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap快取中,這個快取的好處是如果position對應上了,無需重新繫結,直接拿來用。
- 從錨點Item3處往下填充,mAttachedScrap只剩下ViewHolder2和ViewHolder1
- 從錨點Item3處往上填充Item2 Item1,因為Item2,Imte1已經被remove掉了,它消耗的空間不會被記錄,那麼到步驟5的時候還可以填充
- 還有多餘的空間,繼續填充,把Item7、Item8填充到螢幕中
- 因為當前是預佈局,直接返回
至此step1的layout結束
1.2 dispatchLayoutStep2階段
- 尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item。在本Case中,找到Item3
- 移除螢幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap快取中
- 從錨點Item3處往下填充,填充到Item6為止,就沒有足夠的距離了,mAttachedScrap只剩下ViewHolder8,ViewHolder7,ViewHolder2,ViewHolder1
- 往上填充,雖然此時還有兩個View的高度,但是此時,上邊沒有資料了,此處不填充
- 此時還有兩個View的高度,繼續往下填充
注意此時已經佈局完成但是螢幕上部與第一個有GAP,會修復
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
if (mShouldReverseLayout ^ mStackFromEnd) {
int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
} else {
int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
}
}
修復後效果如下
- 當前不是預佈局,但是因為ViewHolder1和ViewHolder2都是被Remove掉的,所以跳過
2. notifyItemInserted
假設在Item1下面插入兩條資料AddItem1,AddItem2
2.1 dispatchLayoutStep1階段
- 尋找錨點,找到Item1
2. 移除螢幕上的Views,放入到mAttachedScrap中 [圖片上傳中…(image-180539-1608907165301-6)]
3. 錨點處從上往下填充 [圖片上傳中…(image-e86354-1608907165301-5)]
4. 錨點處從下往上填充,由上圖可知,上面沒有空間了,不填充 5. 判斷是否還有剩餘的空間,如果有在末尾填充,下面沒空間了,不填充 6. 因為當前是預佈局階段,不填充
2.2 dispatchLayoutStep2階段
- 尋找錨點,找到Item1
2. 移除螢幕上的Views,放入到mAttachedScrap中
3. 錨點處從上往下填充,此時將變化後的資料填充到螢幕上,addItem1和addItem2被填充到item1下面
4. 錨點處從下往上填充,由圖可知,沒有空間不填充
5. 判斷是否還有剩餘的空間,由圖可知,沒有空間不填充
6. 當前是layoutStep2階段,會將mAttachScrap的內容,填充到螢幕末尾,ViewHolder5和ViewHolder6對應的ItemView被填充
2.3 dispatchLayoutStep3階段
開始動畫,動畫結束後,item5和item6會被回收掉,此時會被回收到mCachedViews快取池中
大家如果還想了解更多Android 相關的更多知識點,可以加入Android粉絲裙:872206502,裡面記錄了許多的Android 知識點文件,等待你來領取。
相關文章
- 61歲老大爺應聘Java程式設計師Java程式設計師
- 用大白話告訴你小白都能看懂的Hadoop架構原理Hadoop架構
- 兄弟,用大白話告訴你小白都能看懂的Hadoop架構原理Hadoop架構
- 小白都能看懂的promiseA+實現Promise
- 對RecyclerView Item做動畫View動畫
- 小白都能看懂的tcp三次握手TCP
- RecyclerView動畫原始碼淺析View動畫原始碼
- 外行人都能看懂的SpringCloud講解SpringGCCloud
- ?外行人都能看懂的WebFlux,錯過了血虧!WebUX
- 文科生都能看懂的【機器學習中的】線性代數機器學習
- RecyclerView零點突破(動畫+邊線篇)View動畫
- 這本vue3編譯原理開源電子書,初中級前端竟然都能看懂Vue編譯原理前端
- 人人都能讀懂的編譯器原理編譯
- 小白都能看懂的Linux系統下安裝配置ZabbixLinux
- 外行人都能看懂的SpringCloud,錯過了血虧!SpringGCCloud
- 大白話聊聊微服務——人人都能看懂的演進過程微服務
- 草履蟲都能看懂的系統環境變數配置變數
- Android Retrofit原始碼解析:都能看懂的Retrofit使用詳解Android原始碼
- RecyclerView 之使用 ItemTouchHelper 實現互動動畫View動畫
- 【轉】小白都能看明白的VLAN原理解釋
- Android實現帶動畫的下拉重新整理RecyclerViewAndroid動畫View
- 小白都能看懂的 Spring 原始碼揭祕之Spring MVCSpring原始碼MVC
- 一文看懂Redis的持久化原理Redis持久化
- 自定義RecyclerView動畫——實現remove飛出效果View動畫REM
- RecyclerView如何setEmptyView及淺談ListView的setEmptyView原理View
- 任何傻瓜都能寫機器執行程式碼,而優秀的程式設計師寫的程式碼傻瓜都能看懂行程程式設計師
- 圖解機器學習:人人都能懂的演算法原理圖解機器學習演算法
- 人人都能搞定的大模型原理 - 神經網路大模型神經網路
- 小白都能看懂的AI安全診斷技術 阿里已經用上了AI阿里
- log4j漏洞的產生原因和解決方案,小白都能看懂!!!!
- 五分鐘看懂vue原理(一)Vue
- RecyclerView 事件分發原理實戰分析View事件
- RecyclerView快取原理,有圖有真相View快取
- Android 動畫原理Android動畫
- 小白都能看懂的Spring原始碼揭祕之IOC容器原始碼分析Spring原始碼
- 一個故事看懂機械硬碟原理硬碟
- 人人都能看懂系列:《分散式系統改造方案——之資料篇》分散式
- 《爺爺的城市》:一個設計師手工搭建的解謎世界