RecyclerView動(dòng)畫(huà)源碼淺析

本文是RecyclerView源碼分析系列第四篇文章,內(nèi)容主要是基于前三篇文章來(lái)敘述的,因此在閱讀之前推薦看一下前3篇文章:

RecylcerView的基本設(shè)計(jì)結(jié)構(gòu)

RecyclerView的刷新機(jī)制

RecyclerView的復(fù)用機(jī)制

本文主要分析RecyclerView刪除動(dòng)畫(huà)的實(shí)現(xiàn)原理,不同類(lèi)型動(dòng)畫(huà)的大體實(shí)現(xiàn)流程其實(shí)都是差不多的,所以對(duì)于添加、交換這種動(dòng)畫(huà)就不再做分析。本文主要目標(biāo)是理解清楚的是RecyclerViewItem刪除動(dòng)畫(huà)源碼實(shí)現(xiàn)邏輯。文章比較長(zhǎng)。

可以通過(guò)下面這兩個(gè)方法觸發(fā)RecyclerView的刪除動(dòng)畫(huà):

    //一個(gè)item的刪除動(dòng)畫(huà)
    dataSource.removeAt(1)
    recyclerView.adapter.notifyItemRemoved(1)

    //多個(gè)item的刪除動(dòng)畫(huà)
    dataSource.removeAt(1)
    dataSource.removeAt(1)
    recyclerView.adapter.notifyItemRangeRemoved(1,2)

下面這個(gè)圖是設(shè)置10倍動(dòng)畫(huà)時(shí)長(zhǎng)時(shí)刪除動(dòng)畫(huà)的執(zhí)行效果,可以先預(yù)想一下這個(gè)動(dòng)畫(huà)時(shí)大致可以怎么實(shí)現(xiàn):

RecyclerViewRemoveAnimation.gif

接下來(lái)就結(jié)合前面幾篇文章的內(nèi)容并跟隨源碼來(lái)一塊看一下RecyclerView是如何實(shí)現(xiàn)這個(gè)動(dòng)畫(huà)的:

adapter.notifyItemRemoved(1)會(huì)回調(diào)到RecyclerViewDataObserver:

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

其實(shí)按照onItemRangeRemoved()這個(gè)的執(zhí)行邏輯方法可以將Item刪除動(dòng)畫(huà)分為兩個(gè)部分:

  1. 添加一個(gè)UpdateOpAdapterHelper.mPendingUpdates中。
  2. triggerUpdateProcessor()調(diào)用了requestLayout, 即觸發(fā)了RecyclerView的重新布局。

先來(lái)看mAdapterHelper.onItemRangeRemoved(positionStart, itemCount):

AdapterHelper

這個(gè)類(lèi)可以理解為是用來(lái)記錄adapter.notifyXXX動(dòng)作的,即每一個(gè)Operation(添加、刪除)都會(huì)在這個(gè)類(lèi)中有一個(gè)對(duì)應(yīng)記錄UpdateOp,RecyclerView在布局時(shí)會(huì)檢查這些UpdateOp,并做對(duì)應(yīng)的操作。
mAdapterHelper.onItemRangeRemoved其實(shí)是添加一個(gè)Remove UpdateOp:

    mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
    mExistingUpdateTypes |= UpdateOp.REMOVE;

即把一個(gè)Remove UpdateOp添加到了mPendingUpdates集合中。

RecyclerView.layout

RecyclerView的刷新機(jī)制中知道RecyclerView的布局一共分為3分步驟:dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3(),接下來(lái)我們就分析這3步中有關(guān)Item刪除動(dòng)畫(huà)的工作。

dispatchLayoutStep1(保存動(dòng)畫(huà)現(xiàn)場(chǎng))

直接從dispatchLayoutStep1()開(kāi)始看,這個(gè)方法是RecyclerView布局的第一步:

dispatchLayoutStep1():

    private void dispatchLayoutStep1() {
        ...
        processAdapterUpdatesAndSetAnimationFlags();
        ...
        if (mState.mRunSimpleAnimations) {
            ...
        }
        ...
    }

上面我只貼出了Item刪除動(dòng)畫(huà)主要涉及到的部分, 先來(lái)看一下processAdapterUpdatesAndSetAnimationFlags()所觸發(fā)的操作,整個(gè)操作鏈比較長(zhǎng),就不一一跟了,它最終其實(shí)是調(diào)用到AdapterHelper.postponeAndUpdateViewHolders():

private void postponeAndUpdateViewHolders(UpdateOp op) {
    mPostponedList.add(op); //op其實(shí)是從mPendingUpdates中取出來(lái)的
    switch (op.cmd) {
        case UpdateOp.ADD:
            mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); break;
        case UpdateOp.MOVE:
            mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); break;
        case UpdateOp.REMOVE:
            mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, op.itemCount); break;  
        case UpdateOp.UPDATE:
            mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload); break;    
        ...
    }
}

即這個(gè)方法做的事情就是把mPendingUpdates中的UpdateOp添加到mPostponedList中,并回調(diào)根據(jù)op.cmd來(lái)回調(diào)mCallback,其實(shí)這個(gè)mCallback是回調(diào)到了RecyclerView中:

 void offsetPositionRecordsForRemove(int positionStart, int itemCount, boolean applyToPreLayout) {
        final int positionEnd = positionStart + itemCount;
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for (int i = 0; i < childCount; i++) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
            ...
            if (holder.mPosition >= positionEnd) {
                holder.offsetPosition(-itemCount, applyToPreLayout);
                mState.mStructureChanged = true;
            }
            ...
        }
        ...
    }

offsetPositionRecordsForRemove方法:主要是把當(dāng)前顯示在界面上的ViewHolder的位置做對(duì)應(yīng)的改變,即如果item位于刪除的item之后,那么它的位置應(yīng)該減一,比如原來(lái)的位置是3現(xiàn)在變成了2。

接下來(lái)繼續(xù)看dispatchLayoutStep1()中的操作:

    if (mState.mRunSimpleAnimations) {
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            //根據(jù)當(dāng)前的顯示在界面上的ViewHolder的布局信息創(chuàng)建一個(gè)ItemHolderInfo
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPreLayoutInformation(mState, holder,
                            ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                            holder.getUnmodifiedPayloads());
            mViewInfoStore.addToPreLayout(holder, animationInfo); //把 holder對(duì)應(yīng)的animationInfo保存到 mViewInfoStore中
            ...
        }
    }

即就做了兩件事:

  1. 為當(dāng)前顯示在界面上的每一個(gè)ViewHolder創(chuàng)建一個(gè)ItemHolderInfo,ItemHolderInfo其實(shí)就是保存了當(dāng)前顯示itemview的布局的top、left等信息
  2. 拿著ViewHolder和其對(duì)應(yīng)的ItemHolderInfo調(diào)用mViewInfoStore.addToPreLayout(holder, animationInfo)。

mViewInfoStore.addToPreLayout()就是把這些信息保存起來(lái):

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

即把holder 和 info保存到mLayoutHolderMap中??梢岳斫鉃樗?strong>用來(lái)保存動(dòng)畫(huà)執(zhí)行前當(dāng)前界面ViewHolder的信息一個(gè)集合。

到這里大致理完了在執(zhí)行Items刪除動(dòng)畫(huà)時(shí)AdapterHelperdispatchLayoutStep1()的執(zhí)行邏輯,這里用一張圖來(lái)總結(jié)一下:

Remove動(dòng)畫(huà)dispatchLayoutStep1.png

其實(shí)這些操作可以簡(jiǎn)單的理解為保存動(dòng)畫(huà)前View的現(xiàn)場(chǎng) 。其實(shí)這里有一次預(yù)布局,預(yù)布局也是為了保存動(dòng)畫(huà)前的View信息,不過(guò)這里就不講了。

dispatchLayoutStep2

這一步就是擺放當(dāng)前adapter中剩余的Item,在本文的例子中,就是依次擺放剩余的5個(gè)Item。在前面的文章RecyclerView的刷新機(jī)制中,我們知道LinearLayoutManager會(huì)向RecyclerView來(lái)填充RecyclerView,所以RecyclerView中填幾個(gè)View,其實(shí)和Recycler有很大的關(guān)系,因?yàn)?code>Recycler不給LinearLayoutManager的話(huà),RecyclerView中就不會(huì)有View填充。那RecyclerLinearLayoutManager``View的邊界條件是什么呢?
我們來(lái)看一下tryGetViewHolderForPositionByDeadline()方法:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        if (position < 0 || position >= mState.getItemCount()) {
            throw new IndexOutOfBoundsException("Invalid item position " + position
                    + "(" + position + "). Item count:" + mState.getItemCount()
                    + exceptionLabel());
        }
}

即如果位置大于mState.getItemCount(),那么就不會(huì)再向RecyclerView中填充子View。而這個(gè)mState.getItemCount()一般就是adapter中當(dāng)前數(shù)據(jù)源的數(shù)量。所以經(jīng)過(guò)這一步布局后,View的狀態(tài)如下圖:

Remove動(dòng)畫(huà)布局.png

這時(shí)你可能就有疑問(wèn)了? 動(dòng)畫(huà)呢? 怎么直接成最終的模樣了?別急,這一步只不過(guò)是布局,至于動(dòng)畫(huà)是怎么執(zhí)行的我們繼續(xù)往下看:

dispatchLayoutStep3(執(zhí)行刪除動(dòng)畫(huà))

在上一步中對(duì)刪除操作已經(jīng)完成了布局,接下來(lái)dispatchLayoutStep3()就會(huì)做刪除動(dòng)畫(huà):

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        ...
        mViewInfoStore.process(mViewInfoProcessCallback); //觸發(fā)動(dòng)畫(huà)的執(zhí)行
    }
    ...
}

可以看到主要涉及到動(dòng)畫(huà)的是mViewInfoStore.process(), 其實(shí)這一步可以分為兩個(gè)操作:

  1. 先把Item View動(dòng)畫(huà)前的起始狀態(tài)準(zhǔn)備好
  2. 執(zhí)行動(dòng)畫(huà)使Item View到目標(biāo)布局位置

下面我們來(lái)繼續(xù)跟一下mViewInfoStore.process()這個(gè)方法

Item View動(dòng)畫(huà)前的起始狀態(tài)準(zhǔn)備好

 void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { //對(duì)mLayoutHolderMap中每一個(gè)Holder執(zhí)行動(dòng)畫(huà)
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                callback.unused(viewHolder);
            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);  //被刪除的那個(gè)item會(huì)回調(diào)到這個(gè)地方
            }else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);   //需要上移的item會(huì)回調(diào)到這個(gè)地方
            }  
            ...
            InfoRecord.recycle(record);
        }
    }

這一步就是遍歷mLayoutHolderMap對(duì)其中的每一個(gè)ViewHolder做對(duì)應(yīng)的動(dòng)畫(huà)。這里callback會(huì)調(diào)到了RecyclerView,RecyclerView會(huì)對(duì)每一個(gè)Item執(zhí)行相應(yīng)的動(dòng)畫(huà):

ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
        new ViewInfoStore.ProcessCallback() {
            @Override
            public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,@Nullable ItemHolderInfo postInfo) {
                mRecycler.unscrapView(viewHolder);   //從scrap集合中移除,
                animateDisappearance(viewHolder, info, postInfo);
            }

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

先來(lái)分析被刪除那那個(gè)Item的消失動(dòng)畫(huà):

將Item的動(dòng)畫(huà)消失動(dòng)畫(huà)放入到mPendingRemovals待執(zhí)行隊(duì)列

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

先把Holderattch到RecyclerView上(這是因?yàn)樵?code>dispatchLayoutStep1和dispatchLayoutStep2中已經(jīng)對(duì)這個(gè)Holder做了Dettach)。即它又重新出現(xiàn)在了RecyclerView的布局中(位置當(dāng)然還是未刪除前的位置)。然后調(diào)用了mItemAnimator.animateDisappearance()其執(zhí)行這個(gè)刪除動(dòng)畫(huà),mItemAnimatorRecyclerView的動(dòng)畫(huà)實(shí)現(xiàn)者,它對(duì)應(yīng)的是DefaultItemAnimator。繼續(xù)看animateDisappearance()它其實(shí)最終調(diào)用到了DefaultItemAnimator.animateRemove():

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

即,其實(shí)并沒(méi)有執(zhí)行動(dòng)畫(huà),而是把這個(gè)holder放入了mPendingRemovals集合中,看樣是要等下執(zhí)行。

將未被刪除的Item的移動(dòng)動(dòng)畫(huà)放入到mPendingMoves待執(zhí)行隊(duì)列

其實(shí)邏輯和上面差不多DefaultItemAnimator.animatePersistence():

public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
    if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {  //和預(yù)布局的狀態(tài)不同,則執(zhí)行move動(dòng)畫(huà)
        return animateMove(viewHolder,preInfo.left, preInfo.top, postInfo.left, postInfo.top);
    }
    ...
}

animateMove的邏輯也很簡(jiǎn)單,就是根據(jù)偏移構(gòu)造了一個(gè)MoveInfo然后添加到mPendingMoves中,也沒(méi)有立刻執(zhí)行:

public boolean animateMove(final RecyclerView.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;
    if (deltaX == 0 && deltaY == 0) {
        dispatchMoveFinished(holder);
        return false;
    }
    if (deltaX != 0) {
        view.setTranslationX(-deltaX);  //設(shè)置他們的位置為負(fù)偏移?。。。?!
    }
    if (deltaY != 0) {
        view.setTranslationY(-deltaY);  //設(shè)置他們的位置為負(fù)偏移?。。。?!
    }
    mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
    return true;
}

但要注意這一步把要做滾動(dòng)動(dòng)畫(huà)的View的TranslationXTranslationY都設(shè)置負(fù)的被刪除的Item的高度,如下圖

DefaultItemAnimator.animateMove.png

即被刪除的Item之后的Item都下移了

postAnimationRunner()執(zhí)行所有的pending動(dòng)畫(huà)

上面一步操作已經(jīng)把動(dòng)畫(huà)前的狀態(tài)準(zhǔn)備好了,postAnimationRunner()就是將上面pendding的動(dòng)畫(huà)開(kāi)始執(zhí)行:

//DefaultItemAnimator.java

    public void runPendingAnimations() {
        boolean removalsPending = !mPendingRemovals.isEmpty();
        ...
        for (RecyclerView.ViewHolder holder : mPendingRemovals) {
            animateRemoveImpl(holder); //執(zhí)行pending的刪除動(dòng)畫(huà)
        }
        mPendingRemovals.clear();

        if (!mPendingMoves.isEmpty()) { //執(zhí)行pending的move動(dòng)畫(huà)
            final ArrayList<MoveInfo> moves = new ArrayList<>();
            moves.addAll(mPendingMoves);
            mMovesList.add(moves);
            mPendingMoves.clear();
            Runnable mover = new Runnable() {
                @Override
                public void run() {
                    for (MoveInfo moveInfo : moves) {
                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
                                moveInfo.toX, moveInfo.toY);
                    }
                    moves.clear();
                    mMovesList.remove(moves);
                }
            };
            if (removalsPending) {
                View view = moves.get(0).holder.itemView;
                ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
            } else {
                mover.run();
            }
        }
        ...
    }

至于animateRemoveImplanimateMoveImpl的源碼具體我就不貼了,直接說(shuō)一下它們做了什么操作吧:

  1. animateRemoveImpl 把這個(gè)被Remove的Item做一個(gè)透明度由(1~0)的動(dòng)畫(huà)
  2. animateMoveImpl把它們的TranslationXTranslationY移動(dòng)到0的位置。

我再貼一下刪除動(dòng)畫(huà)的gif, 你感受一下是不是這個(gè)執(zhí)行步驟:

RecyclerViewRemoveAnimation.gif

歡迎關(guān)注我的Android進(jìn)階計(jì)劃。看更多干貨

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容