属性动画

前段时间面试滴滴的时候,跟面试官聊了一下属性动画,上来就问:你看过源码吗?我说没看过,然后他表示属性动画并不是真正的改变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()
        }
    }
}

以上就是属性动画的用法,期中的ObjectAnimatorValueAnimator的父类,而由于后者更加灵活,可复用程度更高,所以一般我们常用ValueAnimator。而ObjectAnimator呢,需要在创建时通过参数传递属性的名称,那不用猜也知道他用了运行时反射,而反射会带来更多的资源消耗,所以拉倒吧!

组合动画AnimatorSet

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的一些新的理解,还有安卓整体的布局体系。

摘要的结论

  1. 我通过属性动画,去改变view.translateX,不论是调用invalidate()还是requestLayout(),都只有targetView自己产生了平移;
  2. 而当我改为修改LayoutParams的marginStart后再requestLayout(),下面的所有View都跟着平移了。

看着这个结果,我是恍然大悟,艹,以前太把大厂的人当神看了,再加上面试时紧张,就没考虑那么多。现在看看,这根本就是个伪命题啊,首先constraintlayout所建立的布局体系是LayoutParams相关的,而View的位置属性(x/y)也在measure和layout过程中根据lp来确定的。所以当我通过属性动画改变translateX时,并不会影响lp,所以也不会改变约束,当然也不会影响到其他View了。

结论:不要感觉大厂的人很权威,其中有很多人可能就擅长装个逼而已,你说你自己都没弄懂,就敢拿来面试别人,还误导人家,好意思吗?现在非常庆幸,虽然当初面试过了,但是幸好因为薪资问题没有去,要不然跟这种人共事天天看他装逼,我心里也不舒服。