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

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

俗話說沛申,好看的皮囊千篇一律,有趣的靈魂萬里挑一耘拇。但是對于我們這些俗人來說撵颊,肯定是選擇好看的皮囊,咱們的用戶也是如此惫叛。你看看應(yīng)用市場上那些花枝招展的APP倡勇,哪個不是用上了五花八門的動畫效果,就算你的內(nèi)在安全省電性能好嘉涌,沒點兒花招可留不住花心的用戶译隘。所以我們今天就來看看怎么實現(xiàn)讓用戶眼前一亮的動畫,當(dāng)然原理也很重要洛心,因此源碼分析必不可少固耘,本文的源碼分析主要聚焦于動畫是怎么觸發(fā)的,以及動畫是怎么實現(xiàn)的词身。

一厅目、動畫的觸發(fā)與實現(xiàn)

當(dāng)Adapter中的數(shù)據(jù)發(fā)生變化時,我們通過notifyItemXXX()等方法通知RecyclerView來改變數(shù)據(jù)的展示法严,這個過程必然伴隨新的layout()损敷。如果在layout()后直接顯示新數(shù)據(jù),效果比較僵硬深啤,因此需要通過動畫來制造良好的用戶體驗拗馒。
那么,為了實現(xiàn)動畫溯街,RecyclerView又額外做了哪些工作呢诱桂?抽象上來講洋丐,RecyclerView實現(xiàn)動畫的步驟如下。
① 數(shù)據(jù)發(fā)生改變時挥等,保存當(dāng)前的item信息為preInfo
② 根據(jù)新的數(shù)據(jù)Layout
③ Layout完畢友绝,保存當(dāng)前的item信息為postInfo
④ 根據(jù)preInfo和postInfo判斷動畫類型并交給ItemAnimator執(zhí)行
可以發(fā)現(xiàn),前3步保存了執(zhí)行動畫所需要的信息肝劲,最后整體交給ItemAnimator來執(zhí)行動畫迁客。前3步涉及到內(nèi)容較為復(fù)雜,我們先從簡單的開始分析辞槐,來看ItemAnimator是怎么實現(xiàn)動畫的掷漱。

1.1 動畫的實現(xiàn)

由于RecyclerView設(shè)計時的低耦合性,ItemAnimator只需要關(guān)注怎么執(zhí)行動畫即可榄檬,其邏輯并不復(fù)雜卜范。RecyclerView為我們實現(xiàn)了DefaultItemAnimator,在不設(shè)置動畫的情況下默認(rèn)使用它丙号,其中實現(xiàn)了4個針對item的動畫,分別為Remove缰冤、Move犬缨、Add和Change。以Remove動畫為例棉浸,當(dāng)一個item被移出RecyclerView時怀薛,DefaultItemAnimator中的animateRemove(holder)方法就會被調(diào)用,但是并沒有馬上開始執(zhí)行動畫迷郑,而是將動畫添加到了mPendingRemovals中枝恋,這是一個待執(zhí)行的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<>();

當(dāng)RecyclerView要執(zhí)行動畫時,ItemAnimator的runPendingAnimations()方法會被調(diào)用霸妹,DefaultItemAnimator重寫后的方法如下十电,為了便于閱讀,添加了注釋并省略了部分代碼叹螟。

    @Override
    public void runPendingAnimations() {
        boolean removalsPending = !mPendingRemovals.isEmpty();
        boolean movesPending = !mPendingMoves.isEmpty();
        boolean changesPending = !mPendingChanges.isEmpty();
        boolean additionsPending = !mPendingAdditions.isEmpty();
        // 判斷是否有動畫需要執(zhí)行
        if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
            return;
        }
        // 執(zhí)行Remove動畫
        for (RecyclerView.ViewHolder holder : mPendingRemovals) {
            animateRemoveImpl(holder); // 實際執(zhí)行Remove動畫的方法
        }
        mPendingRemovals.clear();
        // Move動畫
        if (movesPending) {
            // 注意: Move動畫并不是馬上執(zhí)行鹃骂,會放入一個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動畫結(jié)束之后執(zhí)行Move動畫
            // 如果沒有Remove動畫就馬上執(zhí)行
            if (removalsPending) {
                View view = moves.get(0).holder.itemView;
                ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
            } else {
                mover.run();
            }
        }
        // Change動畫罢绽,與Move動畫一起執(zhí)行
        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();
            }
        }
        // 最后執(zhí)行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動畫都完成之后開始執(zhí)行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();
            }
        }
    }

我們發(fā)現(xiàn)動畫的執(zhí)行是有順序的良价,Remove動畫首先執(zhí)行寝殴,之后Move和Change動畫同時開始蒿叠,等這3個動畫全部結(jié)束之后開始執(zhí)行Add動畫。以RecyclerView刪除一個item為例杯矩,動畫如下栈虚。

gif-Remove動畫.gif

我們發(fā)現(xiàn)動畫有2段,首先是被刪除item的Remove動畫史隆,等到完全不可見之后魂务,下方的多個item同時執(zhí)行向上的Move動畫。相對的泌射,如果向RecyclerView中添加一個item粘姜,會先執(zhí)行item向下的Move動畫,再執(zhí)行插入item的Add動畫熔酷。

runPendingAnimations()中真正執(zhí)行動畫的是animateRemoveImpl()這樣的方法孤紧,來看一下它是怎么實現(xiàn)的,這也是我們今后自定義動畫的關(guān)鍵拒秘。

    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實現(xiàn)一個透明度到0的屬性動畫,不過要記得在動畫開始和結(jié)束時調(diào)用dispatchRemoveStarting()dispatchRemoveFinished()方法躺酒。相對的押蚤,Add動畫就是透明度從0到1的屬性動畫,而Remove動畫就是修改itemView的translation進行移動羹应。
以上3個動畫都只涉及1個item揽碘,而Change動畫會涉及到2個item,DefaultItemAnimator中的實現(xiàn)是讓原來的item執(zhí)行透明度從1到0的動畫园匹,讓新item執(zhí)行透明度從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的基本邏輯,它主要做了兩件事:
① 統(tǒng)一安排動畫的執(zhí)行順序
② 對4種item動畫具體實現(xiàn)
自定義ItemAnimator時裸违,我們可以直接使用DefaultItemAnimator的runPendingAnimations()方法來安排動畫的執(zhí)行順序掖桦,只需要修改item的動畫即可。

1.2 動畫的觸發(fā)

接下來我們來看動畫的觸發(fā)流程供汛,其關(guān)鍵就在于RecyclerView在執(zhí)行動畫前已經(jīng)計算出了每個item在動畫前后的位置等信息滞详,隨后將這些信息傳給ItemAnimator統(tǒng)一執(zhí)行。現(xiàn)在我們從notifyItemRemoved(int position)開始分析動畫觸發(fā)的流程紊馏。

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

mObservable是一個AdapterDataObservable對象,是RecyclerView數(shù)據(jù)集的被觀察者朱监。mObservable會通知所有的Observer數(shù)據(jù)發(fā)生了改變岸啡,RecyclerView有個默認(rèn)的觀察者RecyclerViewDataObserver,來看一下在item被Remove后它做了什么赫编。

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

直接執(zhí)行了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默認(rèn)為false啤贩,為true時有優(yōu)化性能的作用混萝,可以減少requestLayout()方法的調(diào)用,這里先不展開搬味,之后性能優(yōu)化會提到它境氢。
當(dāng)mHasFixedSize為false時進入else代碼塊,執(zhí)行requestLayout()方法碰纬,最終進入RecyclerView的onLayout()方法萍聊。(PS: mHasFixedSize為true時最終也會執(zhí)行到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()中調(diào)用了dispatchLayout()方法,在看它的代碼前不妨先看下方法介紹悦析。介紹里第一句就表示寿桨,這個方法用于處理由Layout產(chǎn)生的動畫,并且將動畫分為了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()代碼如下骑歹,它依次調(diào)用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的數(shù)據(jù)更新衰琐、決定應(yīng)該運行哪種動畫、記錄當(dāng)前所有ItemView的信息炼蹦、進行預(yù)布局pre-layout并保存其信息羡宙。簡化后的代碼如下,只保留了動畫相關(guān)的部分掐隐。

    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;
    }

首先找到?jīng)]有被Remove的Item并保存信息虑省,保存時調(diào)用的是mViewInfoStore.addToPreLayout()匿刮,可以理解為保存的是執(zhí)行動畫前的信息。
如果mState.mRunPredictiveAnimations為true就開始執(zhí)行pre-layout探颈,pre-layout使用的是Item的oldPosition熟丸,它會對所有的Item(包括被Remove的Item)進行布局,并且為動畫后顯示在屏幕上的Item提供位置伪节。什么意思呢光羞?如果當(dāng)前某個Item會Remove绩鸣,原本屏幕外的Item就可能Move到屏幕上,這個Item的信息也需要被記錄纱兑,pre-layout就是為這類Item提供了顯示動畫的能力呀闻。

來看下mViewInfoStore.addToPreLayout(holder, animationInfo)做了什么∏鄙鳎可以發(fā)現(xiàn)它通過鍵值對<ViewHolder, InfoRecord>保存Item的信息捡多,并且由于當(dāng)前保存的是動畫前的Item信息,為InfoRecord添加FLAG_PRE標(biāo)識勘纯。

    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的數(shù)據(jù)結(jié)構(gòu)如下局服,在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)對新的數(shù)據(jù)進行了布局驳遵,隨后對是否支持動畫進行檢查并賦值淫奔。

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動畫會直接執(zhí)行堤结。對于其余動畫來說唆迁,會先記錄動畫后的Item信息,記錄完畢后觸發(fā)動畫竞穷。

    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()) {
                    // 執(zhí)行CHANGE動畫
                    final boolean oldDisappearing = mViewInfoStore.isDisappearing(
                            oldChangeViewHolder);
                    final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
                    if (oldDisappearing && oldChangeViewHolder == holder) {
                        // 如果1個Item CHANGED唐责,但是更新后會消失,則執(zhí)行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);
                }
            }
            // 觸發(fā)動畫
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
        // clean up...
    }

這里通過mViewInfoStore.addToPostLayout(...)將Layout后的信息保存瘾带,再執(zhí)行mViewInfoStore.process(mViewInfoProcessCallback)觸發(fā)動畫鼠哥,主要邏輯為根據(jù)之前保存的item信息執(zhí)行對應(yīng)的回調(diào)。

    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設(shè)置為消失
                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都在看政,執(zhí)行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允蚣,出現(xiàn)在了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);
        }
    }

回調(diào)mViewInfoProcessCallback如下所示于颖,基本就是執(zhí)行對應(yīng)的方法。

    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對應(yīng)的方法返回true的話就執(zhí)行postAnimationRunner()森渐,該方法就是將mItemAnimatorRunner放到下一幀執(zhí)行,而mItemAnimatorRunner實際調(diào)用了mItemAnimator.runPendingAnimations()執(zhí)行了一段時間內(nèi)觸發(fā)的所有動畫冒晰。它們的代碼如下所示同衣。

    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放到下一幀執(zhí)行
            ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
            mPostedAnimatorRunner = true;
        }
    }

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

動畫觸發(fā)的大體邏輯就是這樣,不過為了加深印象壶运,我們再舉個栗子詳細描述下耐齐。還是以REMOVE動畫為例,來逐步分析下方動畫執(zhí)行時保存了哪些信息,又是怎么判斷動畫類型的蚪缀。

gif-例子.gif

上面提到秫逝,dispatchLayoutStep1()方法會將動畫執(zhí)行前的ItemHolderInfo保存至ViewInfoStore,那么上方的例子在動畫前會保存5個ItemHolderInfo询枚,由于是VERTICAL布局违帆,只關(guān)注ItemHolderInfo的top和bottom即可。
下方的ViewHolder(0)等表示保存時的Key金蜀,括號中的數(shù)字為該ViewHolder對應(yīng)ItemView顯示的數(shù)據(jù)刷后。

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()調(diào)用mLayout.onLayoutChildren(mRecycler, mState)進行布局。布局完畢后執(zhí)行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)

隨后執(zhí)行mViewInfoStore.process(mViewInfoProcessCallback)開始動畫,這里通過判斷preInfo和postInfo是否存在去執(zhí)行對應(yīng)的回調(diào)二庵,下面的代碼只保留了本次例子相關(guān)的部分贪染。

    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都有,執(zhí)行callback.processPersistent(...)
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE) != 0) {
                // 有preInfo沒有postInfo催享,說明Item被移除了杭隙,執(zhí)行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代碼塊痰憎,根據(jù)mItemAnimator.animatePersistence(...)的返回值決定是否執(zhí)行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(...)執(zhí)行的是SimpleItemAnimator中重寫的方法,如下所示攀涵。根據(jù)上面總結(jié)的ViewInfoStore中保存的值铣耘,對于ViewHolder(2)、ViewHolder(3)和ViewHolder(4)來說汁果,preInfo.top和postInfo.top不相等涡拘,執(zhí)行animateMove(...)玲躯,最終會執(zhí)行DefaultItemAnimator中重寫的方法据德,將MOVE動畫添加到待執(zhí)行動畫列表中。而對于ViewHolder(0)來說跷车,preInfo和postInfo中的值相等棘利,就不用執(zhí)行動畫。

    @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()朽缴,直接執(zhí)行了animateDisappearance(...)

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

也是根據(jù)mItemAnimator.animateDisappearance()的返回值決定是否執(zhí)行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()執(zhí)行了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,因此執(zhí)行animateRemove(viewHolder)密强,最終執(zhí)行DefaultItemAnimator中重寫的方法茅郎,將動畫添加到了執(zhí)行隊列中蜗元。
到此,我們的例子也就講完了系冗。

二奕扣、自定義動畫

DefaultItemAnimator提供的動畫效果還是比較完善的,如果還有其他需求的話掌敬,在animateRemoveImpl(...)animateAddImpl(...)這樣的方法中修改動畫效果即可惯豆。方法內(nèi)部通過ViewPropertyAnimator實現(xiàn)具體的動畫效果,修改起來比較簡單奔害,例如我們可以將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...效果不怎么好看雅潭,不過只要能設(shè)計出優(yōu)美的動畫瘪匿,自定義ItemAnimator是一件很簡單的事情。

三寻馏、參考

  1. RecyclerView.ItemAnimator終極解讀(一)--RecyclerView源碼解析
  2. RecyclerView機制解析: ChildHelper
  3. RecyclerView剖析
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棋弥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子诚欠,更是在濱河造成了極大的恐慌顽染,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轰绵,死亡現(xiàn)場離奇詭異粉寞,居然都是意外死亡,警方通過查閱死者的電腦和手機左腔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門唧垦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人液样,你說我怎么就攤上這事振亮。” “怎么了鞭莽?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵坊秸,是天一觀的道長。 經(jīng)常有香客問我澎怒,道長褒搔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮星瘾,結(jié)果婚禮上走孽,老公的妹妹穿的比我還像新娘。我一直安慰自己琳状,他們只是感情好融求,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著算撮,像睡著了一般生宛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肮柜,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天陷舅,我揣著相機與錄音,去河邊找鬼审洞。 笑死莱睁,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的芒澜。 我是一名探鬼主播仰剿,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼痴晦!你這毒婦竟也來了南吮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤誊酌,失蹤者是張志新(化名)和其女友劉穎部凑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碧浊,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡涂邀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了箱锐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片比勉。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖驹止,靈堂內(nèi)的尸體忽然破棺而出浩聋,到底是詐尸還是另有隱情,我是刑警寧澤幢哨,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布赡勘,位于F島的核電站嫂便,受9級特大地震影響捞镰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一岸售、第九天 我趴在偏房一處隱蔽的房頂上張望践樱。 院中可真熱鬧,春花似錦凸丸、人聲如沸拷邢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞭稼。三九已至,卻和暖如春腻惠,著一層夾襖步出監(jiān)牢的瞬間环肘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工集灌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留悔雹,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓欣喧,卻偏偏與公主長得像腌零,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子唆阿,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容