RecyclerView動畫源碼淺析

本文是RecyclerView源碼分析系列第四篇文章,內容主要是基於前三篇文章來敘述的,所以在閱讀以前推薦看一下前3篇文章:java

RecylcerView的基本設計結構git

RecyclerView的刷新機制github

RecyclerView的複用機制bash

本文主要分析RecyclerView刪除動畫的實現原理,不一樣類型動畫的大致實現流程其實都是差很少的,因此對於添加、交換這種動畫就再也不作分析。本文主要目標是理解清楚RecyclerViewItem刪除動畫源碼實現邏輯。文章比較長。app

能夠經過下面這兩個方法觸發RecyclerView的刪除動畫:ide

//一個item的刪除動畫
    dataSource.removeAt(1)
    recyclerView.adapter.notifyItemRemoved(1)

    //多個item的刪除動畫
    dataSource.removeAt(1)
    dataSource.removeAt(1)
    recyclerView.adapter.notifyItemRangeRemoved(1,2)
複製代碼

下面這個圖是設置10倍動畫時長時刪除動畫的執行效果,能夠先預想一下這個動畫時大體能夠怎麼實現:源碼分析

接下來就結合前面幾篇文章的內容並跟隨源碼來一塊看一下RecyclerView是如何實現這個動畫的:佈局

adapter.notifyItemRemoved(1)會回調到RecyclerViewDataObserver:post

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

其實按照onItemRangeRemoved()這個方法能夠將Item刪除動畫分爲兩個部分:動畫

  1. 添加一個UpdateOpAdapterHelper.mPendingUpdates中。
  2. triggerUpdateProcessor()調用了requestLayout, 即觸發了RecyclerView的從新佈局。

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

AdapterHelper

這個類能夠理解爲是用來記錄adapter.notifyXXX動做的,即每個Operation(添加、刪除)都會在這個類中有一個對應記錄UpdateOpRecyclerView在佈局時會檢查這些UpdateOp,並作對應的操做。 mAdapterHelper.onItemRangeRemoved實際上是添加一個Remove UpdateOp:

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

即把一個Remove UpdateOp添加到了mPendingUpdates集合中。

RecyclerView.layout

RecyclerView的刷新機制中知道RecyclerView的佈局一共分爲3分步驟:dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3(),接下來咱們就分析這3步中有關Item刪除動畫 的工做。

dispatchLayoutStep1(保存動畫現場)

直接從dispatchLayoutStep1()開始看,這個方法是RecyclerView佈局的第一步:

dispatchLayoutStep1():

private void dispatchLayoutStep1() {
        ...
        processAdapterUpdatesAndSetAnimationFlags();
        ...
        if (mState.mRunSimpleAnimations) {
            ...
        }
        ...
    }
複製代碼

上面我只貼出了Item刪除動畫主要涉及到的部分, 先來看一下processAdapterUpdatesAndSetAnimationFlags()所觸發的操做,整個操做鏈比較長,就不一一跟了,它最終實際上是調用到AdapterHelper.postponeAndUpdateViewHolders():

private void postponeAndUpdateViewHolders(UpdateOp op) {
    mPostponedList.add(op); //op實際上是從mPendingUpdates中取出來的
    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;    
        ...
    }
}
複製代碼

即這個方法作的事情就是把mPendingUpdates中的UpdateOp添加到mPostponedList中,並回調根據op.cmd來回調mCallback,其實這個mCallback是回調到了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方法:主要是把當前顯示在界面上的ViewHolder的位置作對應的改變,即若是item位於刪除的item以後,那麼它的位置應該減一,好比原來的位置是3如今變成了2。

接下來繼續看dispatchLayoutStep1()中的操做:

if (mState.mRunSimpleAnimations) {
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            //根據當前的顯示在界面上的ViewHolder的佈局信息建立一個ItemHolderInfo
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPreLayoutInformation(mState, holder,
                            ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                            holder.getUnmodifiedPayloads());
            mViewInfoStore.addToPreLayout(holder, animationInfo); //把 holder對應的animationInfo保存到 mViewInfoStore中
            ...
        }
    }
複製代碼

即就作了兩件事:

  1. 爲當前顯示在界面上的每個ViewHolder建立一個ItemHolderInfoItemHolderInfo其實就是保存了當前顯示itemview的佈局的top、left等信息
  2. 拿着ViewHolder和其對應的ItemHolderInfo調用mViewInfoStore.addToPreLayout(holder, animationInfo)

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

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中。能夠理解爲它是用來保存動畫執行前當前界面ViewHolder的信息一個集合。

到這裏大體理完了在執行Items刪除動畫AdapterHelperdispatchLayoutStep1()的執行邏輯,這裏用一張圖來總結一下:

其實這些操做能夠簡單的理解爲保存動畫前View的現場 。其實這裏有一次預佈局,預佈局也是爲了保存動畫前的View信息,不過這裏就不講了。

dispatchLayoutStep2

這一步就是擺放當前adapter中剩餘的Item,在本文的例子中,就是依次擺放剩餘的5個Item。在前面的文章RecyclerView的刷新機制中,咱們知道LinearLayoutManager會向RecyclerView來填充RecyclerView,因此RecyclerView中填幾個View,其實和Recycler有很大的關係,由於Recycler不給LinearLayoutManager的話,RecyclerView中就不會有View填充。那RecyclerLinearLayoutManager``View的邊界條件是什麼呢? 咱們來看一下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(),那麼就不會再向RecyclerView中填充子View。而這個mState.getItemCount()通常就是adapter中當前數據源的數量。因此通過這一步佈局後,View的狀態以下圖:

這時你可能就有疑問了? 動畫呢? 怎麼直接成最終的模樣了?別急,這一步只不過是佈局,至於動畫是怎麼執行的咱們繼續往下看:

dispatchLayoutStep3(執行刪除動畫)

在上一步中對刪除操做已經完成了佈局,接下來dispatchLayoutStep3()就會作刪除動畫:

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        ...
        mViewInfoStore.process(mViewInfoProcessCallback); //觸發動畫的執行
    }
    ...
}
複製代碼

能夠看到主要涉及到動畫的是mViewInfoStore.process(), 其實這一步能夠分爲兩個操做:

  1. 先把Item View動畫前的起始狀態準備好
  2. 執行動畫使Item View到目標佈局位置

下面咱們來繼續跟一下mViewInfoStore.process()這個方法

Item View動畫前的起始狀態準備好

void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { //對mLayoutHolderMap中每個Holder執行動畫
            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);  //被刪除的那個item會回調到這個地方
            }else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);   //須要上移的item會回調到這個地方
            }  
            ...
            InfoRecord.recycle(record);
        }
    }
複製代碼

這一步就是遍歷mLayoutHolderMap對其中的每個ViewHolder作對應的動畫。這裏callback會調到了RecyclerView,RecyclerView會對每個Item執行相應的動畫:

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();
                }
            }
            ...
        }
}
複製代碼

先來分析被刪除那那個Item的消失動畫:

將Item的動畫消失動畫放入到mPendingRemovals待執行隊列

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上(這是由於在dispatchLayoutStep1dispatchLayoutStep2中已經對這個Holder作了Dettach)。即它又從新出如今了RecyclerView的佈局中(位置固然仍是未刪除前的位置)。而後調用了mItemAnimator.animateDisappearance()其執行這個刪除動畫,mItemAnimatorRecyclerView的動畫實現者,它對應的是DefaultItemAnimator。繼續看animateDisappearance()它其實最終調用到了DefaultItemAnimator.animateRemove():

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

即,其實並無執行動畫,而是把這個holder放入了mPendingRemovals集合中,看樣是要等下執行。

將未被刪除的Item的移動動畫放入到mPendingMoves待執行隊列

其實邏輯和上面差很少DefaultItemAnimator.animatePersistence():

public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
    if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {  //和預佈局的狀態不一樣,則執行move動畫
        return animateMove(viewHolder,preInfo.left, preInfo.top, postInfo.left, postInfo.top);
    }
    ...
}
複製代碼

animateMove的邏輯也很簡單,就是根據偏移構造了一個MoveInfo而後添加到mPendingMoves中,也沒有馬上執行:

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);  //設置他們的位置爲負偏移!!!!!
    }
    if (deltaY != 0) {
        view.setTranslationY(-deltaY);  //設置他們的位置爲負偏移!!!!!
    }
    mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
    return true;
}
複製代碼

但要注意這一步把要作滾動動畫的View的TranslationXTranslationY都設置負的被刪除的Item的高度,以下圖

即被刪除的Item以後的Item都下移了

postAnimationRunner()執行全部的pending動畫

上面一步操做已經把動畫前的狀態準備好了,postAnimationRunner()就是將上面pendding的動畫開始執行:

//DefaultItemAnimator.java

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

        if (!mPendingMoves.isEmpty()) { //執行pending的move動畫
            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的源碼具體我就不貼了,直接說一下它們作了什麼操做吧:

  1. animateRemoveImpl 把這個被Remove的Item作一個透明度由(1~0)的動畫
  2. animateMoveImpl把它們的TranslationXTranslationY移動到0的位置。

我再貼一下刪除動畫的gif, 你感覺一下是否是這個執行步驟:

歡迎關注個人Android進階計劃。看更多幹貨

相關文章
相關標籤/搜索