面試官:RecyclerView佈局動畫原理了解嗎?
前言
「溫馨提示:文章有點長,建議關注微信公眾號“位元組小站”收藏閱讀」
本文主要通過以下幾個方面來講解RecyclerView的佈局和動畫原理:
-
佈局放置:RecyclerView#dispatchLayout() -
預佈局階段:RecyclerView#dispatchLayoutStep1() -
佈局階段:RecyclerView#dispatchLayoutStep2() -
開啟動畫階段:RecyclerView#dispatchLayoutStep3()
背景知識
RecyclerView的Adapter有幾個notify相關的方法:
-
notifyDataSetChanged() -
notifyItemChanged(int) -
notifyItemInserted(int) -
notifyItemRemoved(int) -
notifyItemRangeChanged(int, int) -
notifyItemRangeInserted(int, int) -
notifyItemRangeRemoved(int, int) -
notifyItemMoved(int, int)
notifyDataSetChanged()與其他方法的區別:
-
會導致整個列表重新整理,其它幾個方法則不會; -
不會觸發RecyclerView的動畫機制,其它幾個方法則會觸發各種不同型別的動畫。
1. 佈局放置
1.1 核心方法
RecyclerView#dispatchLayout()
1.2 作用
-
將View放置到合適的位置 -
記錄佈局階段View的資訊 -
處理動畫
RecyclerView的佈局我們可以分成三個階段,也可以精細分成五個階段。
1.2.1 三個階段
1.2.1.1 預佈局階段
當需要做動畫時,預佈局階段才會工作,否則沒有實際意義,它對應dispatchLayoutStep1方法。動畫有開始狀態和結束狀態,預佈局完成後的RecyclerView是動畫的開始狀態。
1.2.1.2 佈局階段
無論是否需要做動畫,佈局階段都會工作,它對應dispatchLayoutStep2方法。佈局完成後的狀態是使用者最終看到的狀態,也是動畫的結束狀態。
1.2.1.3 佈局後階段
佈局完成後,需要執行動畫操作,它對應的是dispatchLayoutStep3方法。當動畫完成後,還會進行View回收操作。
1.2.2 五個階段
1.2.2.1 預佈局前
在dispatchLayoutStep1方法呼叫onLayoutChildren方法之前。它會儲存當前RecyclerView上所有子View的資訊到ViewInfoStore中,FLAG增加FLAG_PRE。表示View在預佈局前就顯示在RecyclerView上。
1.2.2.2 預佈局中
在dispatchLayoutStep1方法呼叫onLayoutChildren方法時。它會根據演算法,重新佈置RecyclerView的子View,該階段可能會新增新的子View。該階段能夠確定哪些View最終是不會展示給使用者看的,FLAG增加FLAG_DISAPPEARED(例如:removed的View)。
1.2.2.3 預佈局後
在dispatchLayoutStep1方法呼叫onLayoutChildren方法之後,將預佈局完成後的子View與預佈局前的子View對比,將新增的View的FLAG增加FLAG_APPEAR(呼叫notifyItemRemoved後,新填充的View)。
1.2.2.4 佈局中
在dispatchLayoutStep2方法呼叫onLayoutChildren方法時。該階段會把被擠出螢幕的View的FLAG增加FLAG_DISAPPEARED。
1.2.2.5 佈局後
在dispatchLayoutStep3方法中。會將最終的子View的FLAG增加FLAG_POST。
1.2.3 動畫型別
1.2.3.1 PERSISTENT
預佈局前和佈局後都存在的View所做的動畫,位置有可能發生變化了,也有可能沒有發生變化。
1.2.3.2 REMOVED
在佈局前對使用者可見,佈局後不可見,而且資料已經從資料來源中刪除掉了。
1.2.3.3 ADDED
新增資料到資料來源中,並且在佈局後對使用者可見。
1.2.3.4 DISAPPEARING
資料一直都存在於資料來源中,但是佈局後從可見變成不可見狀態(例如因為其它View插入操作,導致被擠出螢幕外了)。
1.2.3.5 APPEARING
資料一直都存在於資料來源中,但是佈局後從不可見變成可見狀態(例如因為其它View被刪除,導致補位到螢幕內了)。
1.3 原始碼解析
1.3.1 RecyclerView#dispatchLayout()
-
dispatchLayoutStep1()執行預佈局,記錄ViewHolder位置資訊; -
dispatchLayoutStep2()執行佈局,使用者最終看到的效果; -
dispatchLayoutStep3()執行動畫操作。
2. 預佈局階段
2.1 核心方法
-
RecyclerView#dispatchLayoutStep1()
-
RecyclerView#processAdapterUpdatesAndSetAnimationFlags()
-
LinearLayoutManager#onLayoutChildren()
-
LinearLayoutManager#updateAnchorInfoForLayout()
2.2 作用
-
處理Adapter變化 -
決定該執行哪種型別動畫 -
儲存當前RecyclerView上的子View的資訊 -
如果需要執行動畫,進行預佈局
2.3 原始碼解析
2.3.1 RecyclerView#dispatchLayoutStep1()
-
判斷是否需要開啟動畫功能 -
如果開啟動畫,將當前螢幕上的Item相關資訊儲存起來供後續動畫使用 -
如果開啟動畫,呼叫mLayout.onLayoutChildren方法預佈局 -
預佈局後,與第二步儲存的資訊對比,將新出現的Item資訊儲存到Appeared中

2.3.2 RecyclerView#processAdapterUpdatesAndSetAnimationFlags()
作用:判斷是否需要開啟動畫
2.3.3 LinearLayoutManager#onLayoutChildren()
以垂直方向的RecyclerView為例子,我們填充RecyclerView的方向有兩種,從上往下填充和從下往上填充。開始填充的位置不是固定的,可以從RecyclerView的任意位置處開始填充。
-
尋找填充的錨點(最終呼叫findReferenceChild方法); -
移除螢幕上的Views(最終呼叫detachAndScrapAttachedViews方法); -
從錨點處從上往下填充(呼叫fill和layoutChunk方法); -
從錨點處從下往上填充(呼叫fill和layoutChunk方法); -
如果還有多餘的空間,繼續填充(呼叫fill和layoutChunk方法); -
佈局完成後有可能產生GAP,需要修復GAP; -
dispatchLayoutStep2階段呼叫layoutForPredictiveAnimation將scrapList中多餘的ViewHolder填充(呼叫fill和layoutChunk方法)。

2.3.3.1 尋找填充的錨點
-
優先返回全部在螢幕內,未標記removed的View; -
次優先順序返回不可見的View; -
最低優先順序返回刪掉的view。

2.3.3.2 移除螢幕上的Views
-
呼叫notifyItemChanged(position),position對應的ViewHolder會放入到mChangedScrap快取中; -
否則會放入到mAttachedScrap快取中
2.3.3.3 ~ 2.3.3.5 填充
呼叫LinearLayoutManager#fill()和LinearLayoutManager#layoutChunk()
-
從快取中獲取View或者建立View -
如果是step1預佈局階段,呼叫addView(),將標記為removed的view放入到DISAPPEARED動畫列表中 -
如果是step2佈局階段,呼叫addDisappearingView(),將被擠出螢幕的view放入到DISAPPEARED動畫列表中 -
如果是removed的或者changed,不會記錄消耗的填充量

2.3.3.6 修復GAP
通過mOrientationHelper.offsetChildren(gap)直接填補GAP

2.3.3.7 layoutForPredictiveAnimation
為了做動畫,增加額外的Item
-
不需要做動畫,或者是預佈局直接返回 -
從mAttachedScrap中遍歷到非removed的ViewHolder,但是返回的結果可能包含removed ViewHolder -
如果遍歷找到了非Removed ViewHolder,填充View
3. 佈局階段
3.1 核心方法
-
RecyclerView#dispatchLayoutStep2() -
LinearLayoutManager#layoutChunk() -
LinearLayoutManager#addDisappearingView() -
ViewInfoStore#addToDisappearedInLayout()
3.2 作用
-
根據資料來源中的資料進行佈局,真正展示給使用者看的最終介面 -
如果開啟動畫,將被擠出螢幕的View的儲存到消失動畫列表中
3.3 原始碼解析
3.3.1 RecyclerView#dispatchLayoutStep2()
-
將預佈局模式改為false -
佈局填充View
3.3.2 LinearLayoutManager#layoutChunk()
佈局階段將被擠出螢幕的View放入到DISAPPEARED動畫列表中
3.3.3 LinearLayoutManager#addDisappearingView()
把Removed的View或被擠出螢幕的View新增到Disappearing動畫列表
3.3.4 ViewInfoStore#addToDisappearedInLayout()
加入到Disappeared動畫列表
4. 觸發動畫階段
4.1 核心方法
-
RecyclerView#dispatchLayoutStep3() -
ViewInfoStore#addToPostLayout() -
ViewInfoStore#process() -
ItemAnimator#animateAppearance()
4.2 作用
-
清理工作 -
儲存佈局後的view的資訊 -
觸發動畫 -
動畫執行完回收工作
4.3 原始碼解析
4.3.1 RecyclerView#dispatchLayoutStep3()
-
將當前螢幕上的View資訊記錄到postLayout動畫列表中 -
執行動畫 -
清理操作 -
佈局完成回撥
4.3.2 ViewInfoStore#addToPostLayout()
View資訊記錄到postLayout動畫列表中
4.3.3 ViewInfoStore#process()
作用:執行動畫
工作流程,按優先順序執行
-
呼叫unuse() 將view回收掉 -
執行消失動畫
-
2.1 預佈局中不可見呼叫unuse() -
2.2 呼叫processDisappeared()
-
呼叫processPersistent()執行move或者change動畫 -
執行remove動畫 -
執行insert動畫
4.3.4 ViewInfoStore$InfoRecord
作用:定義動畫型別
-
FLAG_DISAPPEARED:消失動畫,包含move和remove動畫 -
FLAG_APPEAR:出現動畫,包含move和insert動畫 -
FLAG_PRE:預佈局前已經顯示在RecyclerView上 -
FLAG_POST:佈局後顯示在RecyclerView上 -
FLAG_APPEAR_AND_DISAPPEAR:先做出現動畫,再做消失動畫,無意義 -
FLAG_PRE_AND_POST:預佈局前和佈局後一直顯示在RecyclerView上 -
FLAG_APPEAR_PRE_AND_POST:在FLAG_PRE_AND_POST基礎上做出現動畫

4.3.5 ViewInfoStore$ProccessCallback
作用:定義四種處理動畫的介面
-
processDisappeared 處理消失動畫 -
processAppeared 處理出現動畫 -
processPersistent 處理一直存在動畫,包含move和change動畫 -
unused 不需要處理動畫,執行回收

4.3.6 介面實現

4.3.7 ProccessCallback#processAppeared
兵分兩路
-
呼叫ItemAnimator#animateAppearance() -
呼叫RecyclerView#postAnimationRunner()

4.3.8 一路兵:ItemAnimator#animateAppearance()
4.3.8.1 SimpleItemAnimator#animateAppearance
-
該方法返回true表示需要做動畫 -
否則不需要做動畫 -
如果預佈局前View已經存在而且位置發生改變,處理MOVE動畫 -
否則,處理ADD動畫
4.3.8.2 DefaultItemAnimator.animateMove
-
該方法並沒有真正執行動畫 -
將MoveInfo儲存到mPendingMoves中,以便RecyclerView#postAnimationRunner()使用 -
判斷是否有必要執行MOVE動畫 -
回到preLayout的位置

4.3.8.3 DefaultItemAnimator.animateAdd
先呼叫setAlpha(0),以便做淡入動畫
4.3.9 二路兵:RecyclerView#postAnimationRunner()
4.3.9.1 RecyclerView#postAnimationRunner
最終呼叫到ItemAnimator.runPendingAnimations

4.3.9.2 DefaultItemAnimator.runPendingAnimations
-
首先執行Remove動畫 -
然後同時執行Move和Change動畫 -
最後執行Add動畫
動畫的總時長為removeDuration + Math.max(moveDuration, changeDuration) + addDuration

4.3.10 RecyclerView$ItemAnimatorRestoreListener
作用:動畫結束後執行回收操作
-
動畫執行完畢,removeAnimatingView -
呼叫Recycler.recycleViewHolderInternal執行回收操作
5. 場景篇
5.1 notifyItemRemoved場景
5.1.1 場景描述
-
呼叫notifyItemRemoved() -
Adapter資料有100條,螢幕上有Item1~Item6 6個View,刪除Item1和Item2

5.1.2 佈局過程
-
將Item1 Item2對應的ViewHolder設定為REMOVE狀態 -
將所有的Item對應的ViewHolder的mPreLayoutPosition欄位賦值為當前的position
5.1.2.1 dispatchLayoutStep1階段
-
尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item。在本Case中,找到Item3
-
移除螢幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap快取中,這個快取的好處是如果position對應上了,無需重新繫結,直接拿來用。
-
從錨點Item3處往下填充,mAttachedScrap只剩下ViewHolder2和ViewHolder1
-
從錨點Item3處往上填充Item2 Item1,因為Item2,Imte1已經被remove掉了,它消耗的空間不會被記錄,那麼到步驟5的時候還可以填充
-
還有多餘的空間,繼續填充,把Item7、Item8填充到螢幕中
-
因為當前是預佈局,直接返回
5.1.2.2 dispatchLayoutStep2階段
-
尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item,找到Item3
-
移除螢幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap快取中
-
從錨點Item3處往下填充,填充到Item6為止,就沒有足夠的距離了,mAttachedScrap只剩下ViewHolder8,ViewHolder7,ViewHolder2,ViewHolder1
-
往上填充,雖然此時還有兩個View的高度,但是此時,上邊沒有資料了,此處不填充
-
此時還有兩個View的高度,繼續往下填充
-
修復GAP

-
當前是佈局階段,但是因為ViewHolder1和ViewHolder2都是被Remove掉的,所以跳過
5.1.2.3 dispatchLayoutStep3階段
-
Item1、Item2做消失動畫 -
Item3、Item4~Item8做移動動畫 -
動畫結束後,Item1、Item2會被回收到mCachedViews快取池中
5.2 notifyItemInserted場景
5.2.1 場景描述
假設在Item1下面插入兩條資料AddItem1,AddItem2

5.2.2 佈局過程
5.2.2.1 dispatchLayoutStep1階段
-
尋找錨點,找到Item1 -
移除螢幕上的Views,放入到mAttachedScrap中 -
錨點處從上往下填充 -
錨點處從下往上填充,由上圖可知,上面沒有空間了,不填充 -
判斷是否還有剩餘的空間,如果有在末尾填充,下面沒空間了,不填充 -
因為當前是預佈局階段,不填充
5.2.2.2 dispatchLayoutStep2階段
-
尋找錨點,找到Item1 -
移除螢幕上的Views,放入到mAttachedScrap中 -
錨點處從上往下填充,此時將變化後的資料填充到螢幕上,addItem1和addItem2被填充到item1下面 -
錨點處從下往上填充,由圖可知,沒有空間不填充 -
判斷是否還有剩餘的空間,由圖可知,沒有空間不填充 -
當前是layoutStep2階段,會將mAttachScrap的內容,填充到螢幕末尾,ViewHolder5和ViewHolder6對應的ItemView被填充

5.2.2.3 dispatchLayoutStep3階段
-
Item2、Item3~Item6做移動動畫 -
addItem1、addItem2做淡入動畫 -
動畫結束後Item5、Item6被回收到mCachedViews快取池中

5.3 場景總結
5.3.1 notifyItemRemoved場景

5.3.2 notifyItemInserted場景

本文分享自微信公眾號 - 音視訊開發進階(glumes_blog)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。
- 音視訊進階教程-實現直播間的自定義視訊渲染
- 音視訊開發進階|第六講:色彩和色彩空間·上篇
- H264 視訊檔案如何縮放解析度?
- 星球專享 | 播放器 FFmpeg 依賴庫的配置
- 乾貨收藏 || Vulkan Game Engine 視訊教程
- HDR技術趨勢淺析
- WebRTC 實現 Android 傳屏 demo
- 技術群裡如何提問才能獲得更高的回覆率呢?
- 【建議收藏】30 分鐘入門 Vulkan (中文翻譯版)
- 淺談音視訊自動化測試
- 揭祕版權保護下的視訊隱形水印演算法(下篇)
- 如何用研發效能搞垮一個團隊
- 如何實現H.264的實時傳輸?
- 短視訊中解決音視訊混音出現雜音的問題
- 位元組跳動招聘:30-60k 不限工作經驗!什麼崗位這麼香?
- 進擊的斜槓程式設計師 | 音視訊技術內容變現
- 面試官:RecyclerView佈局動畫原理了解嗎?
- 音視訊開發進階-學習筆記3-使用LAME編碼mp3檔案
- 音視訊開發進階-學習筆記2-LAME交叉編譯
- 網際網路寒冬之下,Android開發的港灣:音視訊進階學習