RecyclerView動畫原始碼淺析

susion發表於2018-12-19

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

RecylcerView的基本設計結構

RecyclerView的重新整理機制

RecyclerView的複用機制

本文主要分析RecyclerView刪除動畫的實現原理,不同型別動畫的大體實現流程其實都是差不多的,所以對於新增、交換這種動畫就不再做分析。本文主要目標是理解清楚RecyclerViewItem刪除動畫原始碼實現邏輯。文章比較長。

可以通過下面這兩個方法觸發RecyclerView的刪除動畫:

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

下面這個圖是設定10倍動畫時長時刪除動畫的執行效果,可以先預想一下這個動畫時大致可以怎麼實現:

RecyclerView動畫原始碼淺析

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

adapter.notifyItemRemoved(1)會回撥到RecyclerViewDataObserver:

    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()的執行邏輯,這裡用一張圖來總結一下:

RecyclerView動畫原始碼淺析

其實這些操作可以簡單的理解為儲存動畫前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的狀態如下圖:

RecyclerView動畫原始碼淺析

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

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的高度,如下圖

RecyclerView動畫原始碼淺析

即被刪除的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, 你感受一下是不是這個執行步驟:

RecyclerView動畫原始碼淺析

歡迎關注我的Android進階計劃。看更多幹貨

來源:https://juejin.im/post/5c19f8f66fb9a049b347edc5

相關文章