距离上一篇RecyclerView
源码分析的文章已通过去了10多天,今天咱们未来看看RecyclerView
的动画机制。不过,本文不会分析ItemAnimator
相关的知识,而是理解RecyclerView
怎么执行ItemAnimator
的,有关ItemAniamtor
的知识,后面我会写专门的文章来分析。android
本文参考资料:git
注意,本文全部的代码都来自于27.1.1。github
RecyclerView
之因此受欢迎,有一部分的缘由得归功于它的动画机制。咱们能够经过RecyclerView
的setItemAnimator
方法来给每一个Item设置在不一样行为下,执行不一样的动画,很是的简单。尽管咱们知道怎么给RecyclerView
设置动画,可是RecyclerView
是怎么经过ItemAnimator
来给每一个Item实现动画,这里面的原理值得咱们去研究和学习。数组
在正式分析RecyclerView
的动画机制以前,咱们先对几个词语有一个概念,咱们来看看:bash
词语 | 含义 |
---|---|
Disappearance | 表示在动画以前,ItemView是可见的,动画以后就可不见了。这里的操做包括,remove操做和普通的滑动致使ItemView划出屏幕 |
Appearance | 表示动画以前,ItemView是不可见,动画以后就可见了。这里的操做包括,add操做和普通的滑动致使ItemView划入屏幕 |
Persistence | 表示动画先后,状态是不变的。这里面的操做包括,无任何操做 |
change | 表示动画先后,状态是不变的。这里面的操做包括,change操做。 |
还有注意的一点就是,ViewHolder
不是用来记录ItemView的位置信息,而是进行数据绑定的,因此在动画中,关于位置信息的记录不是依靠ViewHolder
来实现的,而是依靠一个叫ItemHolderInfo
的类实现的,在这个类里面,有四个成员变量,分别记录ItemView的left、top、right和bottom四个位置信息。app
最后还须要注意一点就是,咱们从RecyclerView
的三大流程中能够获得,在RecyclerView
的内部,dispatchLayout
分为三步,其中dispathchLayoutStep1
被称为预布局,在这里主要是保存ItemView
的OldViewHolder
,同时还会记录下每一个ItemView
在动画以前的位置信息;与之对应的dispathchLayoutStep3
被称为后布局,主要结合真正布局和预布局的相关信息来实现进行动画,固然前提是RecyclerView
自己支持动画。less
本文打算从两个角度来分析RecyclerView
的动画,一是从普通三大的流程来看,这是动画机制的核心所在;而是从Adapeter
的角度上来看,看看咱们每次在调用Adapter的notify相关方法以后,是怎么进行执行动画的(实际上也是回到三大流程里面)。 #1. 再来看RecyclerView的三大流程 取这个题目,我感受有特别的含义。首先,本次分析动画机制就是从新来看看三大流程,固然本次分三大流程确定没有以前的那么仔细,其次侧重点也不一样;其次,本次再来看RecyclerView
的三大流程,还能够填以前在分析RecyclerView
的三大流程留下的坑。ide
本次的分析重点在于dispathchLayoutStep1
和dispathchLayoutStep3
,不会分析完整的三大流程,因此,还有不懂RecyclerView
三大流程的同窗,能够参考个人文章:RecyclerView 源码分析(一) - RecyclerView的三大流程。oop
咱们先来看看dispatchLayoutStep1
方法:源码分析
private void dispatchLayoutStep1() {
// ······
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
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());
mViewInfoStore.addToPreLayout(holder, animationInfo);
if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()
&& !holder.shouldIgnore() && !holder.isInvalid()) {
long key = getChangedHolderKey(holder);
// This is NOT the only place where a ViewHolder is added to old change holders
// list. There is another case where:
// * A VH is currently hidden but not deleted
// * The hidden item is changed in the adapter
// * Layout manager decides to layout the item in the pre-Layout pass (step1)
// When this case is detected, RV will un-hide that view and add to the old
// change holders list.
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
if (mState.mRunPredictiveAnimations) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
// Save old positions so that LayoutManager can run its mapping logic.
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)) {
int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
boolean wasHidden = viewHolder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (!wasHidden) {
flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
}
final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
// 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; } 复制代码
我将dispatchLayoutStep1
方法分为2步(实际上从谷歌爸爸的注释,咱们也能够得出来)。
- 找到每一个没有被remove 掉的ItemView,将它的ViewHolder(OldViewHolder)放在
ViewInfoStore
里面,同时还将它预布局的位置放在ViewInfoStore
里面。这两个信息在后面作动画时都会用到。- 若是当前
RecyclerView
的LayoutManager
支持predictive item animations
(supportsPredictiveItemAnimations
方法返回true,我以为用英语描述这种动画挺好的,由于我不知道怎么翻译),会真正的进行预布局。在这一步,会先调用LayoutManager
的onLayoutChildren
进行一次布局,不过此次布局知识预布局,也就是说不是真正的布局,只是先肯定每一个ItemView
的位置。预布局以后,此时取到的每一个ItemView
的ViewHolder
和ItemHolderInfo
,即是每一个ItemView
的最终信息。
第二步的信息与第一步的信息相互呼应,第一步是变化前的信息,第二步是变化后的信息。这些都是为dispatchLayout3
阶段的动画作准备。其中,咱们发现相对于第一步,第二步变得复杂了不少。不过,咱们能够发现,无论怎么复杂,都是经过调用addToOldChangeHolders
方法来保存当前ItemView
的ViewHolder
(在LayoutManager
的onLayoutChildren
方法先后,在同一个位置上,不必定是同一个ItemView
,也不必定是同一个ViewHolder
),而后调用addXXXLayout
方法将位置信息(ItemHolderInfo)保存起来。
而后,咱们再来看看dispatchLayoutStep3
阶段:
private void dispatchLayoutStep3() {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.mLayoutStep = State.STEP_START;
// 将相关信息取到,而后添加到ViewInfoStore
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()) {
// run a change animation
// If an Item is CHANGED but the updated version is disappearing, it creates
// a conflicting case.
// Since a view that is marked as disappearing is likely to be going out of
// bounds, we run a change animation. Both views will be cleaned automatically
// once their animations finish.
// On the other hand, if it is the same view holder instance, we run a
// disappearing animation instead because we are not going to rebind the updated
// VH unless it is enforced by the layout manager.
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
if (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
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 {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// Step 4: Process view info lists and trigger animations
// 触发动画
mViewInfoStore.process(mViewInfoProcessCallback);
}
// 清理工做阶段
}
复制代码
我将上面的代码分为3阶段。
- 得到相关的位置信息(ItemHolderInfo),而后经过
addToPostLayout
方法将位置保存在ViewInfoStore
里面。- 调用
ViewInfoStore
的process
方法触发动画。- 进行相关的清理工做。
这里,咱们重点关注前两步就好了。
其中第一步很是容易理解,先是获到当前ItemView
的位置信息,保存在ViewInfoStore
里面。其中,咱们在这里发现,若是OldViewHolder不为空的话,会特别处理,为何会这样处理的呢?其实这里考虑到change操做,由于change操做会涉及到两个ItemView
的动画变化,因此,咱们发现,若是一个ItemView
调用的是animateChange
方法进行动画开始,而不是走通用的逻辑(将位置信息经过addToPostLayout
方法保存起来,而后调用process
方法进行统一的调用)。
而后就是第二步。咱们来看看ViewInfoStore
的process
方法,不过在咱们在这方法以前,咱们咱们先看看ProcessCallback
接口的几个方法。
方法 | 做用 |
---|---|
processDisappeared | 一个ItemView从可见到不可见会回调这个方法,主要是执行这种状况下的动画 |
processAppeared | 一个ItemView从不可见到可见会回调这个方法。 |
processPersistent | 一个ItemView动画先后状态为改变,这里面包括:自己未发生任何操做的ItemView、change操做的ItemView |
unused | 一个ItemView的变化不支持动画会回调此方法,这里包括好比一个ItemView先是Appeared而后disappeared,这种状况RecyclerView 找不到合适的动画;还有当前ItemView缺乏preInfo,也就是在预布局未记录位置信息,也会调用此方法,这种状况常常是ItemView进行remove操做,可是Adapter 调用的是notifyDataSetChanged 方法 |
如今,咱们正式的来看看process
方法:
void process(ProcessCallback callback) {
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
final 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) {
// Set as "disappeared" by the LayoutManager (addDisappearingView)
if (record.preInfo == null) {
// similar to appear disappear but happened between different layout passes.
// this can happen when the layout manager is using auto-measure
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) {
// Persistent in both passes. Animate persistence
callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE) != 0) {
// Was in pre-layout, never been added to post layout
callback.processDisappeared(viewHolder, record.preInfo, null);
} else if ((record.flags & FLAG_POST) != 0) {
// Was not in pre-layout, been added to post layout
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_APPEAR) != 0) {
// Scrap view. RecyclerView will handle removing/recycling this.
} else if (DEBUG) {
throw new IllegalStateException("record without any reasonable flag combination:/");
}
InfoRecord.recycle(record);
}
}
复制代码
其实process
方法很是简单,就是经过相关的flag来调用ProcessCallback
相关的方法。咱们如今来同一个看看ProcessCallback
的每一个方法都怎么实现的。
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) {
// 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(); } } @Override public void unused(ViewHolder viewHolder) { mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); } }; 复制代码
其实说到底,就是调用了animateXXX
方法来实现,而animateXXX
方法里面作了啥?其实没啥,就是调用了ViewCompat
的postOnAnimation
方法往任务队列后面post一个Runnable。代码以下:
void postAnimationRunner() {
if (!mPostedAnimatorRunner && mIsAttached) {
ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
mPostedAnimatorRunner = true;
}
}
复制代码
其中,上面的代码中,咱们须要注意的是,postAnimationRunner
每次只会被调用一次。那么若是在某一次操做中,会执行多个动画,怎么办呢?ProcessCallback
每一个回调方法都会调用animateXXX
方法,而animateXXX
方法会调用ItemAnimator
对应的方法,在ItemAnimator
里面,会将当前动画添加到一个数组里面,而后经过mItemAnimatorRunner
调用ItemAnimator
的runPendingAnimations
方法,runPendingAnimations
方法就是全部动画开始的起点。这里,咱们就不讨论ItemAnimator
内部的实现,后面有专门的文章来分析它。
咱们知道,调用Adapte
r的notifyDataSetChanged
方法,RecyclerView
是不会执行动画的;而调用notifyItemRemoved
之类的方法是有动画,这里咱们从Adapter
的角度来分析动画。跟ItemAnimator
同样,这里咱们也不会去分析Adapter
,后面会有专门的文章分析它。
在分析Adapter
以前,咱们先来看一个东西,就是RecyclerView
和Adapter
怎么进行通讯。
咱们思考这个问题以前,首先应该排除Addapter
和RecyclerView
是强耦合的,也就是说,Adapter
内部持有一个RecyclerView
对象。RecyclerView
自己就是插拔式设计,若是Adapter
和RecyclerView
是强耦合,就违背了插拔式的设计思想。那么它俩到底是怎么进行通讯的呢?答案已经很是的明显了,二者是经过观察者模式来进行通讯。
这其中,Adapter
做为被观察者,RecyclerView
做为观察者,当Adapter
的数据发生改变时,会通知它的每一个观察者。
RecyclerView
自己设计又比较特殊,RecyclerView
没有去实现Observer
(这里暂且这么称呼)接口,而是内部持有一个Observer
(RecyclerViewDataObserver
)对象,进而监听Adapter
的状态变化;固然Adapter
也是如此,并无去实现Observable
接口,也是在内部持有一个Observable
(AdapterDataObservable
)对象。
咱们来看Adapter
的notify
方法跟Observer
的方法是怎么进行对应的。
Adapter的notify方法 | 与之对应的Observer的方法 |
---|---|
notifyItemRemoved | notifyItemRangeRemoved |
notifyItemChanged | notifyItemRangeChanged |
notifyItemInserted | notifyItemRangeInserted |
notifyItemMoved | notifyItemMoved |
调用到Observer
的方法时,Observer
会调用AdapterHelper
相关的方法,在AdapterHelper
内部会为每一个操做建立一个UpdateOp
对象,而且添加到一个PendingUpdate
数组。咱们来看看相关代码(以add为例):
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
复制代码
若是onItemRangeInserted
返回为true,就调用triggerUpdateProcessor
方法。为何这里须要判断是否调用triggerUpdateProcessor
方法,实际上是为了不屡次调用,好比一个操做,可能会致使多种动画执行,因此这里保证triggerUpdateProcessor
方法只会被调用一次。
而后,咱们来看看triggerUpdateProcessor
方法:
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
复制代码
其实不论是if的执行语句,仍是else里面,最终仍是调用了requestLayout
方法,从新走一遍三大流程。
可见而知,RecyclerView
的三大流程到底多么重要。此次,咱们看三大流程中的dispatchLayoutStep2
方法。咱们知道,在Observer
阶段,每一个操做其实都建立了一个UpdateOp
对象,添加到PendingUpdate
数组。那么数组里面的操做都是何时执行的呢?其实就是在dispatchLayoutStep2
方法阶段:
private void dispatchLayoutStep2() {
// ······
mAdapterHelper.consumeUpdatesInOnePass();
// ······
}
复制代码
真正执行PendingUpdate
的操做是在AdapterHelper
的consumeUpdatesInOnePass
方法里面,咱们来瞧瞧:
void consumeUpdatesInOnePass() {
// we still consume postponed updates (if there is) in case there was a pre-process call
// w/o a matching consumePostponedUpdates.
consumePostponedUpdates();
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.onDispatchSecondPass(op);
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
case UpdateOp.MOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
recycleUpdateOpsAndClearList(mPendingUpdates);
mExistingUpdateTypes = 0;
}
复制代码
虽然代码很多,可是咱们发现了,最终的操做都是调用到了Callback
接口里面了。而Callback
作了什么呢?主要是作了两件事:
- 可能会更新一些ViewHolder的position
- 会更新一些ViewHolder的flag,好比说,remove的flag或者update的flag。
这部分的内容,咱们后面分析Adapter
会详细的分析,本文就不作过多的介绍了。
到这里,每一个ViewHolder
的position都更新完毕,而且每一个ViewHolder
的flag也已经更新完毕。这样,到了dispatchLayoutStep3
阶段,就知道每一个ViewHolder
应该作什么动画。
而后,咱们来看看为何调用Adapter
的notifyDataSetChanged
方法不执行动画呢?
notifyDataSetChanged
方法会回调到Observer
的notifyChanged
方法里面,咱们看看notifyChanged
方法干什么:
@Override
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
复制代码
在这个方法里面,咱们须要特别关注processDataSetCompletelyChanged
方法。咱们来看看:
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
mDispatchItemsChangedEvent |= dispatchItemsChanged;
mDataSetHasChangedAfterLayout = true;
markKnownViewsInvalid();
}
复制代码
在processDataSetCompletelyChanged
方法里面,调用了markKnownViewsInvalid
方法全部的ViewHolder
标记为了FLAG_INVALID
。这个操做直接致使了,咱们在预布局阶段不能正确得到每一个ItemView
的位置信息和OldViewHolder
,进而致使在后布局阶段不能执行动画。这就是notifyDataSetChanged
方法为何不执行动画的缘由。
RecyclerView
的动画机制仍是比较简单的,这里咱们对它作一个简单的总结。
RecyclerView
执行动画的机制在于,在预布局阶段将每一个ItemView
的位置信息和ViewHolder
保存起来,在后布局阶段,根据每一个ItemView
的ViewHolder
flag状态来判断执行什么动画,根据位置信息来判断怎么作动画。- Adapter的notify方法之因此可以执行动画,是由于他们在三大流程中给每一个
ViewHolder
打了响应的flag,包括remove的flag或者update的flag等。而在后布局中,正是根据flag来执行不一样的动画的。notifyDataSetChanged
方法之因此不支持动画,那是由于notifyDataSetChanged
方法会使每一个ViewHolder
失效(打了FLAG_INVALID
标记),因此致使在预布局阶段,不能正确的得到每一个ItemView
的位置信息和ViewHolder
,进而致使动画不能执行。
若是不出意外的话,下一篇文章将分析Adapter
。