Android - RecyclerView進階(3)—ItemAnimator分析及自定義

我的CSDN: ListerCi
我的簡書: 東方未曦

俗話說,好看的皮囊千篇一律,有趣的靈魂萬里挑一。但是對於我們這些俗人來說,肯定是選擇好看的皮囊,咱們的用戶也是如此。你看看應用市場上那些花枝招展的APP,哪個不是用上了五花八門的動畫效果,就算你的內在安全省電性能好,沒點兒花招可留不住花心的用戶。所以我們今天就來看看怎麼實現讓用戶眼前一亮的動畫,當然原理也很重要,因此源碼分析必不可少,本文的源碼分析主要聚焦於動畫是怎麼觸發的,以及動畫是怎麼實現的。

一、動畫的觸發與實現

當Adapter中的數據發生變化時,我們通過notifyItemXXX()等方法通知RecyclerView來改變數據的展示,這個過程必然伴隨新的layout()。如果在layout()後直接顯示新數據,效果比較僵硬,因此需要通過動畫來製造良好的用戶體驗。
那麼,爲了實現動畫,RecyclerView又額外做了哪些工作呢?抽象上來講,RecyclerView實現動畫的步驟如下。
① 數據發生改變時,保存當前的item信息爲preInfo
② 根據新的數據Layout
③ Layout完畢,保存當前的item信息爲postInfo
④ 根據preInfo和postInfo判斷動畫類型並交給ItemAnimator執行
可以發現,前3步保存了執行動畫所需要的信息,最後整體交給ItemAnimator來執行動畫。前3步涉及到內容較爲複雜,我們先從簡單的開始分析,來看ItemAnimator是怎麼實現動畫的。

1.1 動畫的實現

由於RecyclerView設計時的低耦合性,ItemAnimator只需要關注怎麼執行動畫即可,其邏輯並不複雜。RecyclerView爲我們實現了DefaultItemAnimator,在不設置動畫的情況下默認使用它,其中實現了4個針對item的動畫,分別爲Remove、Move、Add和Change。以Remove動畫爲例,當一個item被移出RecyclerView時,DefaultItemAnimator中的animateRemove(holder)方法就會被調用,但是並沒有馬上開始執行動畫,而是將動畫添加到了mPendingRemovals中,這是一個待執行的Romove動畫List。

    @Override
    public boolean animateRemove(final RecyclerView.ViewHolder holder) {
        resetAnimation(holder);
        mPendingRemovals.add(holder);
        return true;
    }

看一下DefaultItemAnimator的成員變量,原來有4個List分別存儲4種動畫。

private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();

當RecyclerView要執行動畫時,ItemAnimator的runPendingAnimations()方法會被調用,DefaultItemAnimator重寫後的方法如下,爲了便於閱讀,添加了註釋並省略了部分代碼。

    @Override
    public void runPendingAnimations() {
        boolean removalsPending = !mPendingRemovals.isEmpty();
        boolean movesPending = !mPendingMoves.isEmpty();
        boolean changesPending = !mPendingChanges.isEmpty();
        boolean additionsPending = !mPendingAdditions.isEmpty();
        // 判斷是否有動畫需要執行
        if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
            return;
        }
        // 執行Remove動畫
        for (RecyclerView.ViewHolder holder : mPendingRemovals) {
            animateRemoveImpl(holder); // 實際執行Remove動畫的方法
        }
        mPendingRemovals.clear();
        // Move動畫
        if (movesPending) {
            // 注意: Move動畫並不是馬上執行,會放入一個Runnable
            Runnable mover = new Runnable() {
                @Override
                public void run() {
                    for (MoveInfo moveInfo : moves) {
                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
                                moveInfo.toX, moveInfo.toY);
                    }
                }
            };
            // 如果有Remove動畫,就在Remove動畫結束之後執行Move動畫
            // 如果沒有Remove動畫就馬上執行
            if (removalsPending) {
                View view = moves.get(0).holder.itemView;
                ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
            } else {
                mover.run();
            }
        }
        // Change動畫,與Move動畫一起執行
        if (changesPending) {
            // ......
            Runnable changer = new Runnable() {
                @Override
                public void run() {
                    for (ChangeInfo change : changes) {
                        animateChangeImpl(change);
                    }
                }
            };
            if (removalsPending) {
                RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
                ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
            } else {
                changer.run();
            }
        }
        // 最後執行Add動畫
        if (additionsPending) {
            // ......
            Runnable adder = new Runnable() {
                @Override
                public void run() {
                    for (RecyclerView.ViewHolder holder : additions) {
                        animateAddImpl(holder);
                    }
                    additions.clear();
                    mAdditionsList.remove(additions);
                }
            };
            // 在Remove、Move、Change動畫都完成之後開始執行Add動畫
            if (removalsPending || movesPending || changesPending) {
                long removeDuration = removalsPending ? getRemoveDuration() : 0;
                long moveDuration = movesPending ? getMoveDuration() : 0;
                long changeDuration = changesPending ? getChangeDuration() : 0;
                long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
                View view = additions.get(0).itemView;
                ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
            } else {
                adder.run();
            }
        }
    }

我們發現動畫的執行是有順序的,Remove動畫首先執行,之後Move和Change動畫同時開始,等這3個動畫全部結束之後開始執行Add動畫。以RecyclerView刪除一個item爲例,動畫如下。

gif-Remove動畫.gif

我們發現動畫有2段,首先是被刪除item的Remove動畫,等到完全不可見之後,下方的多個item同時執行向上的Move動畫。相對的,如果向RecyclerView中添加一個item,會先執行item向下的Move動畫,再執行插入item的Add動畫。

runPendingAnimations()中真正執行動畫的是animateRemoveImpl()這樣的方法,來看一下它是怎麼實現的,這也是我們今後自定義動畫的關鍵。

    private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
        final View view = holder.itemView;
        final ViewPropertyAnimator animation = view.animate();
        mRemoveAnimations.add(holder);
        animation.setDuration(getRemoveDuration()).alpha(0).setListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchRemoveStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(Animator animator) {
                        animation.setListener(null);
                        view.setAlpha(1);
                        dispatchRemoveFinished(holder);
                        mRemoveAnimations.remove(holder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
    }

Remove動畫很簡單,通過ViewPropertyAnimator實現一個透明度到0的屬性動畫,不過要記得在動畫開始和結束時調用dispatchRemoveStarting()dispatchRemoveFinished()方法。相對的,Add動畫就是透明度從0到1的屬性動畫,而Remove動畫就是修改itemView的translation進行移動。
以上3個動畫都只涉及1個item,而Change動畫會涉及到2個item,DefaultItemAnimator中的實現是讓原來的item執行透明度從1到0的動畫,讓新item執行透明度從0到1的動畫。

    void animateChangeImpl(final ChangeInfo changeInfo) {
        final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
        final View view = holder == null ? null : holder.itemView;
        final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
        final View newView = newHolder != null ? newHolder.itemView : null;
        if (view != null) {
            final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
                    getChangeDuration());
            mChangeAnimations.add(changeInfo.oldHolder);
            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);
                    dispatchChangeFinished(changeInfo.oldHolder, true);
                    mChangeAnimations.remove(changeInfo.oldHolder);
                    dispatchFinishedWhenDone();
                }
            }).start();
        }
        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);
                    dispatchChangeFinished(changeInfo.newHolder, false);
                    mChangeAnimations.remove(changeInfo.newHolder);
                    dispatchFinishedWhenDone();
                }
            }).start();
        }
    }

以上就是DefaultItemAnimator的基本邏輯,它主要做了兩件事:
① 統一安排動畫的執行順序
② 對4種item動畫具體實現
自定義ItemAnimator時,我們可以直接使用DefaultItemAnimator的runPendingAnimations()方法來安排動畫的執行順序,只需要修改item的動畫即可。

1.2 動畫的觸發

接下來我們來看動畫的觸發流程,其關鍵就在於RecyclerView在執行動畫前已經計算出了每個item在動畫前後的位置等信息,隨後將這些信息傳給ItemAnimator統一執行。現在我們從notifyItemRemoved(int position)開始分析動畫觸發的流程。

    /**
     * 通知註冊的觀察者,position上的item從數據集中移除了
     * 之前在oldPosition上的item可能會出現在oldPosition - 1的位置上
     */
    public final void notifyItemRemoved(int position) {
        mObservable.notifyItemRangeRemoved(position, 1);
    }

mObservable是一個AdapterDataObservable對象,是RecyclerView數據集的被觀察者。mObservable會通知所有的Observer數據發生了改變,RecyclerView有個默認的觀察者RecyclerViewDataObserver,來看一下在item被Remove後它做了什麼。

    @Override
    public void onItemRangeRemoved(int positionStart, int itemCount) {
        assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
    }

直接執行了triggerUpdateProcessor()方法。

    void triggerUpdateProcessor() {
        if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
            ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
        } else {
            mAdapterUpdateDuringMeasure = true;
            requestLayout();
        }
    }

這裏有3個判斷條件,POST_UPDATES_ON_ANIMATION在SDK>=16時爲true;mHasFixedSize默認爲false,爲true時有優化性能的作用,可以減少requestLayout()方法的調用,這裏先不展開,之後性能優化會提到它。
當mHasFixedSize爲false時進入else代碼塊,執行requestLayout()方法,最終進入RecyclerView的onLayout()方法。(PS: mHasFixedSize爲true時最終也會執行到onLayout()方法)

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

onLayout()中調用了dispatchLayout()方法,在看它的代碼前不妨先看下方法介紹。介紹裏第一句就表示,這個方法用於處理由Layout產生的動畫,並且將動畫分爲了5種類型。很顯然這個方法就是今天的主角,下面來重點分析。

    /**
     * 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:
     * PERSISTENT: items are visible before and after layout
     * REMOVED: items were visible before layout and were removed by the app
     * ADDED: items did not exist before layout and were added by the app
     * 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)
     * 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()代碼如下,它依次調用dispatchLayoutStep1()dispatchLayoutStep2()dispatchLayoutStep3(),我們來逐個分析。

    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();
    }
dispatchLayoutStep1()

先來講dispatchLayoutStep1(),它的主要工作有:處理Adapter的數據更新、決定應該運行哪種動畫、記錄當前所有ItemView的信息、進行預佈局pre-layout並保存其信息。簡化後的代碼如下,只保留了動畫相關的部分。

    private void dispatchLayoutStep1() {
        // ......
        if (mState.mRunSimpleAnimations) {
            // 找到所有沒有被Remove的Item
            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());
                // 將Item信息保存到mViewInfoStore
                mViewInfoStore.addToPreLayout(holder, animationInfo);
                if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
                        && !holder.shouldIgnore() && !holder.isInvalid()) {
                    // ......
                }
            }
        }
        if (mState.mRunPredictiveAnimations) {
            // 開始pre-layout,此時使用的是oldPositions
            saveOldPositions();
            final boolean didStructureChange = mState.mStructureChanged;
            mState.mStructureChanged = false;
            // temporarily disable flag because we are asking for previous layout
            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)) {
                    // 這裏保存原本在屏幕外的Item的信息,代碼省略......
                }
            }
            // we don't process disappearing list because they may re-appear in post layout pass.
            clearOldPositions();
        } else {
            clearOldPositions();
        }
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
        mState.mLayoutStep = State.STEP_LAYOUT;
    }

首先找到沒有被Remove的Item並保存信息,保存時調用的是mViewInfoStore.addToPreLayout(),可以理解爲保存的是執行動畫前的信息。
如果mState.mRunPredictiveAnimations爲true就開始執行pre-layout,pre-layout使用的是Item的oldPosition,它會對所有的Item(包括被Remove的Item)進行佈局,並且爲動畫後顯示在屏幕上的Item提供位置。什麼意思呢?如果當前某個Item會Remove,原本屏幕外的Item就可能Move到屏幕上,這個Item的信息也需要被記錄,pre-layout就是爲這類Item提供了顯示動畫的能力。

來看下mViewInfoStore.addToPreLayout(holder, animationInfo)做了什麼。可以發現它通過鍵值對<ViewHolder, InfoRecord>保存Item的信息,並且由於當前保存的是動畫前的Item信息,爲InfoRecord添加FLAG_PRE標識。

    void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        record.preInfo = info;
        record.flags |= FLAG_PRE;
    }

InfoRecord的數據結構如下,在dispatchLayoutStep1()中保存的是preInfo

    static class InfoRecord {
        static final int FLAG_DISAPPEARED = 1;
        static final int FLAG_APPEAR = 1 << 1;
        static final int FLAG_PRE = 1 << 2;
        static final int FLAG_POST = 1 << 3;
        static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
        static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
        static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
        int flags;
        @Nullable
        RecyclerView.ItemAnimator.ItemHolderInfo preInfo;
        @Nullable
        RecyclerView.ItemAnimator.ItemHolderInfo postInfo;
    }
dispatchLayoutStep2()

再來看dispatchLayoutStep2(),它主要通過mLayout.onLayoutChildren(mRecycler, mState)對新的數據進行了佈局,隨後對是否支持動畫進行檢查並賦值。

	private void dispatchLayoutStep2() {
        // ......
        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
    }
dispatchLayoutStep3()

dispatchLayoutStep3()對Change動畫進行了特殊處理,如果是Change動畫會直接執行。對於其餘動畫來說,會先記錄動畫後的Item信息,記錄完畢後觸發動畫。

    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()) {
                    // 執行CHANGE動畫
                    final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                            oldChangeViewHolder);
                    final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                    if (oldDisappearing && oldChangeViewHolder == holder) {
                        // 如果1個Item CHANGED,但是更新後會消失,則執行disappear動畫
                        mViewInfoStore.addToPostLayout(holder, animationInfo);
                    } else {
                        final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
                                oldChangeViewHolder);
                        // we add and remove so that any post info is merged.
                        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 {
                    // 將Layout後的信息保存到mViewInfoStore中
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                }
            }
            // 觸發動畫
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
        // clean up...
    }

這裏通過mViewInfoStore.addToPostLayout(...)將Layout後的信息保存,再執行mViewInfoStore.process(mViewInfoProcessCallback)觸發動畫,主要邏輯爲根據之前保存的item信息執行對應的回調。

    void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                // Appeared then disappeared. Not useful for animations.
                callback.unused(viewHolder);
            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                // 被LayoutManager設置爲消失
                if (record.preInfo == null) {
                    callback.unused(viewHolder);
                } else {
                    callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
                }
            } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
                // Appeared in the layout but not in the adapter (e.g. entered the viewport)
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                // preLayout和postLayout都在,執行callback.processPersistent
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE) != 0) {
                // 在pre-layout中,但是不在post-layout中,因此item消失了
                callback.processDisappeared(viewHolder, record.preInfo, null);
            } else if ((record.flags & FLAG_POST) != 0) {
                // 不在pre-layout,出現在了post-layout
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_APPEAR) != 0) {
                // Scrap view. RecyclerView will handle removing/recycling this.
            }
            InfoRecord.recycle(record);
        }
    }

回調mViewInfoProcessCallback如下所示,基本就是執行對應的方法。

    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
            new ViewInfoStore.ProcessCallback() {
                @Override
                public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, @Nullable ItemHolderInfo postInfo) {
                    mRecycler.unscrapView(viewHolder);
                    animateDisappearance(viewHolder, info, postInfo);
                }

                @Override
                public void processAppeared(ViewHolder viewHolder, ItemHolderInfo preInfo, ItemHolderInfo info) {
                    animateAppearance(viewHolder, preInfo, info);
                }

                @Override
                public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
                    viewHolder.setIsRecyclable(false);
                    if (mDataSetHasChangedAfterLayout) {
                        if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {
                            postAnimationRunner();
                        }
                    } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
                        postAnimationRunner();
                    }
                }

                @Override
                public void unused(ViewHolder viewHolder) {
                    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
                }
            };

我們以animateDisappearance(...)爲例來分析,如果mItemAnimator對應的方法返回true的話就執行postAnimationRunner(),該方法就是將mItemAnimatorRunner放到下一幀執行,而mItemAnimatorRunner實際調用了mItemAnimator.runPendingAnimations()執行了一段時間內觸發的所有動畫。它們的代碼如下所示。

    void animateDisappearance(@NonNull ViewHolder holder,
                              @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
        addAnimatingView(holder);
        holder.setIsRecyclable(false);
        if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
            postAnimationRunner();
        }
    }

    void postAnimationRunner() {
        if (!mPostedAnimatorRunner && mIsAttached) {
            // 將mItemAnimatorRunner放到下一幀執行
            ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
            mPostedAnimatorRunner = true;
        }
    }

    private Runnable mItemAnimatorRunner = new Runnable() {
        @Override
        public void run() {
            if (mItemAnimator != null) {
                mItemAnimator.runPendingAnimations();
            }
            mPostedAnimatorRunner = false;
        }
    };

動畫觸發的大體邏輯就是這樣,不過爲了加深印象,我們再舉個栗子詳細描述下。還是以REMOVE動畫爲例,來逐步分析下方動畫執行時保存了哪些信息,又是怎麼判斷動畫類型的。

gif-例子.gif

上面提到,dispatchLayoutStep1()方法會將動畫執行前的ItemHolderInfo保存至ViewInfoStore,那麼上方的例子在動畫前會保存5個ItemHolderInfo,由於是VERTICAL佈局,只關注ItemHolderInfo的top和bottom即可。
下方的ViewHolder(0)等表示保存時的Key,括號中的數字爲該ViewHolder對應ItemView顯示的數據

ViewHolder(0) : InfoRecord->preInfo(top: 0,   bottom: 131)
ViewHolder(1) : InfoRecord->preInfo(top: 131, bottom: 262)
ViewHolder(2) : InfoRecord->preInfo(top: 262, bottom: 393)
ViewHolder(3) : InfoRecord->preInfo(top: 393, bottom: 524)
ViewHolder(4) : InfoRecord->preInfo(top: 524, bottom: 655)

隨後dispatchLayoutStep2()調用mLayout.onLayoutChildren(mRecycler, mState)進行佈局。佈局完畢後執行dispatchLayoutStep3(),開始保存Layout之後的ItemHolderInfo,此時有4個Item,它們的信息會被保存至InfoRecord的postInfo中,最終ViewInfoStore中mLayoutHolderMap的信息如下所示。

ViewHolder0 : InfoRecord->preInfo(top: 0,   bottom: 131), postInfo(top: 0, bottom: 131)
ViewHolder1 : InfoRecord->preInfo(top: 131, bottom: 262), postInfo(null)
ViewHolder2 : InfoRecord->preInfo(top: 262, bottom: 393), postInfo(top: 131, bottom: 262)
ViewHolder3 : InfoRecord->preInfo(top: 393, bottom: 524), postInfo(top: 262, bottom: 393)
ViewHolder4 : InfoRecord->preInfo(top: 524, bottom: 655), postInfo(top: 393, bottom: 524)

隨後執行mViewInfoStore.process(mViewInfoProcessCallback)開始動畫,這裏通過判斷preInfo和postInfo是否存在去執行對應的回調,下面的代碼只保留了本次例子相關的部分。

    void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                // ......
            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                // ......
            } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
                // ......
            } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                // preInfo和postInfo都有,執行callback.processPersistent(...)
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE) != 0) {
                // 有preInfo沒有postInfo,說明Item被移除了,執行callback.processDisappeared()
                callback.processDisappeared(viewHolder, record.preInfo, null);
            } else if ((record.flags & FLAG_POST) != 0) {
                // ......
            } else if ((record.flags & FLAG_APPEAR) != 0) {
                // ......
            }
            InfoRecord.recycle(record);
        }
    }

先來看callback.processPersistent(),由於整個dataSet並未改變,因此進入else代碼塊,根據mItemAnimator.animatePersistence(...)的返回值決定是否執行postAnimationRunner()

    @Override
    public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        viewHolder.setIsRecyclable(false);
        if (mDataSetHasChangedAfterLayout) {
            // since it was rebound, use change instead as we'll be mapping them from
            // stable ids. If stable ids were false, we would not be running any animations
            if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {
                postAnimationRunner();
            }
        } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
            postAnimationRunner();
        }
    }

mItemAnimator.animatePersistence(...)執行的是SimpleItemAnimator中重寫的方法,如下所示。根據上面總結的ViewInfoStore中保存的值,對於ViewHolder(2)、ViewHolder(3)和ViewHolder(4)來說,preInfo.top和postInfo.top不相等,執行animateMove(...),最終會執行DefaultItemAnimator中重寫的方法,將MOVE動畫添加到待執行動畫列表中。而對於ViewHolder(0)來說,preInfo和postInfo中的值相等,就不用執行動畫。

    @Override
    public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,
                                      @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
            return animateMove(viewHolder,
                    preInfo.left, preInfo.top, postInfo.left, postInfo.top);
        }
        dispatchMoveFinished(viewHolder);
        return false;
    }

再來看callback.processDisappeared(),直接執行了animateDisappearance(...)

    @Override
    public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
                            @Nullable ItemHolderInfo postInfo) {
        mRecycler.unscrapView(viewHolder);
        animateDisappearance(viewHolder, info, postInfo);
    }

也是根據mItemAnimator.animateDisappearance()的返回值決定是否執行postAnimationRunner()

    void animateDisappearance(@NonNull ViewHolder holder,
                              @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
        addAnimatingView(holder);
        holder.setIsRecyclable(false);
        if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
            postAnimationRunner();
        }
    }

mItemAnimator.animateDisappearance()執行了SimpleItemAnimator中重寫的方法。

    @Override
    public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder,
                                        @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
        int oldLeft = preLayoutInfo.left;
        int oldTop = preLayoutInfo.top;
        View disappearingItemView = viewHolder.itemView;
        int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
        int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
        if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
            disappearingItemView.layout(newLeft, newTop,
                    newLeft + disappearingItemView.getWidth(),
                    newTop + disappearingItemView.getHeight());
            }
            return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
        } else {
            return animateRemove(viewHolder);
        }
    }

由於viewHolder.isRemoved()返回true,因此執行animateRemove(viewHolder),最終執行DefaultItemAnimator中重寫的方法,將動畫添加到了執行隊列中。
到此,我們的例子也就講完了。

二、自定義動畫

DefaultItemAnimator提供的動畫效果還是比較完善的,如果還有其他需求的話,在animateRemoveImpl(...)animateAddImpl(...)這樣的方法中修改動畫效果即可。方法內部通過ViewPropertyAnimator實現具體的動畫效果,修改起來比較簡單,例如我們可以將DefaultItemAnimator的Remove動畫修改爲:Item縮小至消失。

    private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
        // ......
        animation.setDuration(getRemoveDuration()).scaleX(0).scaleY(0).setListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchRemoveStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(Animator animator) {
                        animation.setListener(null);
                        view.setScaleX(1);
                        view.setScaleY(1);
                        dispatchRemoveFinished(holder);
                        mRemoveAnimations.remove(holder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
    }

真的超簡單啊有沒有!!!來看下效果。

gif-自定義.gif

emmmm…效果不怎麼好看,不過只要能設計出優美的動畫,自定義ItemAnimator是一件很簡單的事情。

三、參考

  1. RecyclerView.ItemAnimator終極解讀(一)–RecyclerView源碼解析
  2. RecyclerView機制解析: ChildHelper
  3. RecyclerView剖析
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章