“Recycler Item是有动画的!” 笔者被扇了一巴掌,“你这特x不是废话吗,我是想知道Item动画怎么做!”
hahahaha…
皮一下很开心。怎么做呢,其实根据不同的效果,可以选择难度不同的实现方式,本篇文章就是讲这个的!为了有更直观的感受,笔者用RecyclerView做了个聊天场景的demo:
需要源码的同学,请转到 github上下载
目录
- RecyclerView是有动画的
- 最简单的Item动画
- 深入定制
一、RecyclerView是有动画的
尼采说,你要理解一样东西,必须先证明其存在。RecyclerView就算什么也不做其实也是有一个默认的ItemAnimator的,只是平常不会发现而已。要明显的感觉出来,不妨把动画的时间拉长。
//配置为默认动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//设置添加、移除时间为1s
mRecyclerView.getItemAnimator().setAddDuration(1000);
mRecyclerView.getItemAnimator().setRemoveDuration(1000);
我们把添加和移除动画各设置为1s,感觉到了吗,分别是逐渐消失和显现的动画。
本篇博文重点不在于属性动画的使用方式,但不可避免地需要使用属性动画。文章中涉及的动画类为ViewPropertyAnimator,链式调用极其简单,相信谁都可以看明白的。
比如,我要把一个view
1秒内x轴360度式丢出100米远:
view.animate().setDuration(1000)//时间
.rotation(360)//旋转360度
.translationX(100)//X轴位移
.start();
简单吧,那下面就是笔者的干货了。
二、最简单的Item动画
如果你已经在代码中有一定时间的浸淫一段时间了,那你一定听过一句古话:不会用轮子的老司机不是好的程序员。对的!这里会推荐一个强大又简单的轮子,github - RecyclerView Animators。
本节的内容是基于此的,如果你断言它不足以满足需求,可以直接跳到三、深入定制
。
2.0 依赖
如果因为android.x报错,可以选择依赖旧版本,如下
implementation 'jp.wasabeef:recyclerview-animators:2.3.0'
2.1 如何使用
轮子提供了一系列的Item动画,分别是Scale(缩放)、Fade (渐变)、Flip(翻转)、Slide(滑动)。详见github链接,下翻到【Animators】
例如Slide就有:
SlideInLeftAnimator
, SlideInRightAnimator
, OvershootInLeftAnimator
, OvershootInRightAnimator
SlideInUpAnimator
,SlideInDownAnimator
可以望文生义了,SlideInUpAnimator就是向上滑动出现的意思,我们来试一试:
//设置上浮动画
mRecyclerView.setItemAnimator(new SlideInUpAnimator());
//时长设置500ms,便于观察
mRecyclerView.getItemAnimator().setAddDuration(500);
mRecyclerView.getItemAnimator().setRemoveDuration(500);
注意,所有的Item动画,必须使用局部刷新来显示,即
notifyItemChanged(int)
notifyItemInserted(int)
notifyItemRemoved(int)
notifyItemRangeChanged(int, int)
notifyItemRangeInserted(int, int)
notifyItemRangeRemoved(int, int)
简单的尝试,已经可以看到效果了
2.2 定制动画
上面的效果是有缺陷的,Robot在撤回时,下滑消失的效果明显是不合适的。怎么办?工程师是要解决一切问题的!现在,我们需要定制动画了。
来做一个360度上丢出现的动画吧~
同样需要用到轮子,我们继承于BaseItemAnimator
,这个是上面提到的SlideInUpAnimator
等动画的父类,选择实现其中 Remove/移除、Move/移动、Add/添加、Change/改变 的动画方法就可以定制了。
Remove动画定制:
@Override
protected void preAnimateRemoveImpl(RecyclerView.ViewHolder holder) {
super.preAnimateRemoveImpl(holder);
//实现移除动画前的处理
}
@Override
protected void animateRemoveImpl(RecyclerView.ViewHolder holder) {
//执行移除动画
holder.itemView.animate()
.alpha(0f)//逐渐透明
.setDuration(getRemoveDuration())//使用默认的移除时间
.start();
}
我们并不需要在移除前做任何处理,所以在preAnimateRemoveImpl(…)方法中没做任何事情,在animateRemoveImpl(Holder holder)方法实现移除动画:item逐渐透明。
Add动画定制:
@Override
protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) {
super.preAnimateAddImpl(holder);
//添加动画前调用
//提前设置item的Y轴偏移,透明度为0。
holder.itemView.setTranslationY(translationY);
holder.itemView.setAlpha(0f);
}
@Override
protected void animateAddImpl(RecyclerView.ViewHolder holder) {
//执行添加动画
holder.itemView.animate()
.setDuration(getAddDuration())
.translationY(0)//回到原位置
.rotation(360)//旋转360度
.alpha(1f)//透明度显现
.start();
}
这段代码有一点长,但同样简单。我们在添加Item前,提前设置了Y轴偏移量,和透明度为0,这样在执行动画时,通过位置和透明度恢复,就可以得到从下往上显现的效果了。 当然,说好的360度旋转进场是必不可少的 ( ˙˘˙ )
辛苦了这么久,看下效果吧
三、深入定制
前面我们已经解决了Item动画使用和简单定制的问题,相信你已经可以自行设计出漂亮的动画了。但是!可能会有超出你设想的情况。上栗子:
对话开始了!笔者在每条消息后, 滑动到最后一项(空白Foot)以保证显示。看出问题了吗?动画并不是连贯的,Item是先进行了位移,再进行了添加动画。 这对我们 吹毛求疵(误) 注重体验的程序员是不允许的,笔者想以此为引子,谈谈深入定制Item动画的思路。
难度加大警告
我们需要追本溯源,去实现更原始的动画类了。第一节的DefaultItemAnimator
、第二节的BaseItemAnimator
都是继承于SimpleItemAnimator
。我们来看看SimpleItemAnimator需要重写的方法:
有些多,重写这么多方法,完全这绝不是笔者的本意。建议大家继承SimpleItemAnimator的时候若非必要,可直接复制
BaseItemAnimator
或者DefaultItemAnimator
代码 。
有一些区别:
BaseItemAnimator
必须重新实现 animateRemoveImpl()/移除 和 animateAddImpl()/添加 这两个抽象方法DefaultItemAnimator
需要修改少量final错误,无动画执行前/Prexxx方法。
请酌情使用那种方式按图索骥。
复制粘贴的小窍门
- 先拷贝到如
Notepad++
等编辑器,再拷贝到AS中,可以避免自动切换对象所属类的麻烦- AS默认快捷键
Ctrl
+R
,可以进行文本内替换
3.0 实现动画的方式是一样的
复制BaseItemAnimator
或者DefaultItemAnimator
代码来继承SimpleItemAnimator,并没有改变定制动画的方式。仍可以找到以下方法:
//移除动画
private void animateRemoveImpl(final ViewHolder holder) {..}
//添加动画
void animateAddImpl(final ViewHolder holder){..}
//位移动画
void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY){..}
//改变动画
void animateChangeImpl(final CustomAnimator.ChangeInfo changeInfo){..}
按需修改里面的内容,去实现动画效果。
3.1 打破Move和Add的时间壁垒
之前的动画实现方式,默认Move/滑动动画完成后才会进行Add/添加动画。为解决这个问题,笔者将带大家看复制代码中真正安排执行动画的方法:runPendingAnimations()
。
runPendingAnimations()
中有四段,依次处理执行remove、move、change、add。四段大同小异,我们来看Add/添加部分:
...
// Next, add stuff
if (additionsPending) {
....
//addAnimator执行的runnable
Runnable adder = new Runnable() {
public void run() {
boolean removed = mAdditionsList.remove(additions);
//已取消
if (!removed) {
// already canceled
return;
}
//执行所有存储的添加动画
for (ViewHolder holder : additions) {
doAnimateAdd(holder);
}
additions.clear();
}
};
//remove、move、change,有任何一种动画正在执行,则延迟到其完成
if (removalsPending || movesPending || changesPending) {
//计算其他动画的时间和总延时
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
//延迟并执行添加动画的runnable
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
//没有其余动画,直接执行runnable
adder.run();
}
}
代码很长,但工程师是要迎难而上的!仔细看看,分两个部分:
- 创建的
Runnable adder
中放入Add动画开始的逻辑 - 判断是否有其他动画,并在其他动画完成后再执行
Runnable adder
好了,那笔者知道怎么改,使move和add动画同时开始了。
long moveDuration = 0;
由于是上浮缓慢显现,笔者还 在此方法中Move动画,和滑至最后一项做了延迟,使其平滑与Add动画衔接。 这里没有贴出来,最终效果见文末。
~~,多说一句,runPendingAnimations()
是一切Item动画的开始,如果你对需要实现的效果还没什么思路,可以从runPendingAnimations()
往下跟逻辑。
3.2 动画试图超出RecyclerView范围
一鼓作气,我们把仅剩的瑕疵也看了。
如果是因为其他图片遮挡,导致滑动或动画时如上图。怎么解决呢,抱歉的是 Item动画是不能超过RecyclerView范围去实现的。
我们可以扩大Recycler的范围,并使用透明渐变蒙层的方式去解决。(请参考另一篇文章:逐渐消失的Item! —— RecyclerView隐藏与渐变的实现)
谢谢大家,我们看下最终的效果吧
四、写在最后
好了,Robot受到了惩罚,我们也学到了不少东西。笔者可能会有疏漏的地方,但如果有一些帮助的话,点个赞鼓励下吧 ~。