RecyclerView动画源码浅析
本文是
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倍动画时长时删除动画的执行效果,可以先料想一下这个动画时大致可以怎样实现:
RecyclerViewRemoveAnimation.gif
接下来就结合前面几篇文章的内容并跟随源码来一块看一下RecyclerView是如何实现这个动画的:
adapter.notifyItemRemoved(1)会回调到RecyclerViewDataObserver:
public void onItemRangeRemoved(int positionStart, int itemCount) { if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { triggerUpdateProcessor(); } }其实按照onItemRangeRemoved()这个的执行逻辑方法可以将Item删除动画分为两个部分:
- 增加一个
UpdateOp到AdapterHelper.mPendingUpdates中。 triggerUpdateProcessor()调用了requestLayout, 即触发了RecyclerView的重新布局。
先来看mAdapterHelper.onItemRangeRemoved(positionStart, itemCount):
AdapterHelper
这个类可以了解为是用来记录adapter.notifyXXX动作的,即每一个Operation(增加、删除)都会在这个类中有一个对应记录UpdateOp,RecyclerView在布局时会检查这些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中 ... } }即就做了两件事:
- 为当前显示在界面上的每一个
ViewHolder创立一个ItemHolderInfo,ItemHolderInfo其实就是保存了当前显示itemview的布局的top、left等信息 - 拿着
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删除动画时AdapterHelper和dispatchLayoutStep1()的执行逻辑,这里用一张图来总结一下:
Remove动画dispatchLayoutStep1.png
其实这些操作可以简单的了解为保存动画前View的现场 。其实这里有一次预布局,预布局也是为了保存动画前的View信息,不过这里就不讲了。
dispatchLayoutStep2
这一步就是摆放当前adapter中剩余的Item,在本文的例子中,就是依次摆放剩余的5个Item。在前面的文章RecyclerView的刷新机制中,我们知道LinearLayoutManager会向Recycler要View来填充RecyclerView,所以RecyclerView中填几个View,其实和Recycler有很大的关系,由于Recycler不给LinearLayoutManager的话,RecyclerView中就不会有View填充。那Recycler给LinearLayoutManager``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的状态如下图:
Remove动画布局.png
这时你可能就有疑问了? 动画呢? 怎样直接成最终的模样了?别急,这一步只不过是布局,至于动画是怎样执行的我们继续往下看:
dispatchLayoutStep3(执行删除动画)
在上一步中对删除操作已经完成了布局,接下来dispatchLayoutStep3()就会做删除动画:
private void dispatchLayoutStep3() { ... if (mState.mRunSimpleAnimations) { ... mViewInfoStore.process(mViewInfoProcessCallback); //触发动画的执行 } ...}可以看到主要涉及到动画的是mViewInfoStore.process(), 其实这一步可以分为两个操作:
- 先把
Item View动画前的起始状态准备好 - 执行动画使
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上(这是由于在dispatchLayoutStep1和dispatchLayoutStep2中已经对这个Holder做了Dettach)。即它又重新出现在了RecyclerView的布局中(位置当然还是未删除前的位置)。而后调用了mItemAnimator.animateDisappearance()其执行这个删除动画,mItemAnimator是RecyclerView的动画实现者,它对应的是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的TranslationX和TranslationY都设置负的被删除的Item的高度,如下图
DefaultItemAnimator.animateMove.png
即被删除的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(); } } ... }至于animateRemoveImpl和animateMoveImpl的源码具体我就不贴了,直接说一下它们做了什么操作吧:
animateRemoveImpl把这个被Remove的Item做一个透明度由(1~0)的动画animateMoveImpl把它们的TranslationX和TranslationY移动到0的位置。
我再贴一下删除动画的gif, 你感受一下是不是这个执行步骤:
RecyclerViewRemoveAnimation.gif
欢迎关注我的Android进阶计划。看更多干货
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » RecyclerView动画源码浅析