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

前言

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

本文主要通過以下幾個方面來講解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源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章