Android RecyclerView 區域性重新整理原理

lovejjfg發表於2019-04-11

前情回顧

Android RecycleView輕鬆實現下拉重新整理、載入更多

Android RecyclerView 定製單選多選模式

SwipeRefreshLayout 在 RecyclerView 空白時下拉失效分析

之前寫的 PowerAdapterSelectPowerAdapter 從建立到現在,已經兩年多,期間發生了翻天覆地的變化。一開始,我把 SwipeRefreshLayoutRecyclerView 直接耦合在一起,佈局就寫一個控制元件,爽。因為那會兒業務場景是那樣,沒有考慮靈活性。後來改業務頭部不能直接用 SwipeRefreshLayout ,突然才意識到這樣侷限性太大。於是乎 Android 打造專屬的下拉重新整理 載入更多 就這麼出現,的確也有實際的應用場景。甚至還狂熱地做了那會 UC 瀏覽器下拉重新整理的效果 。在這之後,我將頭部拆分開,PowerAdapter 什麼的就用於簡化實現載入更多已經多佈局填充。SelectPowerAdapter 繼承自 PowerAdapter ,寫的及其簡陋。用於實現簡單的單選和多選。

更新

倉庫請戳 PowerRecyclerView

這倆 Adapter 就是對於已有 Adapter 功能的裝飾,方便呼叫者實現一些常用功能。再這次之前,陸陸續續已經豐富了一些功能,比如說 SelectAdaper 中以前真是很簡陋,現在慢慢已經能實現 單選 多選 反選 選中刪除 限制最大選中數量 等基本功能。PowerAdapter 則將 增刪改查等基本功能完善。

因為沒有相關規範,一些方法那會兒很隨意。現在一些方法被廢除,比如說之前寫的 adapter.attachRecyclerView() ,為什麼要廢除呢,因為 adapter 直接就有 onAttachedToRecyclerView() 的方法,所以,根本就不需要新增這個方法。那會疏(cai)忽 (ji)和想當然就直接加上。

還有最重要就是加入了區域性重新整理這個好東西,一開始是沒有考慮到這個地方,後面看文件,才發現這麼好一功能差點兒被遺忘。怎麼理解區域性重新整理和使用,將是接下來文章的重點。

我們知道,RecyclerView 中已經新增了 notifyItemChange() notifyItemRemove() 等等單個條目更改的方法,大方向說,這個相對於 ListView 或者 notifyDataChange() 方法 , 它已經算是做到區域性重新整理。小方向再說,這些新增的重新整理方法,其實預設都是帶有動畫效果,具體效果是 DefaultItemAnimator 來控制和處理,就是因為動畫效果,讓開發時會出現一些意料之外的狀況。

假設我們現在有一個上傳照片的場景,我們每一個 ViewHolder 帶有一張圖片,然後上面有一個進度條,進度根據上傳進度實時返回。如果你使用 notifyItemChange() 來更新動畫的話,那麼會有兩個問題:第一,你會發現每重新整理一次,整個佈局都會閃動一下。第二,這個進度的數值我需要怎麼傳遞才好呢?在 ViewHolderBean 物件中新增一個 progress 臨時欄位?

針對上面兩個問題,我其實還額外想問一個問題,也算前置問題。如果我們多次呼叫 notifyItemChange() 方法,條目會重新整理多次嗎?

另外針對區域性重新整理,還有兩個問題,第一,notifyItemChange() 和 真正的區域性重新整理 同一個位置,ViewHolder 是同一個物件嗎?第二,區域性重新整理是沒有設定動畫效果嗎?

帶著這些問題,開始 RecyclerView 這一部分原始碼探索,接下來的所有原始碼基於 Android API 27 Platform

notifyItemChange() 第二個引數

上面說了半天 RecyclerView 真正的區域性重新整理,但是,到底怎麼就是區域性重新整理呢?其實很簡單,看看 notifyItemChange(int position) 另外一個過載函式。

    public final void notifyItemChanged(int position, @Nullable Object payload) {
        mObservable.notifyItemRangeChanged(position, 1, payload);
    }
複製程式碼

同時,在 Adapter 中,與 onBindViewHolder(@NonNull VH holder, int position) 相對應,也有一個過載函式。預設實現就走上面這個方法。

    public void onBindViewHolder(@NonNull VH holder, int position,
            @NonNull List<Object> payloads) {
        onBindViewHolder(holder, position);
    }
複製程式碼

好了,這就是 RecyclerView 區域性重新整理相關 API 的差異。其實對於第一個額外問題(如果我們多次呼叫 notifyItemChange() 方法,條目會重新整理多次嗎?),從上面兩個方法中,我們就能猜到一些答案,多次呼叫應該只會回撥重新整理一次,你看傳入的 payload 是一個 Object,但是到 onBindViewHolder() 方法時引數卻成了一個集合,那應該就是有合併的操作。另外再從效能上說,連著 notify 多次,就重新 measure layout 多次的話,這個開銷也是很大並且沒有必要(RecyclerView 嚴格控制 requestLayout() 方法呼叫),真沒必要。結果真是這樣嗎,直接看相關原始碼。

//RecyclerView
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
    // fallback to onItemRangeChanged(positionStart, itemCount) if app
    // does not override this method.
    onItemRangeChanged(positionStart, itemCount);
}

//RecyclerViewDataObserver
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    assertNotInLayoutOrScroll(null);
    if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
        triggerUpdateProcessor();
    }
}

// AdapterHelper 
void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
複製程式碼

在呼叫 notifyItemChange() 方法(不管一個引數還是兩個個引數)之後,最後都會走到 notifyItemRangeChanged(int positionStart, int itemCount,@Nullable Object payload) ,最後回撥到 RecyclerViewDataObserver. onItemRangeChanged() 方法。在該方法中,有一個 triggerUpdateProcessor() 方法,它本質上說,就是去請求重新佈局。那就是說,這裡只有 if 條件成立,才會去 requestLayout() ,接下來,搞清楚什麼時候 if 成立,就能回答這個前置問題。

/**
 * @return True if updates should be processed.
 */
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    if (itemCount < 1) {
        return false;
    }
    mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
    mExistingUpdateTypes |= UpdateOp.UPDATE;
    return mPendingUpdates.size() == 1;
}
複製程式碼

到這裡,兩個發現:第一,size==1 說明就第一次呼叫是才返回 true 才觸發 requestLayout() ;第二,payload 引數在這裡被包裝為物件,放入 mPendingUpdates 這個集合中。第一個發現,徹底證明上訴猜測是正確的,即使你呼叫 notify 多次,其實只有第一次會觸發 requestLayout()

逃不走的 measure layout

既然有 requestLayout() 呼叫,那麼就回到 onMeasure()onLayout() 這些方法中。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
        ...
    }
    ...
}
複製程式碼

假設我們 RecyclerView 佈局就是兩個 match_parent 或者有一個精確值,那麼執行的程式碼片段就是這樣。接著再看看 onLayout()

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    dispatchLayout();
    mFirstLayoutComplete = true;
}

void dispatchLayout() {
    ...
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to
        // changed size.
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}
複製程式碼

RecyclerViewonLayout() 方法中,一共執行三大步驟。從命名上已經能清楚看懂。對於三個 step ,每個方法上面都有詳細註釋。翻譯過來就是說,第一步時,處理 Adapter 更新,決定執行的動畫效果,儲存當前 Views 的資訊,最後,如果必要的話,預測佈局並儲存相關資訊;第二步時,根據最終狀態執行佈局,並且可能執行多次;第三步,儲存 View 資訊,執行動畫,最後做一些清除重置操作。

道理我都懂,但是還是過不好這一生。這是另外一個極簡翻譯

由於篇(neng)幅(li)有(bu)限(xing),接下來對 step 方法只做本文相關及區域性重新整理相關程式碼的解析(只關心上面提到的幾個問題),RecyclerView 程式碼太 TM 多了,一次是啃不完,搞不好一輩子可能也啃不完。

dispatchLayoutStep1

處理 Adapter 更新,決定執行的動畫效果,儲存當前 Views 的資訊,最後,如果必要的話,預測佈局並儲存相關資訊。

private void dispatchLayoutStep1() {
    mState.assertLayoutStep(State.STEP_START);
    startInterceptRequestLayout();
    //1.更新 mRunSimpleAnimations 和 mRunPredictiveAnimations flag 其實還有其他一些騷操作
    processAdapterUpdatesAndSetAnimationFlags();
    //2.mInPreLayout 設定為 true 後面有用
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    ...
    if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
                continue;
            }
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPreLayoutInformation(mState, holder,
                            ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                            holder.getUnmodifiedPayloads());
            //5.儲存動畫資訊相關
            mViewInfoStore.addToPreLayout(holder, animationInfo);
            if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                    && !holder.shouldIgnore() && !holder.isInvalid()) {
                //3.如果holder確定要更新,就把它新增到 oldChangeHolders 集合中
                long key = getChangedHolderKey(holder);
                mViewInfoStore.addToOldChangeHolders(key, holder);
            }
        }
    }
    if (mState.mRunPredictiveAnimations) {
        ...
        //4.很重要,LayoutManager 開始工作
        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;
            }
            if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                ...
                //5.儲存動畫資訊相關
                mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
            }
        }
        ...
    } ...
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    mState.mLayoutStep = State.STEP_LAYOUT;
}
複製程式碼

一共額外註釋五點,第一點,更新動畫相關標識位 mRunSimpleAnimationsmRunPredictiveAnimations,後面的操作都依賴它們。第二點,將 mInPreLayout 的狀態和 mRunPredictiveAnimations 同步。這個在後面的步驟中也需要使用。第三點,儲存需要更新的 ViewHolderoldChangeHolder 集合中。第四點,呼叫 LayoutManager. onLayoutChildren() 。第五點,儲存相關動畫資訊。

腦子不能亂,我們現在就關心三個問題,第一個, payload 引數怎麼傳遞到 ViewHolder 中;第二個,動畫效果是否和 payload 有關係;第三個 ViewHolder 重新整理時到底是不是同一個物件。

//RecyclerView
private void processAdapterUpdatesAndSetAnimationFlags() {
    ...
    // simple animations are a subset of advanced animations (which will cause a
    // pre-layout step)
    // If layout supports predictive animations, pre-process to decide if we want to run them
    ...
        mAdapterHelper.preProcess();
    ...
    boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
    // 通常情況就是 ture
    mState.mRunSimpleAnimations = mFirstLayoutComplete
            && mItemAnimator != null
            && (mDataSetHasChangedAfterLayout
            || animationTypeSupported
            || mLayout.mRequestedSimpleAnimations)
            && (!mDataSetHasChangedAfterLayout
            || mAdapter.hasStableIds());
    // 通常情況就是 ture
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
            && animationTypeSupported
            && !mDataSetHasChangedAfterLayout
            && predictiveItemAnimationsEnabled();
}

//AdapterHelper
void preProcess() {
    mOpReorderer.reorderOps(mPendingUpdates);
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            ...
            case UpdateOp.UPDATE:
                applyUpdate(op);
                break;
        }
       ...
    }
    mPendingUpdates.clear();
}
 //AdapterHelper
private void postponeAndUpdateViewHolders(UpdateOp op) {
    if (DEBUG) {
        Log.d(TAG, "postponing " + op);
    }
    mPostponedList.add(op);
    switch (op.cmd) {
        ...
        case UpdateOp.UPDATE:
            mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
            break;
        default:
            throw new IllegalArgumentException("Unknown update op type for " + op);
    }
}
複製程式碼

上面幾個方法,涉及到 RecyclerViewAdapter 互動,首先執行 mAdapterHelper.preProcess() 後,會將剛剛上文說到的onItemRangeChanged() 方法中的 payload 包裝成 UpdateOp 物件,到這裡,要開始處理這個物件。

    UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
        this.cmd = cmd;
        this.positionStart = positionStart;
        this.itemCount = itemCount;
        this.payload = payload;
    }
複製程式碼

cmd 對應我們的操作,這裡就是 update,後面就是 notifyItemRangeChange() 方法中對應的引數。AdapterHelper 最後會使用 callback 回撥到 RecyclerView 中,在 RecyclerView 中執行 viewRangeUpdate() 方法。這個 callbackRecyclerView 在建立時就已經設定。

//RecyclerView 初始化是呼叫
void initAdapterManager() {
    mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
        ....
        @Override
        public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
            viewRangeUpdate(positionStart, itemCount, payload);
            mItemsChanged = true;
        }
    });
}

//RecyclerView
void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    final int positionEnd = positionStart + itemCount;

    for (int i = 0; i < childCount; i++) {
        final View child = mChildHelper.getUnfilteredChildAt(i);
        final ViewHolder holder = getChildViewHolderInt(child);
        if (holder == null || holder.shouldIgnore()) {
            continue;
        }
        if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
            // 很重要,在這裡更新了 Flag 然後將 payload 傳遞到 Viewholder 中
            holder.addFlags(ViewHolder.FLAG_UPDATE);
            holder.addChangePayload(payload);
            // lp cannot be null since we get ViewHolder from it.
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
        }
    }
    mRecycler.viewRangeUpdate(positionStart, itemCount);
}
複製程式碼

到這裡,我們可以看到,在 viewRangeUpdate() 方法中,** holder 加上 FLAG_UPDATE 標識,請先記住,這個標識很重要。然後,關鍵問題之一來了,payload 通過 addChangePayload() 方法直接加到對應 holder 中。**一個核心問題得到解決。

接著回到 processAdapterUpdatesAndSetAnimationFlags() 後部分,設定 mRunSimpleAnimationsmRunPredictiveAnimations 兩個標識為 true。再返回 dispatchLayoutStep1() 方法中第三點。

        if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                && !holder.shouldIgnore() && !holder.isInvalid()) {
            //3.如果holder確定要更新,就把它新增到 oldChangeHolders 集合中
            long key = getChangedHolderKey(holder);
            mViewInfoStore.addToOldChangeHolders(key, holder);
        }
複製程式碼

上面說了,** holder 如果需要被更新,那麼 FLAG_UPDATE 就會被新增,然後 holder.isUpdated() 方法就會返回 true 。所以第三點條件符合,就會執行。**

接著是第四點:

if (mState.mRunPredictiveAnimations) {
    ...
    //4.很重要,LayoutManager 開始工作
    mLayout.onLayoutChildren(mRecycler, mState);
複製程式碼

在 LayoutManger 的 onLayoutChildren() 中有大量程式碼,這裡就看我們關心的兩行程式碼,其實就是兩個方法,detachAndScrapAttachedViews() 和 fill() 方法。

//LinearLayoutManger
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
     detachAndScrapAttachedViews(recycler);
     fill(recycler, mLayoutState, state, false);
    ...
}
複製程式碼

detachAndScrapAttachedViews() 這個方法最後會反向遍歷所有 View 依次呼叫 RecyclerscrapView() 方法。關於 Recycler ,可以說是RecyclerView 的核心之一,單獨開一篇文章講快取複用 Recycler 機制都不過分,這裡我們關心區域性重新整理相關就好。

    void scrapView(View view) {
        final ViewHolder holder = getChildViewHolderInt(view);
        // 如果不需要更新 放到 mAttachedScrap 中
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
            ...
            holder.setScrapContainer(this, false);
            mAttachedScrap.add(holder);
        } else {
             // 需要更新 放到 mChangedScrap 中
            if (mChangedScrap == null) {
                mChangedScrap = new ArrayList<ViewHolder>();
            }
            holder.setScrapContainer(this, true);
            mChangedScrap.add(holder);
        }
    }
複製程式碼

這個方法中,也要注意,如果不需要更新,會加到 mAttachedScrap 全家桶中,需要更新的,就會放到 mChangedScrap。為什麼要加入到這些集合中呢,因為後面 fill() 的時候會通過這些集合去找對應的 holder,生產對應的 View 最後真正新增到 RecyclerView 控制元件中。

fill() 方法中,最終會呼叫 tryGetViewHolderForPositionByDeadline() 方法找到 ViewHolder,拿到對應 View,然後 addView()

//Recycler
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ...
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 在 dispatchLayoutStep1 中 設定為   mState.mInPreLayout = mState.mRunPredictiveAnimations
    // 0. 從 mChangedScrap 集合中尋找
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1. Find by position from scrap/hidden list/cache
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        ...
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        ...
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2. Find from scrap/cache via stable ids, if exists
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        // 3. 從 mViewCacheExtension 查詢,mViewCacheExtension 預設為 null 
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                ...
            }
        }
        if (holder == null) { // fallback to pool
            ...
            //4. 從 RecycledViewPool 中查詢
            holder = getRecycledViewPool().getRecycledView(type);
            ...
        }
        if (holder == null) {
            ...
            //5. 老實建立
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
複製程式碼

holder 的查詢是一個漫長過程,注意這裡第 0 步,只有 isPreLayout()true 才會從 mChangedScrap 集合中查詢 ViewHolder ,在 dispatchLayoutStep1() 中,mState.mInPreLayout = mState.mRunPredictiveAnimations 預設會設定為 true ,所以會執行到這裡。第一步從 mAttachedScrap mHiddenViews mCachedViews 這些集合中查詢;第二步通過 獨立 id 再次查詢;第三步,可能的話,通過 mViewCacheExtension 擴充查詢,這個可以通過 RecyclerView 設定;第四部,從 RecycledViewPool 中查詢;最後,通過 adapter 建立。

到這裡,dispatchLayoutStep1() 方法差不多結束。

dispatchLayoutStep2

根據最終狀態執行佈局,並且可能執行多次。

private void dispatchLayoutStep2() {
    ...
    // 注意,這裡 mInPreLayout 設定為 false 
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);

    mState.mStructureChanged = false;
    mPendingSavedState = null;
    ...
}
複製程式碼

相比第一步預備,第二步其實來得簡單得多,核心就是將 mInPreLayout 設定為 false 然後重新呼叫 LayoutManageronLayoutChildren() 方法。過程就如上分析,但是,因為這裡 mInPreLayout 欄位為 false而我們之前修改的 ViewHolder 是被新增到 mChangedScrap 集合中,但是因為 mInPreLayoutfalse, 它不會再去 mChangedScrap 查詢ViewHoldertryGetViewHolderForPositionByDeadline() 方法中第 0 步操作將不在執行,所以 舊的 ViewHolder 無法被複用,它會從下面步驟中獲取ViewHolder 返回。也就是說,當我們通常使用 notifyItemChangge(pisition) 一個引數的方法之後,重新整理時它使用的不是同一個 ViewHolder

dispatchLayoutStep3

儲存 View 資訊,執行動畫效果,最後做一些清除操作。

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));
            ...
            long key = getChangedHolderKey(holder);
            //獲取當前 holder 動畫資訊
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
            
            //獲取 olderViewHolder
            ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
            if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                        oldChangeViewHolder);
                final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                //如果新舊一樣,都是消失,那就直接執行
                if (oldDisappearing && oldChangeViewHolder == holder) {
                    // run disappear animation instead of change
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                } else {
                    // 獲取之前儲存的資訊
                    final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                            oldChangeViewHolder);
                    // 這裡一存一取 完成資訊覆蓋
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                    ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);

                    if (preInfo == null) {
                        handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
                    } else {
                        //新增執行動畫效果
                        animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
                                oldDisappearing, newDisappearing);
                    }
                }
            } else {
                mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
        }

        // 重點,真正開始執行新增的動畫效果
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    //回收 和 重置
   ...
}
複製程式碼

dispatchLayoutStep1() 中,我們儲存了一些資訊,現在終於要派上用場。關於動畫相關邏輯,已經新增相關注釋。最後需要注意,如果需要執行動畫,將會執行 animateChange() 方法,該方法會完成動畫相關的建立,並不會直接執行,而是到最後 mViewInfoStore.process(mViewInfoProcessCallback) 呼叫,才開始真正的獲取相關動畫資訊,並執行。

// RecyclerView 
private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo,
        boolean oldHolderDisappearing, boolean newHolderDisappearing) {
    oldHolder.setIsRecyclable(false);
    if (oldHolderDisappearing) {
        addAnimatingView(oldHolder);
    }
    if (oldHolder != newHolder) {
        if (newHolderDisappearing) {
            addAnimatingView(newHolder);
        }
        // 這裡很有意思,有點兒像繞口令,這種設定是為了後面做相關釋放
        oldHolder.mShadowedHolder = newHolder;
        addAnimatingView(oldHolder);
        mRecycler.unscrapView(oldHolder);
        newHolder.setIsRecyclable(false);
        newHolder.mShadowingHolder = oldHolder;
    }
    if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) {
        // 到這裡真正執行相關動畫
        postAnimationRunner();
    }
}


// DefaultItemAnimator
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
        int fromX, int fromY, int toX, int toY) {
    if (oldHolder == newHolder) {
        // 如果新舊 holder 相同時,就新增一個 move 動畫效果 
        // 如果偏移量為 0 直接返回 false 不執行動畫效果
        return animateMove(oldHolder, fromX, fromY, toX, toY);
    }
    // 如果新舊不一樣 那麼就需要 計算出偏移量,然後建立一個 ChangeInfo 
    final float prevTranslationX = oldHolder.itemView.getTranslationX();
    final float prevTranslationY = oldHolder.itemView.getTranslationY();
    final float prevAlpha = oldHolder.itemView.getAlpha();
    resetAnimation(oldHolder);
    int deltaX = (int) (toX - fromX - prevTranslationX);
    int deltaY = (int) (toY - fromY - prevTranslationY);
    // recover prev translation state after ending animation
    oldHolder.itemView.setTranslationX(prevTranslationX);
    oldHolder.itemView.setTranslationY(prevTranslationY);
    oldHolder.itemView.setAlpha(prevAlpha);
    if (newHolder != null) {
        // carry over translation values
        resetAnimation(newHolder);
        newHolder.itemView.setTranslationX(-deltaX);
        newHolder.itemView.setTranslationY(-deltaY);
        newHolder.itemView.setAlpha(0);
    }
    // 新增到 動畫 集合中,等待接下來真正執行
    mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
    return true;
}

@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
        int toX, int toY) {
    final View view = holder.itemView;
    fromX += (int) holder.itemView.getTranslationX();
    fromY += (int) holder.itemView.getTranslationY();
    resetAnimation(holder);
    int deltaX = toX - fromX;
    int deltaY = toY - fromY;
    // 如果平移偏移量都是 0 那麼就不執行動畫效果
    if (deltaX == 0 && deltaY == 0) {
        dispatchMoveFinished(holder);
        //false 不會觸發動畫效果
        return false;
    }
    if (deltaX != 0) {
        view.setTranslationX(-deltaX);
    }
    if (deltaY != 0) {
        view.setTranslationY(-deltaY);
    }
    // 新增動畫效果 等待執行
    mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
    return true;
}
複製程式碼

上面三個方法,分別是具體的動畫新增過程,其中有些注意點,如果 oldHoldernewHolder 相等,並且相關偏移量為零,那麼不會新增和執行相關動畫效果

如果有相關動畫效果,會建立加入到 DefaultItemAnimator 的集合中,然後 mItemAnimator.animateChange() 方法返回 true ,最後呼叫 postAnimationRunner() 方法執行。

postAnimationRunner() 方法的 Runnable 中最後會呼叫 mItemAnimator.runPendingAnimations() ,最後將會執行到 animateChangeImpl() 方法。

//DefaultItemAnimator 
void animateChangeImpl(final ChangeInfo changeInfo) {
    final ViewHolder holder = changeInfo.oldHolder;
    final View view = holder == null ? null : holder.itemView;
    final ViewHolder newHolder = changeInfo.newHolder;
    final View newView = newHolder != null ? newHolder.itemView : null;
    //舊View存在的話,執行相關動畫
    if (view != null) {
        final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
                getChangeDuration());
        mChangeAnimations.add(changeInfo.oldHolder);
        oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
        oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
        oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animator) {
                dispatchChangeStarting(changeInfo.oldHolder, true);
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                //釋放
                oldViewAnim.setListener(null);
                view.setAlpha(1);
                view.setTranslationX(0);
                view.setTranslationY(0);
                 //回撥 RecyclerView 
                dispatchChangeFinished(changeInfo.oldHolder, true);
                mChangeAnimations.remove(changeInfo.oldHolder);
                dispatchFinishedWhenDone();
            }
        }).start();
    }
    //新 View 執行相關動畫
    if (newView != null) {
        final ViewPropertyAnimator newViewAnimation = newView.animate();
        mChangeAnimations.add(changeInfo.newHolder);
        newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
                .alpha(1).setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchChangeStarting(changeInfo.newHolder, false);
                    }
                    @Override
                    public void onAnimationEnd(Animator animator) {
                        //回收釋放
                        newViewAnimation.setListener(null);
                        newView.setAlpha(1);
                        newView.setTranslationX(0);
                        newView.setTranslationY(0);
                        //回撥 RecyclerView 
                        dispatchChangeFinished(changeInfo.newHolder, false);
                        mChangeAnimations.remove(changeInfo.newHolder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
    }
}

// RecyclerView inner 
private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {

    ItemAnimatorRestoreListener() {
    }

    @Override
    public void onAnimationFinished(ViewHolder item) {
        item.setIsRecyclable(true);
        //前面 ViewHolder 繞口令式設定,在這裡做最後釋放
        if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh
            item.mShadowedHolder = null;
        }
        // always null this because an OldViewHolder can never become NewViewHolder w/o being
        // recycled.
        item.mShadowingHolder = null;
        if (!item.shouldBeKeptAsChild()) {
            if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {
                removeDetachedView(item.itemView, false);
            }
        }
    }
} 
複製程式碼

animateChangeImpl() 方法中,分別為 oldViewnewView 執行相關動畫,最後回撥到 RecyclerView 中的 onAnimationFinished() 方法中,完成對 ViewHolder 之前動畫相關聯 holder 的釋放。

到這裡,第三步分析基本完成。再回頭看之前提出的區域性重新整理的兩個問題:

第一,notifyItemChange() 和 區域性重新整理 ViewHolder 是同一個物件嗎?第二,區域性重新整理是沒有設定動畫效果嗎?

基於上面第三步分析的結論,如果 oldHoldernewHolder 相等,並且偏移量為零,那麼不會新增和執行相關動畫效果,再結合實際情況,我們不妨大膽猜測第一個問題的答案,它們使用的是同一個 ViewHolder 物件。接著針對第二個問題,如果使用同一個物件,並且偏移值為 0 ,那麼就不會執行相關動畫效果。

但是,這個結論似乎和我們分析第二步時得出的結論有出入:而我們之前修改的 ViewHolder 是被新增到 mChangedScrap 集合中,但是因為 mInPreLayout 此時設定為 false, 它不會再去 mChangedScrap 查詢ViewHoldertryGetViewHolderForPositionByDeadline() 方法中第 0 步操作,所以 舊的 ViewHolder 無法被複用,它會從下面步驟中獲取 ViewHolder 返回。也就是說,當我們通常使用 notifyItemChangge() 一個引數的方法之後,它使用的不是同一個 ViewHolder

到底是哪裡錯了呢?其實都沒錯,上面說的是使用 notifyItemChangge(position) 一個引數的方法時的情況,完全正確。區域性重新整理時,我們使用的可是兩個引數的方法。

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    // 如果不需要更新 放到 mAttachedScrap 中
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
        // 沒有更新 或者 可以被複用
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        ...
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
         // 需要更新 放到 mChangedScrap 中
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}
複製程式碼

針對第二步中,呼叫的 scrapView() 方法我們再看一次,我們可以看到,在 if 判斷中,如果 holder 確實需要被更新,那麼它也可能被新增到 mAttachedScrap 集合中,只要 anReuseUpdatedViewHolder(holder) 這個方法能返回 true

//RecyclerView
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
    return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
            viewHolder.getUnmodifiedPayloads());
}
複製程式碼

RecyclerView 最後會呼叫 ItemAnimator 中的 canReuseUpdatedViewHolder() 方法,我們看看具體實現:

@Override
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
        @NonNull List<Object> payloads) {
    return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
複製程式碼

所噶,當我們使用區域性重新整理,payload 不為空 ,這個時候,如果 ViewHolder 需要更新,它的 更新標識 的確會被加入,但是同時canReuseUpdatedViewHolder() 也會返回 true ,所以,這個時候 ViewHolder 不會被新增到 mChangedScrap 集合中,而是加入 mAttachedScrap 集合中,真是程式設計師的小巧思

所以,當你使用區域性重新整理時,前後都是同一個 ViewHolder ,如果位置沒有變化,就不會執行動畫效果;而當你不使用區域性重新整理時,使用的不是同一個 ViewHolder ,不管位置是否變化,都會執行相關動畫,所以你看到的 itemView 會閃爍一下。當我們多次呼叫 notifyItemChange() 方法時,也不會多次觸發 requestLayout() 和回撥 bindViewHolder()

到此,上面提到的疑問都解決,至於提到那個場景,就可以使用 區域性重新整理 來處理,首先不會再有閃爍,那是在執行動畫,其次,是那個傳值問題,完全就可以使用 payload 引數來傳遞。每次拿取出 payloads 集合中最後一個值(最新的進度),然後更新到進度條,這個設計也算得上程式設計師的小巧思嘛(狗頭)。

相關文章