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

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

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

2.3.3.2 移除屏幕上的Views
調(diào)用notifyItemChanged(position),position對(duì)應(yīng)的ViewHolder會(huì)放入到mChangedScrap緩存中; 否則會(huì)放入到mAttachedScrap緩存中 
2.3.3.3 ~ 2.3.3.5 填充
調(diào)用LinearLayoutManager#fill()和LinearLayoutManager#layoutChunk()
從緩存中獲取View或者創(chuàng)建View 如果是step1預(yù)布局階段,調(diào)用addView(),將標(biāo)記為removed的view放入到DISAPPEARED動(dòng)畫列表中 如果是step2布局階段,調(diào)用addDisappearingView(),將被擠出屏幕的view放入到DISAPPEARED動(dòng)畫列表中 如果是removed的或者changed,不會(huì)記錄消耗的填充量

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

2.3.3.7 layoutForPredictiveAnimation
為了做動(dòng)畫,增加額外的Item
不需要做動(dòng)畫,或者是預(yù)布局直接返回 從mAttachedScrap中遍歷到非removed的ViewHolder,但是返回的結(jié)果可能包含removed ViewHolder 如果遍歷找到了非Removed ViewHolder,填充View 
3. 布局階段
3.1 核心方法
RecyclerView#dispatchLayoutStep2() LinearLayoutManager#layoutChunk() LinearLayoutManager#addDisappearingView() ViewInfoStore#addToDisappearedInLayout()
3.2 作用
根據(jù)數(shù)據(jù)源中的數(shù)據(jù)進(jìn)行布局,真正展示給用戶看的最終界面 如果開啟動(dòng)畫,將被擠出屏幕的View的保存到消失動(dòng)畫列表中
3.3 源碼解析
3.3.1 RecyclerView#dispatchLayoutStep2()
將預(yù)布局模式改為false 布局填充View 
3.3.2 LinearLayoutManager#layoutChunk()
布局階段將被擠出屏幕的View放入到DISAPPEARED動(dòng)畫列表中
3.3.3 LinearLayoutManager#addDisappearingView()
把Removed的View或被擠出屏幕的View添加到Disappearing動(dòng)畫列表
3.3.4 ViewInfoStore#addToDisappearedInLayout()
加入到Disappeared動(dòng)畫列表
4. 觸發(fā)動(dòng)畫階段
4.1 核心方法
RecyclerView#dispatchLayoutStep3() ViewInfoStore#addToPostLayout() ViewInfoStore#process() ItemAnimator#animateAppearance()
4.2 作用
清理工作 保存布局后的view的信息 觸發(fā)動(dòng)畫 動(dòng)畫執(zhí)行完回收工作
4.3 源碼解析
4.3.1 RecyclerView#dispatchLayoutStep3()
將當(dāng)前屏幕上的View信息記錄到postLayout動(dòng)畫列表中 執(zhí)行動(dòng)畫 清理操作 布局完成回調(diào) 
4.3.2 ViewInfoStore#addToPostLayout()
View信息記錄到postLayout動(dòng)畫列表中
4.3.3 ViewInfoStore#process()
作用:執(zhí)行動(dòng)畫
工作流程,按優(yōu)先級(jí)執(zhí)行
調(diào)用unuse() 將view回收掉 執(zhí)行消失動(dòng)畫
2.1 預(yù)布局中不可見調(diào)用unuse() 2.2 調(diào)用processDisappeared()
調(diào)用processPersistent()執(zhí)行move或者change動(dòng)畫 執(zhí)行remove動(dòng)畫 執(zhí)行insert動(dòng)畫 
4.3.4 ViewInfoStore$InfoRecord
作用:定義動(dòng)畫類型
FLAG_DISAPPEARED:消失動(dòng)畫,包含move和remove動(dòng)畫 FLAG_APPEAR:出現(xiàn)動(dòng)畫,包含move和insert動(dòng)畫 FLAG_PRE:預(yù)布局前已經(jīng)顯示在RecyclerView上 FLAG_POST:布局后顯示在RecyclerView上 FLAG_APPEAR_AND_DISAPPEAR:先做出現(xiàn)動(dòng)畫,再做消失動(dòng)畫,無意義 FLAG_PRE_AND_POST:預(yù)布局前和布局后一直顯示在RecyclerView上 FLAG_APPEAR_PRE_AND_POST:在FLAG_PRE_AND_POST基礎(chǔ)上做出現(xiàn)動(dòng)畫

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

4.3.6 接口實(shí)現(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表示需要做動(dòng)畫 否則不需要做動(dòng)畫 如果預(yù)布局前View已經(jīng)存在而且位置發(fā)生改變,處理MOVE動(dòng)畫 否則,處理ADD動(dòng)畫 
4.3.8.2 DefaultItemAnimator.animateMove
該方法并沒有真正執(zhí)行動(dòng)畫 將MoveInfo保存到mPendingMoves中,以便RecyclerView#postAnimationRunner()使用 判斷是否有必要執(zhí)行MOVE動(dòng)畫 回到preLayout的位置

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

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

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

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

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

從錨點(diǎn)Item3處往下填充,mAttachedScrap只剩下ViewHolder2和ViewHolder1

從錨點(diǎn)Item3處往上填充Item2 Item1,因?yàn)镮tem2,Imte1已經(jīng)被remove掉了,它消耗的空間不會(huì)被記錄,那么到步驟5的時(shí)候還可以填充

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

因?yàn)楫?dāng)前是預(yù)布局,直接返回
5.1.2.2 dispatchLayoutStep2階段
尋找填充的錨點(diǎn),尋找錨點(diǎn)的邏輯是,從上往下,找到第一個(gè)非remove狀態(tài)的Item,找到Item3

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

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

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

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

修復(fù)GAP

當(dāng)前是布局階段,但是因?yàn)閂iewHolder1和ViewHolder2都是被Remove掉的,所以跳過 
5.1.2.3 dispatchLayoutStep3階段
Item1、Item2做消失動(dòng)畫 Item3、Item4~Item8做移動(dòng)動(dòng)畫 動(dòng)畫結(jié)束后,Item1、Item2會(huì)被回收到mCachedViews緩存池中 
5.2 notifyItemInserted場(chǎng)景
5.2.1 場(chǎng)景描述
假設(shè)在Item1下面插入兩條數(shù)據(jù)AddItem1,AddItem2

5.2.2 布局過程
5.2.2.1 dispatchLayoutStep1階段
尋找錨點(diǎn),找到Item1 
移除屏幕上的Views,放入到mAttachedScrap中 
錨點(diǎn)處從上往下填充 
錨點(diǎn)處從下往上填充,由上圖可知,上面沒有空間了,不填充 判斷是否還有剩余的空間,如果有在末尾填充,下面沒空間了,不填充 因?yàn)楫?dāng)前是預(yù)布局階段,不填充
5.2.2.2 dispatchLayoutStep2階段
尋找錨點(diǎn),找到Item1 
移除屏幕上的Views,放入到mAttachedScrap中 
錨點(diǎn)處從上往下填充,此時(shí)將變化后的數(shù)據(jù)填充到屏幕上,addItem1和addItem2被填充到item1下面 
錨點(diǎn)處從下往上填充,由圖可知,沒有空間不填充 判斷是否還有剩余的空間,由圖可知,沒有空間不填充 當(dāng)前是layoutStep2階段,會(huì)將mAttachScrap的內(nèi)容,填充到屏幕末尾,ViewHolder5和ViewHolder6對(duì)應(yīng)的ItemView被填充

5.2.2.3 dispatchLayoutStep3階段
Item2、Item3~Item6做移動(dòng)動(dòng)畫 addItem1、addItem2做淡入動(dòng)畫 動(dòng)畫結(jié)束后Item5、Item6被回收到mCachedViews緩存池中

5.3 場(chǎng)景總結(jié)
5.3.1 notifyItemRemoved場(chǎng)景

5.3.2 notifyItemInserted場(chǎng)景

—————END—————
我是南塵,只做比心的公眾號(hào),歡迎關(guān)注我。
推薦閱讀:
歡迎關(guān)注南塵的公眾號(hào):nanchen
做不完的開源,寫不完的矯情,只做比心的公眾號(hào),如果你喜歡,你可以選擇分享給大家。如果你有好的文章,歡迎投稿,讓我們一起來分享。? ? ??? ??長(zhǎng)按上方二維碼關(guān)注? ? ? ? 做不完的開源,寫不完的矯情? ? ? ? 一起來看 nanchen 同學(xué)的成長(zhǎng)筆記
