AnimationSet 顺序执行动画的问题
有个需求是要做的一个TextView弹跳三次的动画,基本上就是六个参数不同的位移动画,比较简单,所以就懒得用属性动画,准备直接用xml写一个Animation实现了。
于是在xml文件里面是这么写的:第一个translate动画从原位置(0%)移动到上面一定高度(-40%),然后再从这个高度(-40%)移动到原位置(0%),以此类推,然后用startOffset参数控制动画顺序。
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="0%"
android:toYDelta="-40%"
android:duration="350" />
<translate
android:fromYDelta="-40%"
android:toYDelta="0%"
android:startOffset="350"
android:duration="200" />
...
</set>
然后就发现悲剧了,动画播放的时候TextView总会突然跳上去,然后继续向上运动,动画全部完成后又再跳回来。
研究了一下,发现是掉入fillEnabled和fillBefore的坑中了:
- fill开头的属性总共有三个:fillEnabled、fillBefore和fillAfter
- 其中fillAfter属性是独立的、与其它两个属性无关,表示动画结束以后是否将动画的最后一帧应用到View上
- fillAfter默认为false,即回到原处,上面的例子里没有涉及这个属性
- fillEnabled属性是fillBefore属性的开关,fillBefore属性表示动画执行的第一帧是否应用到这个View
- 默认fillEnabled和fillBefore都为true
- 这两个属性的逻辑比较绕,是因为fillEnabled属性是Google为了解决一些问题后续版本才加上去的
在上面的例子中,虽然我是想让这几个translate动画顺序执行,设置了不同的startOffset,但是AnimationSet在加载并判断是否应该fillBefore时,却并没有考虑startOffset。
这就导致了一个问题,fillBefore默认为true,那么在试图startAnimation时,系统会将AnimationSet中的每一个动画的初始值都设置到这个View上,相同状态后面的会覆盖前面的,在上面这个代码的三次弹跳(6个translate动画)中,最后一次恰好是从上到下,即初始状态在上。所以整个动画刚开始的时候这个TextView会向上跳一下,然后再向上运动,这就与我们所期望的效果不一致了
明白了问题的原因,就有办法解决了。
最直观的解决办法就是使用AnimationListener,在一个动画播放结束以后再播放下一个动画,避免后面的动画被提前fillBefore。但显然,这个方法需要写大量的Java代码,太过啰嗦,多了几个内部类对方法数也有影响,不够优雅。
第二个办法就是利用AnimationSet播放时整个动画播放完毕才会判断是否fillAfter的特性,每个动画都只以自身为原点做相对运动,保持初始状态均为0,即:
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="0%"
android:toYDelta="-40%"
android:duration="350" />
<translate
android:fromYDelta="0%"
android:toYDelta="40%"
android:startOffset="350"
android:duration="200" />
<translate
android:fromYDelta="0%"
android:toYDelta="-25%"
android:startOffset="550"
android:duration="300" />
<translate
android:fromYDelta="0%"
android:toYDelta="25%"
android:startOffset="850"
android:duration="200" />
<translate
android:fromYDelta="0%"
android:toYDelta="-8%"
android:startOffset="1050"
android:duration="200" />
<translate
android:fromYDelta="0%"
android:toYDelta="8%"
android:startOffset="1250"
android:duration="150" />
</set>
其中,fromYDelta默认为零,可以省去。运行一下,效果完美。
Interpolator被覆盖的问题
需求中的另一个要求,就是这个弹跳动画要用贝塞尔曲线进行插值,
因为上弹和下落的贝塞尔曲线插值器的参数不同,不能在set中统一设置,需要单独设置:
mAnimSet = AnimationUtils.loadAnimation(getContext(), R.anim.textview_jump);
if (mAnimSet instanceof AnimationSet) {
Interpolator upInterpolator = PathInterpolatorCompat.create(xx, xx, xx, xx);
Interpolator downInterpolator = PathInterpolatorCompat.create(xx, xx, xx, xx);
boolean even = true;
for (Animation anim : ((AnimationSet) mAnimSet).getAnimations()) {
anim.setInterpolator(even ? upInterpolator : downInterpolator);
even = !even;
}
}
如上,我在代码里对这个AnimationSet的每一个子Animation设置插值器后,发现并没有任何变化,感觉仍然是默认的加速减速插值器,效果很差。
研究了好一会,打了断点、跟踪了一下这个AnimationSet的执行过程,才发现问题的所在:
AnimationSet是Animation的子类,意味着AnimationSet本身就是一个Animation,也有自己的Interpolator插值器。而同时AnimationSet有一个名为PROPERTY_SHARE_INTERPOLATOR_MASK的flag,即是否分享(覆盖)自己的插值器到所有子Animation中。
默认情况下这个值为true,即AnimationSet中所有子Animation的Interpolator是默认被覆盖了的,在xml文件和Java代码中对子动画的插值器进行设置,都不会起任何作用!
要分别设置插值器的话,其实只需要在xml文件的set中加入一句:「android:shareInterpolator=”false”」即可,这一点确实有点坑啊……