1. 程式人生 > 實用技巧 >結合例項講解RecyclerView的佈局過程

結合例項講解RecyclerView的佈局過程

本文主要結合例項講解以下幾個方面:

  • 預佈局&預測動畫
  • Recycler 的快取的複用過程
  • RecyclerView的佈局過程
    • Step1 預佈局
    • Step2 佈局
    • Step3 執行動畫
  • Q&A

起因:

在試用重構後的播放器時,因為VideoView響應了 onVideoViewAttachToWindow 、 onVideoViewDetachFromWindow、onWindowVisibilityChanged 等事件,發現在點選VideoFeed時,傳送PayLoad 為 FULL_SCREEN等事件的notifyItemChanged時,RecyclerView會先新增一個View,然後隨之刪去,雖然不影響頁面的展示,但是卻出發了VideoView裡面的邏輯,造成了一些不可預期的結果。

預佈局和預測動畫

使用者有A、B、C三個item,A、B正顯示在螢幕中,這時,使用者把B刪除了,最終C會顯示在原先B的位置。

僅僅知道原狀態和最終狀態是無法執行合理的動畫的。因為知道C的最終位置,但是不知道C的起始位置。

針對這個問題,谷歌給出的答案是,通過預佈局把C先 預佈局 出來,然後再觸發動畫。

預佈局:

第一次是預佈局,將之前原狀態下的item都佈局出來。並且根據Adapter的notify資訊,知道哪些item即將變化,所以可以加載出另外的View來備用(可能有多個)。上述例子中,因為知道B已經被刪除(寬高變化也可觸發),所以把螢幕之外的C也預載入進來。

預佈局:

普通動畫:

https://tylerliu.top/2020/05/15/Android-RecyclerView的快取機制/171d8db8bf70b9ce

預測動畫:

https://tylerliu.top/2020/05/15/Android-RecyclerView的快取機制/171d8e01b6b0ba6c

Recycler 的四級快取的複用過程

從adapter的notify到RecyclerView的 layout

從略~

第一步: RecyclerView.dispatchLayoutStep1()


LayoutManage開始佈局的時候(預佈局或者是最終佈局),當前佈局中的所有view,都會被dump到scrap中(具體可見LinearLayoutManage#onLayoutChildren

中呼叫的了detachAndScrap),然後LayoutManager挨個取回view,除非view發生了什麼變化,否則它會馬上從scrap中回到原來的位置。

為什麼LayoutManager需要先執行detach,然後再重新attach這些view,而不是隻移除那些變化的子view呢?Scrap快取列表的存在,是為了隔離LayoutManagerRecyclerView.Recycler之間的關注點/職責。LayoutManager不需要知道哪些子view需要保留或者被回收到RecyclerViewPool或者其他地方。這是Recycler的職責。

除了在佈局時不為空之外,還有另外一個與scrap有關的規律:所有scrap的view都會跟RecyclerView分離。ViewGroup中的attachViewdetachView方法跟addViewremoveView很像,但是不會觸發請求佈局重繪的事件。它們只是從ViewGroup的子view列表中刪除對應的子view。,並將該子view的parent設定為null。detached狀態必須是臨時的,後面緊隨著attachremove事件。

下面我們看下在預佈局時候新增View的兩種情況:

第一種情況:

  1. 先從 mAttachedScrap 快取中移除。再呼叫mChildHelper的 attachViewToParent() 方法,進而通過其 mCallback 呼叫到 RecyclerView的 attachViewToParent 方法。正如上文所述,該方法不會觸發子View 的 onAttachedToWindow() 和 RecyclerView 中註冊的 OnChildAttachStateChangeListener 中的方法。

第二種情況:

layout 時載入 額外的一個View到RecyclerView中。

此時,因為該ViewHolder為通過 onCreateViewHolder新生成的,所以呼叫了ChildHelper 的 addView() 方法,該方法最終會呼叫到RecyclerView中的 dispatchChildAttached() 方法,使OnChildAttachStateChangeListener 進行相應。子View的 onAttachedToWindow() 也會被呼叫。

佈局第二步 dispatchLayoutStep2()


主要也是呼叫 mLayout.onLayoutChildren(mRecycler, mState);

因為在第一步中,我們已經在RecyclerView中多加入了一個Item,所以mAttachedScrap 共蒐集到 0、1、2、3、4、5、6 七個,比原來的多一個 0,1,2,3,4,5,6。

A

B

C

D ABCD B (notify 1) prelayout

A

B '

C

D

佈局和第一步類似,在fill()中 ,但是改變的第0個ViewHolder 現在的 layoutChunkResult.mIgnoreConsumed 也是false了,所以也需要消耗剩餘空間了。

所以 fill()方法執行完成後, 最後一個(即第 6 個)依然存在於 mAttachedScrap 中。

最後執行到 layoutForPredictiveAnimations 方法,其中會取出其中還未使用完的 mAttachedScrap

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);
            if (scrap.isRemoved()) {
                continue;
            }
            final int position = scrap.getLayoutPosition();
            final int direction = position < firstChildPos != mShouldReverseLayout
                    ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
//為scrap新增 Extra空間
            if (direction == LayoutState.LAYOUT_START) {
                scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
            } else {
                scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
            }
        }
mLayoutState.mScrapList = scrapList;

if (scrapExtraEnd > 0) {
            View anchor = getChildClosestToEnd();
            updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
            mLayoutState.mExtraFillSpace = scrapExtraEnd;
            mLayoutState.mAvailable = 0;
            mLayoutState.assignPositionFromScrapList();
//續命後再次fill
            fill(recycler, mLayoutState, state, false);
        }

再次進入到 layoutChunk 時,因為 layoutState.mScrapList 不為 null,此時將 呼叫 addDisappearingView(view);

佈局第三步dispatchLayoutStep3()

A

B '

C

D


根據 mChildHelper 中記錄的數量, 遍歷子獲取holder,統計需要執行的動畫。

統計完成後,執行動畫:

mViewInfoStore.process(mViewInfoProcessCallback);

遍歷 mLayoutHolderMap,然後根據 record的Flag,執行相應的動畫。

本例項中,因為addDisappearingView(view) 新增View時, 會將record的Flag 加入 FLAG_APPEAR_AND_DISAPPEAR ,所以會據此執行callback.unused(viewHolder); 之後通過ChildHelper的 removeView方法 ,刪除上一步加入的待刪除 View。然後會呼叫 RecyclerView的 dispatchChildDetached方法,通知 mOnChildAttachStateListeners 響應。子View的 onAttachedToWindow()也將響應。

Questions

Q:為什麼Update 也需要多新增一個View?

A:因為update也有可能隱藏部分佈局,導致漏出下一條內容。

Q:在什麼時候判斷,新加入的View是該保留還是該刪除?

A:在第二步中,fill時,每新增一個新的View,會判斷 他的高度,高度變化(或者甚至是其中一個唄刪除了),會計算消耗高度 layoutChunkResult.mConsumed ,如果空出了空間 ,則會正常呼叫 addView 而不是 addDisappearingView(view); 這樣,在第三步執行動畫時,也不會 走到 unused() 方法中。但是,如果,在fill時,如果前面的所有View高度都沒有發生變化,還是會走到 addDisappearingView(view); 中。

所以,可以說,最後需要被刪除,在第二步時已經 註定了。

Q:既然第二步已經決定最後這個View會被刪除,那麼直接不新增不就行了嗎?

A:新增是在第一步預佈局時候加入進去的,所以第二步 真正佈局 時只能進行標記,讓他在第三步中刪除掉。

Q:既然第二步中,可以得到變化後的 layoutChunkResult.mConsumed 為變化後的值: newHeight,那麼為什麼第一步時候,為何還要在layoutChunk時:

if (params.isItemRemoved() || params.isItemChanged()) {
                result.mIgnoreConsumed = true;
            }

然後再fill方法中根據此值忽略掉該View所耗費高度(其實第一步計算時高度為變化前高度),而是直接無腦預先多加入一個View呢? 這要看 RecyclerView的快取相關的邏輯了:

獲取Holder:

結論:第一步為 preload 階段,該階段認為沒有必要對 bind過得 View 重新執行 tryBindViewHolderByDeadline 和業務中 onBindViewHolder 方法的呼叫。所以,預佈局階段,一切都是老佈局時的View和屬性。

Q:那麼這一切 和 LinearLayoutManager 中 的 supportsPredictiveItemAnimations() 有什麼關係呢?

A:簡單的說,就是 如果 這個地方返回 false,則 在第一步dispatchLayoutStep1() 中就不會呼叫 LinearLayoutManager 的 onLayoutChildren() 方法了。

mState.mRunPredictiveAnimations 決定 dispatchLayoutStep1() 中是否進行預佈局,而該值在第一步開始時即在 processAdapterUpdatesAndSetAnimationFlags() 方法中賦值如下:

mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                && animationTypeSupported
                && !mDataSetHasChangedAfterLayout
                && predictiveItemAnimationsEnabled();

private boolean predictiveItemAnimationsEnabled() {
        return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
    }

所以說,整體結論是:

第一步佈局時,如果預佈局開關開啟,如果有Item發生了變化或刪除,不管三七二十一,先多佈局一個再說。

然後第二步進行佈局時,根據重新Bind後的ViewHolder和其中的View的寬高,進行真正的佈局。

第三步,佈局完成後,按需執行動畫,該刪除的刪除,該移動的移動。