面試官:RecyclerView佈局動畫原理了解嗎?

語言: CN / TW / HK

前言

「溫馨提示:文章有點長,建議關注微信公眾號“位元組小站”收藏閱讀」

本文主要通過以下幾個方面來講解RecyclerView的佈局和動畫原理:

  1. 佈局放置:RecyclerView#dispatchLayout()
  2. 預佈局階段:RecyclerView#dispatchLayoutStep1()
  3. 佈局階段:RecyclerView#dispatchLayoutStep2()
  4. 開啟動畫階段: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()與其他方法的區別:

  1. 會導致整個列表重新整理,其它幾個方法則不會;
  2. 不會觸發RecyclerView的動畫機制,其它幾個方法則會觸發各種不同型別的動畫。

1. 佈局放置

1.1 核心方法

RecyclerView#dispatchLayout()

1.2 作用

  1. 將View放置到合適的位置
  2. 記錄佈局階段View的資訊
  3. 處理動畫

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()

  1. dispatchLayoutStep1()執行預佈局,記錄ViewHolder位置資訊;
  2. dispatchLayoutStep2()執行佈局,使用者最終看到的效果;
  3. dispatchLayoutStep3()執行動畫操作。

2. 預佈局階段

2.1 核心方法

  1. RecyclerView#dispatchLayoutStep1()

  2. RecyclerView#processAdapterUpdatesAndSetAnimationFlags()

  3. LinearLayoutManager#onLayoutChildren()

  4. LinearLayoutManager#updateAnchorInfoForLayout()

2.2 作用

  1. 處理Adapter變化
  2. 決定該執行哪種型別動畫
  3. 儲存當前RecyclerView上的子View的資訊
  4. 如果需要執行動畫,進行預佈局

2.3 原始碼解析

2.3.1 RecyclerView#dispatchLayoutStep1()

  1. 判斷是否需要開啟動畫功能
  2. 如果開啟動畫,將當前螢幕上的Item相關資訊儲存起來供後續動畫使用
  3. 如果開啟動畫,呼叫mLayout.onLayoutChildren方法預佈局
  4. 預佈局後,與第二步儲存的資訊對比,將新出現的Item資訊儲存到Appeared中

2.3.2 RecyclerView#processAdapterUpdatesAndSetAnimationFlags()

作用:判斷是否需要開啟動畫

2.3.3 LinearLayoutManager#onLayoutChildren()

以垂直方向的RecyclerView為例子,我們填充RecyclerView的方向有兩種,從上往下填充和從下往上填充。開始填充的位置不是固定的,可以從RecyclerView的任意位置處開始填充。

  1. 尋找填充的錨點(最終呼叫findReferenceChild方法);
  2. 移除螢幕上的Views(最終呼叫detachAndScrapAttachedViews方法);
  3. 從錨點處從上往下填充(呼叫fill和layoutChunk方法);
  4. 從錨點處從下往上填充(呼叫fill和layoutChunk方法);
  5. 如果還有多餘的空間,繼續填充(呼叫fill和layoutChunk方法);
  6. 佈局完成後有可能產生GAP,需要修復GAP;
  7. dispatchLayoutStep2階段呼叫layoutForPredictiveAnimation將scrapList中多餘的ViewHolder填充(呼叫fill和layoutChunk方法)。

2.3.3.1 尋找填充的錨點

  1. 優先返回全部在螢幕內,未標記removed的View;
  2. 次優先順序返回不可見的View;
  3. 最低優先順序返回刪掉的view。

2.3.3.2 移除螢幕上的Views

  1. 呼叫notifyItemChanged(position),position對應的ViewHolder會放入到mChangedScrap快取中;
  2. 否則會放入到mAttachedScrap快取中

2.3.3.3 ~ 2.3.3.5 填充

呼叫LinearLayoutManager#fill()和LinearLayoutManager#layoutChunk()

  1. 從快取中獲取View或者建立View
  2. 如果是step1預佈局階段,呼叫addView(),將標記為removed的view放入到DISAPPEARED動畫列表中
  3. 如果是step2佈局階段,呼叫addDisappearingView(),將被擠出螢幕的view放入到DISAPPEARED動畫列表中
  4. 如果是removed的或者changed,不會記錄消耗的填充量

2.3.3.6 修復GAP

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


2.3.3.7 layoutForPredictiveAnimation

為了做動畫,增加額外的Item

  1. 不需要做動畫,或者是預佈局直接返回
  2. 從mAttachedScrap中遍歷到非removed的ViewHolder,但是返回的結果可能包含removed ViewHolder
  3. 如果遍歷找到了非Removed ViewHolder,填充View

3. 佈局階段

3.1 核心方法

  1. RecyclerView#dispatchLayoutStep2()
  2. LinearLayoutManager#layoutChunk()
  3. LinearLayoutManager#addDisappearingView()
  4. ViewInfoStore#addToDisappearedInLayout()

3.2 作用

  1. 根據資料來源中的資料進行佈局,真正展示給使用者看的最終介面
  2. 如果開啟動畫,將被擠出螢幕的View的儲存到消失動畫列表中

3.3 原始碼解析

3.3.1 RecyclerView#dispatchLayoutStep2()

  1. 將預佈局模式改為false
  2. 佈局填充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 核心方法

  1. RecyclerView#dispatchLayoutStep3()
  2. ViewInfoStore#addToPostLayout()
  3. ViewInfoStore#process()
  4. ItemAnimator#animateAppearance()

4.2 作用

  1. 清理工作
  2. 儲存佈局後的view的資訊
  3. 觸發動畫
  4. 動畫執行完回收工作

4.3 原始碼解析

4.3.1 RecyclerView#dispatchLayoutStep3()

  1. 將當前螢幕上的View資訊記錄到postLayout動畫列表中
  2. 執行動畫
  3. 清理操作
  4. 佈局完成回撥

4.3.2 ViewInfoStore#addToPostLayout()

View資訊記錄到postLayout動畫列表中

4.3.3 ViewInfoStore#process()

作用:執行動畫

工作流程,按優先順序執行

  1. 呼叫unuse() 將view回收掉
  2. 執行消失動畫
  • 2.1 預佈局中不可見呼叫unuse()
  • 2.2 呼叫processDisappeared()
  1. 呼叫processPersistent()執行move或者change動畫
  2. 執行remove動畫
  3. 執行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

兵分兩路

  1. 呼叫ItemAnimator#animateAppearance()
  2. 呼叫RecyclerView#postAnimationRunner()

4.3.8 一路兵:ItemAnimator#animateAppearance()

4.3.8.1 SimpleItemAnimator#animateAppearance
  1. 該方法返回true表示需要做動畫
  2. 否則不需要做動畫
  3. 如果預佈局前View已經存在而且位置發生改變,處理MOVE動畫
  4. 否則,處理ADD動畫
4.3.8.2 DefaultItemAnimator.animateMove
  1. 該方法並沒有真正執行動畫
  2. 將MoveInfo儲存到mPendingMoves中,以便RecyclerView#postAnimationRunner()使用
  3. 判斷是否有必要執行MOVE動畫
  4. 回到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
  1. 首先執行Remove動畫
  2. 然後同時執行Move和Change動畫
  3. 最後執行Add動畫

動畫的總時長為removeDuration + Math.max(moveDuration, changeDuration) + addDuration

4.3.10 RecyclerView$ItemAnimatorRestoreListener

作用:動畫結束後執行回收操作

  1. 動畫執行完畢,removeAnimatingView
  2. 呼叫Recycler.recycleViewHolderInternal執行回收操作

5. 場景篇

5.1 notifyItemRemoved場景

5.1.1 場景描述

  1. 呼叫notifyItemRemoved()
  2. Adapter資料有100條,螢幕上有Item1~Item6 6個View,刪除Item1和Item2

5.1.2 佈局過程

  1. 將Item1 Item2對應的ViewHolder設定為REMOVE狀態
  2. 將所有的Item對應的ViewHolder的mPreLayoutPosition欄位賦值為當前的position

5.1.2.1 dispatchLayoutStep1階段

  1. 尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item。在本Case中,找到Item3

  2. 移除螢幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap快取中,這個快取的好處是如果position對應上了,無需重新繫結,直接拿來用。

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

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

  5. 還有多餘的空間,繼續填充,把Item7、Item8填充到螢幕中

  6. 因為當前是預佈局,直接返回


5.1.2.2 dispatchLayoutStep2階段

  1. 尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item,找到Item3

  2. 移除螢幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap快取中

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

  4. 往上填充,雖然此時還有兩個View的高度,但是此時,上邊沒有資料了,此處不填充

  5. 此時還有兩個View的高度,繼續往下填充

  6. 修復GAP

  1. 當前是佈局階段,但是因為ViewHolder1和ViewHolder2都是被Remove掉的,所以跳過

5.1.2.3 dispatchLayoutStep3階段

  1. Item1、Item2做消失動畫
  2. Item3、Item4~Item8做移動動畫
  3. 動畫結束後,Item1、Item2會被回收到mCachedViews快取池中

5.2 notifyItemInserted場景

5.2.1 場景描述

假設在Item1下面插入兩條資料AddItem1,AddItem2

5.2.2 佈局過程

5.2.2.1 dispatchLayoutStep1階段

  1. 尋找錨點,找到Item1
  2. 移除螢幕上的Views,放入到mAttachedScrap中
  3. 錨點處從上往下填充
  4. 錨點處從下往上填充,由上圖可知,上面沒有空間了,不填充
  5. 判斷是否還有剩餘的空間,如果有在末尾填充,下面沒空間了,不填充
  6. 因為當前是預佈局階段,不填充

5.2.2.2 dispatchLayoutStep2階段

  1. 尋找錨點,找到Item1
  2. 移除螢幕上的Views,放入到mAttachedScrap中
  3. 錨點處從上往下填充,此時將變化後的資料填充到螢幕上,addItem1和addItem2被填充到item1下面
  4. 錨點處從下往上填充,由圖可知,沒有空間不填充
  5. 判斷是否還有剩餘的空間,由圖可知,沒有空間不填充
  6. 當前是layoutStep2階段,會將mAttachScrap的內容,填充到螢幕末尾,ViewHolder5和ViewHolder6對應的ItemView被填充

5.2.2.3 dispatchLayoutStep3階段

  1. Item2、Item3~Item6做移動動畫
  2. addItem1、addItem2做淡入動畫
  3. 動畫結束後Item5、Item6被回收到mCachedViews快取池中

5.3 場景總結

5.3.1 notifyItemRemoved場景

刪除場景

5.3.2 notifyItemInserted場景

增加場景

本文分享自微信公眾號 - 音視訊開發進階(glumes_blog)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。