最近在看别人关于RecyclerView
的博客,恰巧看到一位国外大神的博客,是关于RecyclerView
动画的。我从那篇博客里面学习到了很多的东西,觉得有必要把它分享出来,所以我打算将它翻译成中文,不过还是建议大家去看英文原文。
原文链接:RecyclerView animations - AndroidDevSummit write-up,原文是需要翻墙的哦。
RecyclerView Animations and Behind the Scenes - 我们再来回到一个演示视频里面来。在所有移动平台上,有一个不争的事实,列表视图(list views)或者更一般的视图集合是最常见的视图模式,所以,尽可能了解他们的原理是非常重要的。
今天,我们来借助于Android Dev的教学视频,来仔细的了解一下RecyclerView
的ItemAnimator
1. Default Animations in RecyclerView
谷歌官方通过默认的几种操作,例如:adding
、removing
和changing
,为我们提供了如下的动画:
- 淡入和淡出(对应着adding操作和removing操作)
- 位移(把剩余的items移动到正确位置上)
- 同时渐变(淡入和淡出同时执行,对应着update特定item的操作)
官方为我们提供了RecyclerView.ItemAnimator
的一个子类--DefaultItemAnimator
实现了上面三种动画,同时DefaultItemAnimator
在RecyclerView
内部作为默认动画在使用。
如果你对DefaultItemAnimator
的原理感兴趣,推荐去看它的源码。源码只有600多行,非常的简洁。
简而言之,DefaultItemAnimator
的原理主要可以分为两步:
- 准备每个动画(每个操作可能会同时进行多个动画),然后放入即将执行的动画数组里面。
- 执行所有的动画。
基于下面的Demo,让我们看看delete的动画是怎么执行的。
(1 ~ 3步会以不同的顺序或者混合执行)
- 准备add动画(那些不可见但是即将出现在屏幕中的item,当然是必须保证有足够的空间)。
- 准备delete动画(被点击的item)
- 准备move动画(应该被移动到正确位置的items)
- 执行所有准备好的动画(这里面有很多的逻辑,比如,move动画执行完成之后,才执行位移动画--可以通过ViewCompat.postOnAnimationDelayed()了解得更多)。
2. PredictiveItemAnimations
从用户体验来看,PredictiveItemAnimations
确实比较简单,但是从实现原理来说的话,我们更应该关心在执行add或者remove操作之后,之前不可见但是此时应该显示的items和这些items执行着的动画。
我们必须明白的是,从技术上来讲,在RecyclerView
中,屏幕中可见的Item才算是实时存在的。因此,在这种情况下,也就是我们前面所说的操作(remove 操作),屏幕下方会有很多还没有移动的items。因为在此之前,他们还不存在。在这种情况下,那些在屏幕下方的items出现在屏幕时,会执行appearance的动画,在DefaultItemAnimator
中,就是淡入的动画。
再或者,我们可以知道的是,每个item从哪里来的。这里需要注意的是,RecyclerView.ItemAnimator
只负责,每个ItemView从初始状态到最终状态的动画过程,而每个ItemView的如何布局,则依然还是LinearLayoutManager
(或者RecyclerView.LayoutManager
)。
LayoutManager
有一个public的方法supportsPredictiveItemAnimations()
,这个方法默认返回为false。当LayoutManager
的条件允许时,这个方法会返回为true。当屏幕下方的items出现在屏幕时,如果supportsPredictiveItemAnimations()
返回为true,此时这些Item执行是位移动画,而不是appearance
动画。
在我们的代码中,我们是通过如下方式来设置是否开启的。
//MainActivity.java
LinearLayoutManager layoutManager = new LinearLayoutManager(this) {
@Override
public boolean supportsPredictiveItemAnimations() {
return cbPredictive.isChecked();
}
};
rvColors.setLayoutManager(layoutManager);
效果如下(仔细看,最后一个item是通过位移动画出现在屏幕当中的)
如果你对
supportsPredictiveItemAnimations()
方法感兴趣的话,或者你正在设计自己的LayoutManager
并且想要知道supportsPredictiveItemAnimations()
方法相关细节,最好的方式就是看官方文档:supportsPredictiveItemAnimations()。
3. Custom change Item animations
在DefaultItemAnimator
中, change
动画是执行cross-fade(同时渐变)动画。基于一个例子,我们实现了一个同时执行很多动画的change
动画。
通过上面的例子,我们可以知道,我们的item将两个itemView的text拧在了一起,并且background的颜色也从一个颜色渐变成了另一个颜色。我们是通过继承
DefaultItemAnimator
来实现的。
4. Items animation under the hood
change
动画是展现ItemAnimator
工作原理的最简单的方式。值得说一句的是,这里这个change
动画实际上跟add动画或者remove动画没有很大的区别。
这里有最终的实现--MyItemAnimator。
(1). Notify change
现在,让我们从头开始来看。我们的list views被展示出来,同时RecyclerView
使用自定义的动画--MyItemAnimator为了每个itemView做动画。
当我们点击一个itemView时,下面的代码会被执行:
//ColorsAdapter.java
public void changeItemAtPosition(int position) {
colors.set(position, ColorsHelper.getRandomColor());
notifyItemChanged(position);
}
从notifyItemChanged
方法开始(直接调用notifyItemChanged
方法,同时给出点击的position和被点击的item数量。),我们就会通知RecyclerView
--item应该被更新了。
(2). Record recent state
现在,Animators
通过调用 recordPreLayoutInformation(RecyclerView.State state, RecyclerView.ViewHolder viewHolder, int changeFlags, List payloads)(通过文档可以了解得更多)记录每个ItemView最近的状态值了。在这个方法里面,我们会访问被更新的View的ViewHolder
,同时还会保存一些状态值(尤其是做动画的那些属性)。
状态值保存在一个返回对象ItemHolderInfo
里面的。
这是我们的实现:
private class ColorTextInfo extends ItemHolderInfo {
int color;
String text;
//...
}
默认的ItemHolderInfo
保存着ItemView的边界信息,我们在里面额外的加了做动画的background color 和text。
recordPreLayoutInformation()
方法会收集显示的Views的信息,虽然有些ItemView没有被更新(在这种情况,ItemView
的changeFlags
被设置为ViewHolder.FLAG_BOUND
,等于0)。其它的flags表示正在请求某种操作(change
动画对应着值为2 - ViewHolder.FLAG_UPDATE
)。
此时效果如下(我怀疑大佬将gif上传成png了):
(3). Bind new view
此时,我们的Adapter
正在请求bind一个新的ViewHolder
,作为ItemView最新的数据。
@Override
public void onBindViewHolder(ColorViewHolder holder, int position) {
int color = colors.get(position);
holder.itemView.setBackgroundColor(color);
holder.tvColor.setText("#" + Integer.toHexString(color));
}
这就意味着,我们的ItemView将会被改变,效果如下:
(4). Record new state
现在我们回到ItemAnimator
。此时就会调用recordPostLayoutInformation(RecyclerView.State state, RecyclerView.ViewHolder viewHolder)方法来记录ItemView最终的状态值(通过文档可以了解得更多)。此时,我们会再一次的访问ItemView的ViewHolder
,但是此时的ItemView是新的ItemView,将重要的信息保存在ItemHolderInfo
(我们的ColorTextInfo
)。
(5). Play (or just prepare animation)
RecyclerView.ItemAnimator
有一系列的方法用于不同的动画操作,前面我们说的animateChange
方法就会被调用,同时这里也是准备动画绝佳时机。我们有如下4个参数来帮助我们完成动画的执行(或者准备):
RecyclerView.ViewHolder oldHolder
RecyclerView.ViewHolder newHolder
ItemHolderInfo preInfo
-
ItemHolderInfo postInfo
oldHolder
和newHolder
分别表示itemView布局前和布局后。在我们的case下,这俩是同一个对象,因为如下代码:
@Override
public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
return true;
}
这就意味着,对于动画,我们没有必要将ViewHolder
分离(因为,我们执行动画是基于ItemHolderInfo
)
preInfo
和postInfo
对象 分别来自于recordPreLayoutInformation()
方法和recordPostLayoutInformation()
方法。
正如你所知,我们的View已经bind了最终的状态,此时我们可以调用animateChange
方法,ItemView就从初始状态(保存在preInfo
)开始,准备动画和可选的执行动画到最终状态。
为什么强调可选呢?
animateChange()
方法返回的是一个布尔值,其中false表示动画已经被执行了,true表示动画正在准备,保存和等待执行。
在false的情形下,我们需要记得在执行最后调用dispatchAnimationFinished
方法来告诉RecyclerView
动画已经结束了。代码如下:
overallAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchAnimationFinished(newHolder);
}
});
这就告诉了RecyclerView
,此时这个ViewHolder
可以被复用了。
其他情况就是animateChange()
方法返回为true。
(6). Play all pending animations
这次更新,我们同时会执行多个动画(在add或者remove操作中,有位移动画、出现的动画或者消失的动画),在执行animate...()
方法时,会通过某一个方式来保存所有即将执行的动画(在DefaultItemAnimator
中,使用的ArrayList
),然后会调用runPendingAnimations
方法来执行所有准备好的动画。
这里还有其他的操作没有介绍(比如,cancel动画),这里我留给大家,让大家自己去探索。
整个执行流程以简短的方式来展示,效果如下:
5. Example app and source code
Chet Haase 和 Yigit Boyar 的视频有实现的代码: RecyclerView Animations and Behind the Scenes,这些不是谷歌官方的,只是比较相似而已。
本文没有介绍重复点击一个ItemView,会cancel之前的动画,然后执行新的动画这种情况,但是在视频里面有介绍。
6. Source code
本文完整的代码已经上传到github:https://github.com/frogermcs/RecyclerViewAnimations