属性动画 Sun, May 22, 2022 前段时间面试滴滴的时候,跟面试官聊了一下属性动画,上来就问:你看过源码吗?我说没看过,然后他表示属性动画并不是真正的改变view的属性,原因是一个例子:在constraintlayout中,通过相互约束定义纵向的一列view,然后通过属性动画,让第一个view平移,你认为下面的view是否会跟着动?他的结论是从系统设计上,谷歌就不可能允许跟着动的情况,因为我们指定动画的目标就是第一个view,所以平移操作也应该局限在第一个view中,而不应该影响其他的view。我当时觉得他太牛逼了,能从源码想到设计思想,后面我还靠他这个结论去忽悠过其他人,而其他人也觉得我很牛逼,哈哈哈。今天心血来潮,自己写个demo测一下,通过属性动画去改view的x位置,结果下面的view真的没有跟着动,可是接下来的表现却让我瞠目结舌。
使用方式 ObjectAnimator & ValueAnimator 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MainActivity : AppCompatActivity () {
private lateinit var target : Button
private lateinit var objectAni : ObjectAnimator
private lateinit var valueAni : ValueAnimator
private var type = 0
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
target = findViewById ( R . id . btn_target )
objectAni = ObjectAnimator . ofFloat ( target , "alpha" , 1f , 0f )
valueAni = ValueAnimator . ofFloat ( 1f , 0f ). apply {
duration = 500
startDelay = 1000
repeatCount = 2
repeatMode = ValueAnimator . REVERSE
addUpdateListener {
val currentValue = it . animatedValue as Float
target . alpha = currentValue
target . invalidate ()
}
setTarget ( target )
}
}
fun objectStart ( v : View ) {
objectAni . start ()
type = 1
}
fun valueStart ( v : View ) {
valueAni . start ()
type = 2
}
/**
* 反转
*/
fun reset ( v : View ) {
when ( type ) {
1 -> objectAni . reverse ()
2 -> valueAni . reverse ()
}
}
}
以上就是属性动画的用法,期中的ObjectAnimator
是ValueAnimator
的父类,而由于后者更加灵活,可复用程度更高,所以一般我们常用ValueAnimator
。而ObjectAnimator
呢,需要在创建时通过参数传递属性的名称,那不用猜也知道他用了运行时反射,而反射会带来更多的资源消耗,所以拉倒吧!
组合动画AnimatorSet after(Animator anim) :在参数动画执行完之后再执行 after(long delay) :参数动画执行完之后延迟执行 before(Animator anim): 插入到参数动画之前执行 with(Animator anim) :与参数动画同时执行 1
2
AnimatorSet aniset = new AnimatorSet ();
aniset . play ( ani2 ). with ( ani3 ). before ( ani1 ). after ( ani0 );
插值器与估值器 插值器和估值器用来定义动画执行时的矢量动态,如加速度、重力、阻力、阻尼等。
eg:
1
2
3
4
ObjectAnimator anim = ObjectAnimator . ofFloat ( mButton , "rotation" , 0.0f , 360.0f );
anim . setDuration ( 5000 );
anim . setInterpolator ( new DecelerateAccelerateInterpolator ());
anim . start ();
插值器:时间矢量 插值器(Interpolator)用于定义动画随时间流逝的变化规律。
默认插值器 插值器 表现 AccelerateDecelerateInterpolator 先加速,后减速 LinearInterpolator 线性插值器,动画匀速运行 AccelerateInterpolator 加速插值器,动画加速运行至结束 DecelerateInterpolator 减速插值器,动画减速运行至结束 OvershootInterpolator 弹簧插值器,快速完成动画,超出终点一小部分后再回到终点 AnticipateInterpolator 发条插值器,先后退一小步再加速前进至结束 AnticipateOvershootInterpolator 板簧插值器,先后退一小步再加速前进,超出终点一小部分后再回到终点 BounceInterpolator 弹性插值器,在动画结束之前会有一个弹性动画的效果 CycleInterpolator 周期运动
自定义插值器 需要实现接口:
1
2
3
4
5
6
7
public interface TimeInterpolator {
/**
* 回调参数是动画执行时间的百分比
*/
float getInterpolation ( float input );
}
估值器:起始矢量 估值器(TypeEvaluator)的作用是定义从初始值过渡到结束值的计算规则。
自定义估值器 需要实现接口:
1
2
3
public interface TypeEvaluator < T > {
public T evaluate ( float fraction , T startValue , T endValue );
}
参数fraction
表示动画的完成度,实际上就是插值器中getInterpolation()
方法的返回值。最后的返回值是在『当前完成度』这个条件之下的计算结果值。
XML方式 上面提到ObjectAnimator
是靠反射的方式获取到View的属性,那么自然可以想到通过xml文件声明,然后同样靠反射获取到View的属性再赋值,于是有了通过定义xml文件的方式来玩动画的方案:
在res
资源目录下创建一个xml文件:
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android= "http://schemas.android.com/apk/res/android" >
<animator android:duration= "100"
android:repeatMode= "reverse" />
</set>
然后就可以填充动画了:
1
val animator = AnimatorInflater . loadAnimator ( this , R . animator . my_ani )
源码解读 ValueAnimator 从ofFloat方法入手 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 注意这个
PropertyValuesHolder [] mValues ;
// 创建对象并赋值。
public static ValueAnimator ofFloat ( float ... values ) {
ValueAnimator anim = new ValueAnimator ();
anim . setFloatValues ( values );
return anim ;
}
// 赋值操作
public void setFloatValues ( float ... values ) {
if ( values == null || values . length == 0 ) {
return ;
}
if ( mValues == null || mValues . length == 0 ) {
setValues ( PropertyValuesHolder . ofFloat ( "" , values ));
} else {
PropertyValuesHolder valuesHolder = mValues [ 0 ];
valuesHolder . setFloatValues ( values );
}
// New property/values/target should cause re-initialization prior to starting
mInitialized = false ;
}
通过内存储存mValues
这个数组的类型,和代码中赋值的方式,我们可以发现他用了一个类似RecyclerView的holder
的方式做缓存和复用。
这里感觉要去复习一下View的相关知识才能继续写下去了,包括对View的一些新的理解,还有安卓整体的布局体系。
摘要的结论 我通过属性动画,去改变view.translateX,不论是调用invalidate()
还是requestLayout()
,都只有targetView自己产生了平移; 而当我改为修改LayoutParams的marginStart
后再requestLayout()
,下面的所有View都跟着平移了。 看着这个结果,我是恍然大悟,艹,以前太把大厂的人当神看了,再加上面试时紧张,就没考虑那么多。现在看看,这根本就是个伪命题啊,首先constraintlayout
所建立的布局体系是LayoutParams相关的,而View的位置属性(x/y)也在measure和layout过程中根据lp来确定的。所以当我通过属性动画改变translateX
时,并不会影响lp,所以也不会改变约束,当然也不会影响到其他View了。
结论:不要感觉大厂的人很权威,其中有很多人可能就擅长装个逼而已,你说你自己都没弄懂,就敢拿来面试别人,还误导人家,好意思吗?现在非常庆幸,虽然当初面试过了,但是幸好因为薪资问题没有去,要不然跟这种人共事天天看他装逼,我心里也不舒服。