在RecyclerView中,我们要添加动画是非常简单的,我们可以继承SimpleItemAnimator
,实现内部的各个部分的动画细节,通过setItemAnimator
设置进去。RecyclerView内部也提供了自己继承SimpleItemAnimator
的DefaultItemAnimator
。如果我们不设置动画,那么就会使用这个默认的动画效果。 我们先介绍一下SimpleItemAnimator
,然后在这个基础上,研究RecyclerView
内部关于动画的实现。
动画业务层使用
业务层使用RecyclerView
动画有三层继承结构来供我们使用的。
graph TD
ItemAnimator --> SimpleItemAnimator
SimpleItemAnimator --> DefaultItemAnimator
SimpleItemAnimator --> 我们自己的动画实现
ItemAnimator
是一个抽象类,RecyclerView
内部操作这个类进行动画的操作,通过ItemHolderInfo
存储view的位置。内部有四个抽象方法需要我们实现。
抽象方法 | 意义 |
---|---|
animateAppearance | 从不可见到可见,对应addView或者划入屏幕 |
animatePersistence | 状态不变 |
animateDisappearance | 从可见到不可见,对应remvoe或者划出屏幕 |
animateChange | 对应update操作 |
看到这四个方法,可能和我们的想象中的动画实现不一样,我们一般都是通过局部刷新运行动画的,比如insert、remove、move等操作,如果只利用上面的这四个方法,很难知道对应了那儿些操作。所以SimpleItemAnimator就在这上面继续封装一层adapter,转换成我们熟悉的局部刷新的接口。相当于一层骨架实现类。
SimpleItemAnimator也是一个抽象类,抽象方法如下:
抽象方法 | 意义 |
---|---|
animateAdd | 对应notifyInsert |
animateChange | 对应notifyUpdate |
animateMove | 对应notifyMove |
animateRemove | 对应notifyMove |
看到上面的方法,我们应该比较熟悉了。正好对应了我们局部刷新的操作,我们自己实现每个变化部分的动画细节即可。
RecyclerView
内的DefaultItemAnimator,就是继承了SimpleItemAnimator,实现了默认的动画效果,需要实现自己的效果的可以参考DefaultItemAnimator。
上面就是业务层使用RecyclerView
动画的细节,还是比较容易理解的。
动画底层实现
前面的几个章节,都隐藏了关于动画的部分,因为动画的处理和正常的流程混在一次,会干扰我们对正常流程的分析。在测绘流程分析时,我们提到dispatchLayoutStep1()
和dispatchLayoutStep3()
主要处理动画的逻辑,dispatchLayoutStep2()
处理正常测绘的流程。
动画的操作部分时夹在在正常测绘两边的。对这整套流程,我们应该猜到什么,dispatchLayoutStep1()
主要记录正常测绘前的状态,经过dispatchLayoutStep2()
的正式测绘后,在dispatchLayoutStep3()
中根据测绘前的历史数据和最新的数据比较一下,根据比较的结果,进行动画的运行。大体的底层实现逻辑就是这样。下面具体分析设计的具体方法。
dispatchLayoutStep1
dispatchLayoutStep1的职责主要是记录正常测绘前的状态并初始化一下动画操作的各个参数。内部的逻辑先通过processAdapterUpdatesAndSetAnimationFlags()
方法处理动画和判断预测性动画是否需要进行。存储正式测绘前的数据,供测绘完成后使用。预测性动画会调用LayoutManager#onLayoutChildren()
进行提前的预布局。
private void processAdapterUpdatesAndSetAnimationFlags() {
。。。
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
}
复制代码
内部先处理局部刷新产生的UpdateOp
数据,predictiveItemAnimationsEnabled()主要判断是否支持预测性布局,如果支持就调用preProcess(),这个方法在局部刷新那篇里面讲到过。如果不支持就调用consumeUpdatesInOnePass()方法。这两个方法的主要区别有两点。
- preProcess()会进行局部请求数据的重新排序,把MOVE操作移动到最后
- preProcess()会把局部刷新数据
UpdateOp
放入mPostponedList
。 - preProcess()会把请求的数据拆分成两部分。显示区域和显示区域外的,屏幕内直接更新到Viewholder上和Recycler上。屏幕外的根据
mPostponedList
的数据进行处理,判断是否需要更新到Viewholder上和Recycler上。
具体的代码在上一篇局部刷新讲到过,可以具体看下。 consumeUpdatesInOnePass()内部直接处理了局部刷新的数据,更新到Viewholder上和Recycler上。
什么是预测性动画呢,预测是指对还没有发生的事情进行猜想,期望猜想的正确性,可能不正确。但是计算机会预测吗,预测也只是通过数据进行计算而已。
动画需要进行预测吗,可能有两种情况是需要的,
- 对显示的item进行动画,比如进行remove操作,会让其他item添加进来,这时候需要预测到新进入的item,并设置动画运动的动画。
- 多种动画叠加,互相产生影响,比如显示区域内的进行了remove,新显示进行了update操作,那么这个新进入的update也是需要动画的。
所以我们重写LayoutManager#supportsPredictiveItemAnimations()
关闭预测性动画,上面的效果就没有了,有兴趣的可以试验下。
最终计算的mRunSimpleAnimations和mRunPredictiveAnimations两个变量分别表示是否要运行动画和预测性动画。
摘录上方代码
private void processAdapterUpdatesAndSetAnimationFlags() {
。。。
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
。。。
}
复制代码
mRunSimpleAnimations的赋值true要满足以下情况:
- mFirstLayoutComplete为true
这个是在onLayout执行完成赋值的。也就是完成第一次布局。所以第一次进行布局是没有动画的。 - mItemAnimator为ture
说明我们要设置动画Animator,RecyclerView内部是有默认的动画 - mDataSetHasChangedAfterLayout判断
mDataSetHasChangedAfterLayout为true表示更改了数据集,比如调用setAdapter、swapAdapter、notifyDataSetChanged等。mDataSetHasChangedAfterLayout为false的情况下,animationTypeSupported表示需要支持的动画类型局部刷新的四种操作都会让animationTypeSupported变为true。mDataSetHasChangedAfterLayout为true的情况下,只有设置了hasStableIds()标记,才有动画。
我们调用了notifyDataSetChanged方法,一般认为是没有动画效果的,但是我们设置了hasStableIds(),是有动画效果的,这点需要注意。
摘录上方代码
private void processAdapterUpdatesAndSetAnimationFlags() {
。。。
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
。。。
}
private boolean predictiveItemAnimationsEnabled() {
return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
}
复制代码
mRunPredictiveAnimations的赋值true要满足以下情况:
- 上面的mRunSimpleAnimations为true,需要可以运行动画
- animationTypeSupported上面说过
- 没有更改数据集,也就是没有调用过setAdapter、swapAdapter、notifyDataSetChanged等
- LayoutManager需要supportsPredictiveItemAnimations()返回true。默认都是false。
执行完成processAdapterUpdatesAndSetAnimationFlags后,就通过这这两个值进行数据的存储,如果可以执行动画,就存储显示的ViewHolder的位置信息,如果执行预测性动画,就调用onLayoutChildren进行提前的布局。
先看下支持执行动画的代码
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
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);
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
}
复制代码
先遍历显示的viewHolder,通过recordPreLayoutInformation方法把这个viewHolder的位置信息放到ItemHolderInfo
内部,然后通过mViewInfoStore.addToPreLayout存储到mViewInfoStore中,设置成FLAG_PRE
状态,mViewInfoStore时动画的中枢,负责动画信息存储和执行。
mTrackOldChangeHolders表示这个viewHolder是否执行了change操作,也就是notityChanged。如果和change相关,那么就会mViewInfoStore.addToOldChangeHolders存储这个change的viewHolder。
这里简单介绍下ViewInfoStore类。他负责动画的信息存储,包括布局完成前后的新老位置信息。并通过这些新老信息执行动画。是动画执行的核心类。他内部存储的ViewHolder有四种状态
状态 | 意义 |
---|---|
FLAG_DISAPPEARED | 可见到不可见,比如remove操作 |
FLAG_APPEAR | 不可见到可见,remove操作后,新显示的item |
FLAG_PRE | 布局前的所有item会设置这个状态,表示属于老布局 |
FLAG_POST | 布局完成后的所有item会设置这个状态,表示属于新布局 |
再看下如果支持预测性布局的具体执行
if (mState.mRunPredictiveAnimations) {
。。。
mLayout.onLayoutChildren(mRecycler, mState);
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);
}
}
}
clearOldPositions();
} else {
clearOldPositions();
}
复制代码
内部的大体逻辑就是先进行onLayoutChildren进行预测性布局,他的目的上面我们也说到了。和正常的布局中调用onLayoutChildren有什么区别呢
- 首先是
mState.mInPreLayout
赋值为true,这是一个很重要的变量,之前的分析都把mState.mInPreLayout
为true的情况剔除了,他会标记这次布局是在进行预测性布局阶段,onLayoutChildren方法内部会执行很多独特的逻辑以支持预测性布局。比如在提取缓存时,从一二级取出检测合法性执行validateViewHolderForOffsetPosition方法时,如果去除的ViewHolder已经被remove了,那么合法性就取决于isPreLayout,也就是是是否在预测性布局阶段。
boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
if (holder.isRemoved()) {
return mState.isPreLayout();
}
复制代码
- 上面的processAdapterUpdatesAndSetAnimationFlags方法处理局部刷新数据时,如果支持预测性布局,就会填充
mPostponedList
集合。在提取缓存时,从四级缓存提取时,会通过mAdapterHelper.findPositionOffset方法通过mPostponedList
判断当前的position的最新位置。 - 从缓存的提取,会多一级
mChangedScrap
,这级缓存主要存储了notifyChanged的viewHolder。主要处理notifyChanged设计的ViewHolder。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
复制代码
boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
viewHolder.getUnmodifiedPayloads());
}
复制代码
@Override
DefaultItemAnimator内
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
@NonNull List<Object> payloads) {
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
复制代码
先看下填充的逻辑,可以看出填充mChangedScrap
必须是holder.isUpdated()。还有canReuseUpdatedViewHolder(holder)为false,这个方法返回false表示需要update的动画。我们看在RecyclerView的默认动画DefaultItemAnimator内,内部判断了我们传入的payloads是否为空。如果为空那么进行了update的viewHolder就会被缓存在mChangedScrap中,反之存储在一级缓存mAttachedScrap中。
在缓存提取的过程中,如果在预测性布局阶段,会首先从mChangedScrap
提取。提取完成使用后就会把mChangedScrap
清空,没有放入其他级别缓存。所以后面在正式布局时,从缓存中说是取不到这个viewHolder的,会重新创建一个ViewHolder,以支持change的动画,默认就是渐隐渐出了。
所以我们调用notifyItemChanged(int position, Object payload),payload传非null的payload,不会闪一下,只在DefaultItemAnimator才生效,也就是缓存的回收放入mAttachedScrap,可以进行复用。这时也是没有动画的。
上面的不同点,可能不太好理解,这里举一个例子,比如我们remove一个显示区域内位置为2的item,那么屏幕外的不可见item需要执行向上的动画显示出来,所以我们需要知道他的动画前后的位置,动画前的位置就是正常布局前的位置。怎么知道布局前的位置,这时他还没有显示出来呢,结合上面两个不同点就可以得出布局前的位置。在isPreLayout()为true时,从一二级缓存取出位置为2的viewHolder时,他肯定是isRemoved状态,因为processAdapterUpdatesAndSetAnimationFlags方法处理局部刷新数据时,会刷新ViewHolder的状态。这时她的可用性因为isPreLayout为true,所以是可用的。但是下面会设置mIgnoreConsumed变量为true,也就是不不占用空间,那么因为这个被remove的ViewHolder不占用空间,所以不可见的view也会被计算出来,这样就提前得到了下面那个没有显示的item的确切位置。可以通过首末位置进行动画了。
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
复制代码
通过上面的remove例子,大体知道了预测性布局onLayoutChildren的逻辑。其他的局部刷新逻辑也是类似的。
通过这样的预测性的onLayoutChildren重新布局后,我们就拿到实际布局前需要进行动画的老数据信息。下面应该进行存储。先通过mViewInfoStore.isInPreLayout(viewHolder)判断,在运行动画逻辑中,屏幕上的viewHolder都会进入这个isInPreLayout状态,所以这里只会存储新add到viewGroup的view了。结合上面的举例,应该比较清晰了。最后通过mViewInfoStore.addToAppearedInPreLayoutHolders
让ViewHolder进入Appeared
状态。也就是从不可见到可见。
通过上面的分析,通过支持动画的和支持预测性动画的执行过程,老数据的存储就完成了。下面就会执行dispatchLayoutStep2()
进行实际的布局了,具体流程可以看以前详细介绍的文章。 下面详细分析dispatchLayoutStep3()
,内部继续存储布局后的新数据,并使用了新老数据执行动画。
dispatchLayoutStep3
dispatchLayoutStep3方法内通过dispatchLayoutStep2的测绘完成后,拿到现有的最新位置。并最终调用ViewInfoStore#process运行动画。
if (mState.mRunSimpleAnimations) {
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()) {
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);
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);
}
}
mViewInfoStore.process(mViewInfoProcessCallback);
}
复制代码
上面的逻辑遍历最新的ViewHolder,和老数据进行对比,最终的结果会通过ViewInfoStore.addToPostLayout装在到ViewInfoStore中。这里直接处理了animateChange动画,其他的动画在ViewInfoStore.process统一处理的。详细的逻辑有兴趣的可以自己阅读。看下最终执行动画的process方法。
static final int FLAG_DISAPPEARED = 1;
static final int FLAG_APPEAR = 1 << 1;
static final int FLAG_PRE = 1 << 2;
static final int FLAG_POST = 1 << 3;
static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
void process(ProcessCallback callback) {
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
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) {
if (record.preInfo == null) {
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) {
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE) != 0) {
callback.processDisappeared(viewHolder, record.preInfo, null);
} else if ((record.flags & FLAG_POST) != 0) {
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);
}
}
复制代码
上面的逻辑比较有意思,执行到这里,测绘前后的数据都映射到了record.flags里面,这里设计到了位操作运算,每一位都代表一种状态。四种状态我们上面介绍ViewInfoStore
的时候也讲到过。通俗的讲就是初试结束状态和可见性变化的状态。这样不同的组合就代表了不同的意义。比如FLAG_PRE_AND_POST这个,表示这个ViewHolder在初试结束情况下都有,而且没有经历过可见行的变化,所以直接执行processPersistent。其他组合的状态也类似这种逻辑。我们业务中也可以借鉴这种方式进行状态的转移判断。
通过不同的判断,接下来通过ViewCompat.postOnAnimation
,执行mItemAnimator.runPendingAnimations()
执行动画。动画就这么被执行起来了。
动画的逻辑感兴趣的可以看DefaultItemAnimator
,这里就不分析了。