Android RecyclerView 局部刷新原理

前情回顧

Android RecycleView輕鬆實現下拉刷新、加載更多git

Android RecyclerView 定製單選多選模式程序員

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

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

更新

倉庫請戳 PowerRecyclerView緩存

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

由於沒有相關規範,一些方法那會兒很隨意。如今一些方法被廢除,好比說以前寫的 adapter.attachRecyclerView() ,爲何要廢除呢,由於 adapter 直接就有 onAttachedToRecyclerView() 的方法,因此,根本就不須要添加這個方法。那會疏(cai)忽 (ji)和想固然就直接加上。app

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

咱們知道,RecyclerView 中已經添加了 notifyItemChange() notifyItemRemove() 等等單個條目更改的方法,大方向說,這個相對於 ListView 或者 notifyDataChange() 方法 , 它已經算是作到局部刷新。小方向再說,這些添加的刷新方法,其實默認都是帶有動畫效果,具體效果是 DefaultItemAnimator 來控制和處理,就是由於動畫效果,讓開發時會出現一些意料以外的情況。函數

假設咱們如今有一個上傳照片的場景,咱們每個 ViewHolder 帶有一張圖片,而後上面有一個進度條,進度根據上傳進度實時返回。若是你使用 notifyItemChange() 來更新動畫的話,那麼會有兩個問題:第一,你會發現每刷新一次,整個佈局都會閃動一下。第二,這個進度的數值我須要怎麼傳遞纔好呢?在 ViewHolderBean 對象中添加一個 progress 臨時字段?oop

針對上面兩個問題,我其實還額外想問一個問題,也算前置問題。若是咱們屢次調用 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 集合中最後一個值(最新的進度),而後更新到進度條,這個設計也算得上程序員的小巧思嘛(狗頭)。

相關文章
相關標籤/搜索