老大爺都能看懂的RecyclerView動畫原理

如何閱讀本篇文章

本文主要講解RecyclerView Layout變化觸發動畫執行的原理。前半部分偏重原理和代碼的講解,後半部分經過圖文結合場景講解各個階段的執行過程。java

建議先粗略閱讀前半部分的原理和代碼篇,作到心中有概念,帶着理論知識去閱讀後半部分的場景篇。最後結合全文學到的知識,帶着問題去閱讀源碼,效果會更好。web

原理篇

1. Adapter的notify方法

用過RecyclerView的同窗大概都應該知道Adapter有幾個notify相關的方法,它們分別是:面試

  • notifyDataSetChanged()
  • notifyItemChanged(int)
  • notifyItemInserted(int)
  • notifyItemRemoved(int)
  • notifyItemRangeChanged(int, int)
  • notifyItemRangeInserted(int, int)
  • notifyItemRangeRemoved(int, int)
  • notifyItemMoved(int, int)

稍微有點開發經驗的同窗都知道,notifyDataSetChanged()方法比其它的幾個方法更重量級一點,它會致使整個列表刷新,其它幾個方法則不會。有更多開發經驗的同窗可能還知道notifyDataSetChanged()方法不會觸發RecyclerView的動畫機制,其它幾個方法則會觸發各類不一樣類型的動畫。算法

2. RecyclerView的佈局邏輯

2.1 RecyclerView的dispatchLayout

dispatchLayout顧名思義,固然是把子View佈局(添加並放置到合適的位置)到RecyclerView上面了。打開它的源碼咱們能夠看到這樣一段註釋。緩存

Wrapper around layoutChildren() that handles animating changes caused by layout.  Animations work on the assumption that there are five different kinds of items in play:微信

  1. PERSISTENT: items are visible before and after layoutapp

  2. REMOVED: items were visible before layout and were removed by the app框架

  3. ADDED: items did not exist before layout and were added by the app編輯器

  4. DISAPPEARING: items exist in the data set before/after, but changed from visible to non-visible in the process of layout (they were moved off screen as a side-effect of other changes)ide

  5. APPEARING: items exist in the data set before/after, but changed from non-visible to visible in the process of layout (they were moved on  screen as a side-effect of other changes)

從註釋咱們能夠知道。dispatchLayout方法不只有給子View佈局的功能,並且能夠處理動畫。動畫主要分爲五種:

  1. PERSISTENT:針對佈局前和佈局後都在手機界面上的View所作的動畫
  2. REMOVED:在佈局前對用戶可見,可是數據已經從數據源中刪除掉了
  3. ADDED:新增數據到數據源中,而且在佈局後對用戶可見
  4. DISAPPEARING:數據一直都存在於數據源中,可是佈局後從可見變成不可見狀態
  5. APPEARING:數據一直都存在於數據源中,可是佈局後從不可見變成可見狀態

到目前爲止,咱們還不能徹底理解這五種類型的動畫有什麼具體的區別,分別在什麼樣的場景下會觸發這些類型的動畫。可是給咱們提供了很好的研究思路。目前咱們只須要簡單瞭解有這五種動畫,接着往下,咱們這裏看下dispatchLayout的源碼,爲了響應文章標題,這裏貼出精簡過的源碼:

void dispatchLayout(){
  ...
  dispatchLayoutStep1();
  dispatchLayoutStep2();
  dispatchLayoutStep3();
  ...
}

關於dispatchLayoutStepX方法,相信不少人都據說或者瞭解過,文章後面我會作詳細的介紹,簡單介紹以下:

從dispatchLayout的註釋中,咱們注意到before和after兩個單詞,分別表示佈局前和佈局後。這麼說來那就簡單了。dispatchLayoutStep1對應的是before(佈局前),dispatchLayoutStep2的意思是佈局中,dispatchLayoutStep3對應的是after(佈局後)。它們的做用描述以下:

  1. dispatchLayoutStep1
    1. 判斷是否須要開啓動畫功能

    2. 若是開啓動畫,將當前屏幕上的Item相關信息保存起來供後續動畫使用

    3. 若是開啓動畫,調用mLayout.onLayoutChildren方法預佈局

    4. 預佈局後,與第二步保存的信息對比,將新出現的Item信息保存到Appeared中

精簡後的代碼以下:

private void dispatchLayoutStep1() {
  ...
  //第一步 判斷是否須要開啓動畫功能
  processAdapterUpdatesAndSetAnimationFlags();
  ...
  if (mState.mRunSimpleAnimations) {
    ...
    //第二步  將當前屏幕上的Item相關信息保存起來供後續動畫使用
    int count = mChildHelper.getChildCount();
    for (int i = 0; i < count; ++i) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
        final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
        mViewInfoStore.addToPreLayout(holder, animationInfo);
    }
    ...
    if (mState.mRunPredictiveAnimations) {
          saveOldPositions();
          //第三步 調用onLayoutChildren方法預佈局
          mLayout.onLayoutChildren(mRecycler, mState);
          mState.mStructureChanged = didStructureChange;

          for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
              final View child = mChildHelper.getChildAt(i);
              final ViewHolder viewHolder = getChildViewHolderInt(child);
              if (viewHolder.shouldIgnore()) {
                  continue;
              }
                        //第四步 預佈局後,對比預佈局先後,哪些item須要放入到Appeared中

              if (!mViewInfoStore.isInPreLayout(viewHolder)) {

                  if (wasHidden) {
                      recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                  } else {
                      mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                  }
              }
          }
          clearOldPositions();
      } else {
          clearOldPositions();
      }
  }

}
  1. dispatchLayoutStep2 根據數據源中的數據進行佈局,真正展現給用戶看的最終界面
private void dispatchLayoutStep2() {
    ...
    // Step 2: Run layout
    mState.mInPreLayout = false;//此處關閉預佈局模式
    mLayout.onLayoutChildren(mRecycler, mState);
    ...
}
  1. dispatchLayoutStep3 觸發動畫
private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        // Step 3: Find out where things are now, and process change animations.
        // traverse list in reverse because we may call animateChange in the loop which may
        // remove the target view holder.
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore()) {
                continue;
            }
            long key = getChangedHolderKey(holder);
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                // run a change animation
                ...
            } else {
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        // Step 4: Process view info lists and trigger animations
        //觸發動畫
        mViewInfoStore.process(mViewInfoProcessCallback);
    }

  ...
    }

從代碼咱們能夠看出dispatchLayoutStep1和dispatchLayoutStep2方法中調用了onLayoutChildren方法,而dispatchLayoutStep3沒有調用。

2.2 LinearLayoutManager的onLayoutChildren方法

以垂直方向的RecyclerView爲例子,咱們填充RecyclerView的方向有兩種,從上往下填充和從下往上填充。開始填充的位置不是固定的,能夠從RecyclerView的任意位置處開始填充。該方法的功能我精簡爲如下幾個步驟:

  1. 尋找填充的錨點(最終調用findReferenceChild方法)
  2. 移除屏幕上的Views(最終調用detachAndScrapAttachedViews方法)
  3. 從錨點處從上往下填充(調用fill和layoutChunk方法)
  4. 從錨點處從下往上填充(調用fill和layoutChunk方法)
  5. 若是還有多餘的空間,繼續填充(調用fill和layoutChunk方法)
  6. 非預佈局,將scrapList中多餘的ViewHolder填充(調用layoutForPredictiveAnimations)

本文只講解onLayoutChildren的主流程,具體的填充邏輯請參考RecyclerView填充邏輯一文

LinearLayoutManager#onLayoutChildren

  public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    //1. 尋找填充的錨點
    updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    
    ...
    //2. 移除屏幕上的Views
    detachAndScrapAttachedViews(recycler);
    
    ...
    //3. 從錨點處從上往下填充
    updateLayoutStateToFillEnd(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForEnd;
    fill(recycler, mLayoutState, state, false);
    
    ...
    //4. 從錨點處從下往上填充
    // fill towards start
    updateLayoutStateToFillStart(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForStart;
    mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
    fill(recycler, mLayoutState, state, false);
    
    ...
    //5. 若是還有多餘的空間,繼續填充
    if (mLayoutState.mAvailable > 0) {
        extraForEnd = mLayoutState.mAvailable;
        // start could not consume all it should. add more items towards end
        updateLayoutStateToFillEnd(lastElement, endOffset);
        mLayoutState.mExtraFillSpace = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
    }
  }
    ...
    //6. 非預佈局,將scrapList中多餘的ViewHolder填充
    layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
    ...

LinearLayoutManager#layoutForPredictiveAnimations

 private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,
            RecyclerView.State state, int startOffset,
            int endOffset) {
        //判斷是否知足條件,若是是預佈局直接返回
        if (!state.willRunPredictiveAnimations() ||  getChildCount() == 0 || state.isPreLayout()
                || !supportsPredictiveItemAnimations()) {
            return;
        }
        // 遍歷scrapList,步驟2中屏幕中被移除的View
        int scrapExtraStart = 0, scrapExtraEnd = 0;
        final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
        final int scrapSize = scrapList.size();
        final int firstChildPos = getPosition(getChildAt(0));
        for (int i = 0; i < scrapSize; i++) {
            RecyclerView.ViewHolder scrap = scrapList.get(i);
            //若是被remove掉了,跳過
            if (scrap.isRemoved()) {
                continue;
            }
            //計算額外的控件
                scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);

        }

        mLayoutState.mScrapList = scrapList;
        ...
        // 步驟6 繼續填充
        if (scrapExtraEnd > 0) {
            View anchor = getChildClosestToEnd();
            updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
            mLayoutState.mExtraFillSpace = scrapExtraEnd;
            mLayoutState.mAvailable = 0;
            mLayoutState.assignPositionFromScrapList();
            fill(recycler, mLayoutState, state, false);
        }
        mLayoutState.mScrapList = null;
    }

至此,佈局的邏輯已經講解完畢。關於具體的動畫執行邏輯,因爲篇幅有限。不在本文中講解

場景篇

1. notifyItemRemoved

咱們來測試從屏幕中刪除View,調用notifyItemRemoved相關的方法,dispatchLayout是如何從新佈局的。假設初始狀態以下圖,假設Adapter數據有100條,屏幕上有Item1~Item6 6個View,刪除Item1和Item2。

  1. 將Item1 Item2對應的ViewHolder設置爲REMOVE狀態
  2. 將全部的Item對應的ViewHolder的mPreLayoutPosition字段賦值爲當前的position

咱們回顧一下onLayoutChildren的幾個步驟

  1. 尋找填充的錨點(最終調用findReferenceChild方法)
  2. 移除屏幕上的Views(最終調用detachAndScrapAttachedViews方法)
  3. 從錨點處從上往下填充(調用fill和layoutChunk方法)
  4. 從錨點處從下往上填充(調用fill和layoutChunk方法)
  5. 若是還有多餘的空間,繼續填充(調用fill和layoutChunk方法)
  6. 非預佈局,將scrapList中多餘的ViewHolder填充(調用layoutForPredictiveAnimations)

1.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. 還有多餘的空間,繼續填充,把Item七、Item8填充到屏幕中

  6. 由於當前是預佈局,直接返回

至此step1的layout結束

1.2 dispatchLayoutStep2階段

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

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

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

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

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

注意此時已經佈局完成可是屏幕上部與第一個有GAP,會修復

 if (getChildCount() > 0) {
            // because layout from end may be changed by scroll to position
            // we re-calculate it.
            // find which side we should check for gaps.
            if (mShouldReverseLayout ^ mStackFromEnd) {
                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
                startOffset += fixOffset;
                endOffset += fixOffset;
                fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
                startOffset += fixOffset;
                endOffset += fixOffset;
            } else {
                int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
                startOffset += fixOffset;
                endOffset += fixOffset;
                fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
                startOffset += fixOffset;
                endOffset += fixOffset;
            }
        }

修復後效果以下

  1. 當前不是預佈局,可是由於ViewHolder1和ViewHolder2都是被Remove掉的,因此跳過

2. notifyItemInserted

假設在Item1下面插入兩條數據AddItem1,AddItem2

2.1 dispatchLayoutStep1階段

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

2.2 dispatchLayoutStep2階段

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

2.3 dispatchLayoutStep3階段

開始動畫,動畫結束後,item5和item6會被回收掉,此時會被回收到mCachedViews緩存池中

本篇不涉及到動畫具體如何執行,且聽下回分解吧。

往期文章

面試官:簡歷上最好不要寫Glide,不是問源碼那麼簡單

常見的鏈表翻轉,字節跳動加了個條件,面試者高呼「我太難了」| 圖解算法

面試官,怎樣實現 Router 框架?

java 版劍指offer集錦

面試官系列 - LeetCode鏈表知識點&題型總結

徐公自敘

得屌絲者得天下,米粉≠屌絲

Gson 和 Kotlin data class 的避坑指南


若是你以爲對你有所幫助的話,能夠關注個人公衆號 徐公碼字(stormjun94),第一時間會在上面更新




本文分享自微信公衆號 - 徐公碼字(stormjun94)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索