面試官:RecyclerView布局動畫原理了解嗎?
前言
「溫馨提示:文章有點長,建議關注微信公眾號“字節(jié)小站”收藏閱讀」
本文主要通過以下幾個方面來講解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()與其他方法的區(qū)別:
會導致整個列表刷新,其它幾個方法則不會; 不會觸發(fā)RecyclerView的動畫機制,其它幾個方法則會觸發(fā)各種不同類型的動畫。
1. 布局放置
1.1 核心方法
RecyclerView#dispatchLayout()
1.2 作用
將View放置到合適的位置 記錄布局階段View的信息 處理動畫
RecyclerView的布局我們可以分成三個階段,也可以精細分成五個階段。
1.2.1 三個階段
1.2.1.1 預布局階段
當需要做動畫時,預布局階段才會工作,否則沒有實際意義,它對應dispatchLayoutStep1方法。動畫有開始狀態(tài)和結束狀態(tài),預布局完成后的RecyclerView是動畫的開始狀態(tài)。
1.2.1.2 布局階段
無論是否需要做動畫,布局階段都會工作,它對應dispatchLayoutStep2方法。布局完成后的狀態(tài)是用戶最終看到的狀態(tài),也是動畫的結束狀態(tài)。
1.2.1.3 布局后階段
布局完成后,需要執(zhí)行動畫操作,它對應的是dispatchLayoutStep3方法。當動畫完成后,還會進行View回收操作。
1.2.2 五個階段
1.2.2.1 預布局前
在dispatchLayoutStep1方法調(diào)用onLayoutChildren方法之前。它會保存當前RecyclerView上所有子View的信息到ViewInfoStore中,F(xiàn)LAG增加FLAG_PRE。表示View在預布局前就顯示在RecyclerView上。
1.2.2.2 預布局中
在dispatchLayoutStep1方法調(diào)用onLayoutChildren方法時。它會根據(jù)算法,重新布置RecyclerView的子View,該階段可能會添加新的子View。該階段能夠確定哪些View最終是不會展示給用戶看的,F(xiàn)LAG增加FLAG_DISAPPEARED(例如:removed的View)。
1.2.2.3 預布局后
在dispatchLayoutStep1方法調(diào)用onLayoutChildren方法之后,將預布局完成后的子View與預布局前的子View對比,將新增的View的FLAG增加FLAG_APPEAR(調(diào)用notifyItemRemoved后,新填充的View)。
1.2.2.4 布局中
在dispatchLayoutStep2方法調(diào)用onLayoutChildren方法時。該階段會把被擠出屏幕的View的FLAG增加FLAG_DISAPPEARED。
1.2.2.5 布局后
在dispatchLayoutStep3方法中。會將最終的子View的FLAG增加FLAG_POST。
1.2.3 動畫類型
1.2.3.1 PERSISTENT
預布局前和布局后都存在的View所做的動畫,位置有可能發(fā)生變化了,也有可能沒有發(fā)生變化。
1.2.3.2 REMOVED
在布局前對用戶可見,布局后不可見,而且數(shù)據(jù)已經(jīng)從數(shù)據(jù)源中刪除掉了。
1.2.3.3 ADDED
新增數(shù)據(jù)到數(shù)據(jù)源中,并且在布局后對用戶可見。
1.2.3.4 DISAPPEARING
數(shù)據(jù)一直都存在于數(shù)據(jù)源中,但是布局后從可見變成不可見狀態(tài)(例如因為其它View插入操作,導致被擠出屏幕外了)。
1.2.3.5 APPEARING
數(shù)據(jù)一直都存在于數(shù)據(jù)源中,但是布局后從不可見變成可見狀態(tài)(例如因為其它View被刪除,導致補位到屏幕內(nèi)了)。
1.3 源碼解析
1.3.1 RecyclerView#dispatchLayout()
dispatchLayoutStep1()執(zhí)行預布局,記錄ViewHolder位置信息; dispatchLayoutStep2()執(zhí)行布局,用戶最終看到的效果; dispatchLayoutStep3()執(zhí)行動畫操作。 
2. 預布局階段
2.1 核心方法
RecyclerView#dispatchLayoutStep1()
RecyclerView#processAdapterUpdatesAndSetAnimationFlags()
LinearLayoutManager#onLayoutChildren()
LinearLayoutManager#updateAnchorInfoForLayout()
2.2 作用
處理Adapter變化 決定該執(zhí)行哪種類型動畫 保存當前RecyclerView上的子View的信息 如果需要執(zhí)行動畫,進行預布局
2.3 源碼解析
2.3.1 RecyclerView#dispatchLayoutStep1()
判斷是否需要開啟動畫功能 如果開啟動畫,將當前屏幕上的Item相關信息保存起來供后續(xù)動畫使用 如果開啟動畫,調(diào)用mLayout.onLayoutChildren方法預布局 預布局后,與第二步保存的信息對比,將新出現(xiàn)的Item信息保存到Appeared中

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

2.3.3.1 尋找填充的錨點
優(yōu)先返回全部在屏幕內(nèi),未標記removed的View; 次優(yōu)先級返回不可見的View; 最低優(yōu)先級返回刪掉的view。

2.3.3.2 移除屏幕上的Views
調(diào)用notifyItemChanged(position),position對應的ViewHolder會放入到mChangedScrap緩存中; 否則會放入到mAttachedScrap緩存中 
2.3.3.3 ~ 2.3.3.5 填充
調(diào)用LinearLayoutManager#fill()和LinearLayoutManager#layoutChunk()
從緩存中獲取View或者創(chuàng)建View 如果是step1預布局階段,調(diào)用addView(),將標記為removed的view放入到DISAPPEARED動畫列表中 如果是step2布局階段,調(diào)用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 作用
根據(jù)數(shù)據(jù)源中的數(shù)據(jù)進行布局,真正展示給用戶看的最終界面 如果開啟動畫,將被擠出屏幕的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. 觸發(fā)動畫階段
4.1 核心方法
RecyclerView#dispatchLayoutStep3() ViewInfoStore#addToPostLayout() ViewInfoStore#process() ItemAnimator#animateAppearance()
4.2 作用
清理工作 保存布局后的view的信息 觸發(fā)動畫 動畫執(zhí)行完回收工作
4.3 源碼解析
4.3.1 RecyclerView#dispatchLayoutStep3()
將當前屏幕上的View信息記錄到postLayout動畫列表中 執(zhí)行動畫 清理操作 布局完成回調(diào) 
4.3.2 ViewInfoStore#addToPostLayout()
View信息記錄到postLayout動畫列表中
4.3.3 ViewInfoStore#process()
作用:執(zhí)行動畫
工作流程,按優(yōu)先級執(zhí)行
調(diào)用unuse() 將view回收掉 執(zhí)行消失動畫
2.1 預布局中不可見調(diào)用unuse() 2.2 調(diào)用processDisappeared()
調(diào)用processPersistent()執(zhí)行move或者change動畫 執(zhí)行remove動畫 執(zhí)行insert動畫 
4.3.4 ViewInfoStore$InfoRecord
作用:定義動畫類型
FLAG_DISAPPEARED:消失動畫,包含move和remove動畫 FLAG_APPEAR:出現(xiàn)動畫,包含move和insert動畫 FLAG_PRE:預布局前已經(jīng)顯示在RecyclerView上 FLAG_POST:布局后顯示在RecyclerView上 FLAG_APPEAR_AND_DISAPPEAR:先做出現(xiàn)動畫,再做消失動畫,無意義 FLAG_PRE_AND_POST:預布局前和布局后一直顯示在RecyclerView上 FLAG_APPEAR_PRE_AND_POST:在FLAG_PRE_AND_POST基礎上做出現(xiàn)動畫

4.3.5 ViewInfoStore$ProccessCallback
作用:定義四種處理動畫的接口
processDisappeared 處理消失動畫 processAppeared 處理出現(xiàn)動畫 processPersistent 處理一直存在動畫,包含move和change動畫 unused 不需要處理動畫,執(zhí)行回收

4.3.6 接口實現(xiàn)

4.3.7 ProccessCallback#processAppeared
兵分兩路
調(diào)用ItemAnimator#animateAppearance() 調(diào)用RecyclerView#postAnimationRunner()

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

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

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

4.3.10 RecyclerView$ItemAnimatorRestoreListener
作用:動畫結束后執(zhí)行回收操作
動畫執(zhí)行完畢,removeAnimatingView 調(diào)用Recycler.recycleViewHolderInternal執(zhí)行回收操作 
5. 場景篇
5.1 notifyItemRemoved場景
5.1.1 場景描述
調(diào)用notifyItemRemoved() Adapter數(shù)據(jù)有100條,屏幕上有Item1~Item6 6個View,刪除Item1和Item2

5.1.2 布局過程
將Item1 Item2對應的ViewHolder設置為REMOVE狀態(tài) 將所有的Item對應的ViewHolder的mPreLayoutPosition字段賦值為當前的position
5.1.2.1 dispatchLayoutStep1階段
尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態(tài)的Item。在本Case中,找到Item3

移除屏幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap緩存中,這個緩存的好處是如果position對應上了,無需重新綁定,直接拿來用。

從錨點Item3處往下填充,mAttachedScrap只剩下ViewHolder2和ViewHolder1

從錨點Item3處往上填充Item2 Item1,因為Item2,Imte1已經(jīng)被remove掉了,它消耗的空間不會被記錄,那么到步驟5的時候還可以填充

還有多余的空間,繼續(xù)填充,把Item7、Item8填充到屏幕中

因為當前是預布局,直接返回
5.1.2.2 dispatchLayoutStep2階段
尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態(tài)的Item,找到Item3

移除屏幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap緩存中

從錨點Item3處往下填充,填充到Item6為止,就沒有足夠的距離了,mAttachedScrap只剩下ViewHolder8,ViewHolder7,ViewHolder2,ViewHolder1

往上填充,雖然此時還有兩個View的高度,但是此時,上邊沒有數(shù)據(jù)了,此處不填充

此時還有兩個View的高度,繼續(xù)往下填充

修復GAP

當前是布局階段,但是因為ViewHolder1和ViewHolder2都是被Remove掉的,所以跳過 
5.1.2.3 dispatchLayoutStep3階段
Item1、Item2做消失動畫 Item3、Item4~Item8做移動動畫 動畫結束后,Item1、Item2會被回收到mCachedViews緩存池中 
5.2 notifyItemInserted場景
5.2.1 場景描述
假設在Item1下面插入兩條數(shù)據(jù)AddItem1,AddItem2

5.2.2 布局過程
5.2.2.1 dispatchLayoutStep1階段
尋找錨點,找到Item1 
移除屏幕上的Views,放入到mAttachedScrap中 
錨點處從上往下填充 
錨點處從下往上填充,由上圖可知,上面沒有空間了,不填充 判斷是否還有剩余的空間,如果有在末尾填充,下面沒空間了,不填充 因為當前是預布局階段,不填充
5.2.2.2 dispatchLayoutStep2階段
尋找錨點,找到Item1 
移除屏幕上的Views,放入到mAttachedScrap中 
錨點處從上往下填充,此時將變化后的數(shù)據(jù)填充到屏幕上,addItem1和addItem2被填充到item1下面 
錨點處從下往上填充,由圖可知,沒有空間不填充 判斷是否還有剩余的空間,由圖可知,沒有空間不填充 當前是layoutStep2階段,會將mAttachScrap的內(nèi)容,填充到屏幕末尾,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場景

