0%

Android 动画深入分析(三):属性动画

1. 属性动画概述

  • 属性动画是 API 11 新加入的特性,和 View 动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至还可以没有对象

  • 属性动画的效果也得到了加强,不再像 View 动画那样只支持四种简单的变换

  • 属性动画默认时间间隔是 300ms,默认帧率是 10ms/帧,可以达到的效果是:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。比较常用的动画类是:ValueAnimatorObjectAnimatorAnimatorSet

  • Demo:

    1
    2
    // 改变一个对象的 translationY 属性,让其沿着 Y 轴向上平移一段距离(它的高度),在默认时间内完成
    ObjectAnimator.ofFloat(myObject, "translationY", -myObject.getHeight()).start();
    1
    2
    3
    4
    5
    6
    7
    // 改变一个对象的背景色属性,让背景色在 3 秒内实现从 0xFFFF8080 到 0xFF8080FF 的渐变,无限循环且反转
    ValueAnimator colorAnim = ObjectAnimator.ofInt(this, "background", /* Red */ 0xFFFF808080, /* Blue */ 0xFF8080FF);
    colorAnim.setDuration(3000);
    colorAnim.setEvaluator(new ArgbEvaluator());
    colorAnim.setRepeatCount(ValueAnimator.INFINITE);
    colorAnim.setRepeatMode(ValueAnimator.REVERSE);
    colorAnim.start();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 动画集合,5 秒内对 View 的旋转、平移、缩放和透明度都进行了改变
    AnimatorSet set = new AnimatorSet();
    set.playTogether(
    ObjectAnimator.ofFloat(myView, "rotationX", 0, 360),
    ObjectAnimator.ofFloat(myView, "rotationY", 0, 180),
    ObjectAnimator.ofFloat(myView, "rotation", 0, -90),
    ObjectAnimator.ofFloat(myView, "translationX", 0, 90),
    ObjectAnimator.ofFloat(myView, "translationY", 0, 90),
    ObjectAnimator.ofFloat(myView, "scaleX", 1, 1.5f),
    ObjectAnimator.ofFloat(myView, "scaleY", 1, 0.5f),
    ObjectAnimator.ofFloat(myView, "alpha", 1, 0.25f, 1));
    set.setDuration(5 * 1000).start();

2. 使用属性动画

  • 属性动画除了通过代码实现以外,还可以通过 XML 来定义。属性动画需要定义在 res/animator/ 目录下,语法如下:

    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
    <set
    android:ordering=["together" | "sequentially"] >
    <objectAnimator
    android:propertyName="string"
    android:duration="int"
    android:valueFrom="float | int | color"
    android:valueTo="float | int | color"
    android:startOffset="int"
    android:repeatCount="int"
    android:repeatMode=["restart" | "reverse"]
    android:valueType=["intType" | "floatType"] />

    <animator
    android:duration="int"
    android:valueFrom="float | int | color"
    android:valueTo="float | int | color"
    android:startOffset="int"
    android:repeatCount="int"
    android:repeatMode=["restart" | "reverse"]
    android:valueType=["intType" | "floatType"] />

    <set
    ...
    </set>
    </set>
    • ValueAnimator 对应 <animator> 标签,ObjectAnimator 对应 <objectAnimator> 标签,AnimatorSet 对应 <set> 标签

    • <set> 标签的 android:ordering 属性有两个可选值:togethersequentiallytogether 表示动画集合中的子动画同时播放sequentially 表示动画集合中的子动画按照前后顺序依次播放默认值是 together

    • <objectAnimator> 标签的属性及含义

      属性 含义
      android:propertyName 表示属性动画的作用对象的属性的名称
      android:duration 表示动画的时长
      android:valueFrom 表示属性的起始值
      android:valueTo 表示属性的结束值
      android:startOffset 表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正播放此动画
      android:repeatCount 表示动画的重复次数,默认值为 0,-1 表示无限循环
      android:repeatMode 表示动画的重复模式,有两个选项 restartreverse,分别表示连续重复逆向重复。连续重复就是动画每次都重新开始播放,逆向重复是指第一次播放完以后,第二次会倒着播放动画,第三次再重头开始播放动画,第四次再倒着播放动画,如此反复
      android:valueType 表示 android:propertyName 所指定的属性的类型,有 intTypefloatType 两个可选项,分别表示属性的类型为整型浮点型。另外,如果 android:propertyName 所指定的属性表示的是颜色,那么不需要指定 android:valueType,系统会自动对颜色类型的属性做处理
  • Demo:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 通过 XML 定义一个属性动画并将其作用在 View 上
    // res/animator/property_animator.xml
    <set android:ordering="together">
    <objectAnimator
    android:propertyName="x"
    android:duration="300"
    android:valueTo="200"
    android:valueType="intType" />
    <objectAnimator
    android:propertyName="y"
    android:duration"300"
    android:valueTo="300"
    android:valueType="intType" />
    </set>
    1
    2
    3
    4
    // 使用上面的属性
    AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext, R.anim_property_animator);
    set.setTarget(mButton);
    set.start();
    • 实际开发中建议采用代码的方式实现属性动画,一方面代码的方式比较简单,另一方面很多方面一个属性的起始值是无法提前确定的

3. 理解插值器和估值器

  • TimeInterpolator时间插值器,作用是根据时间流逝的百分比来计算出当前属性值改变的百分比

    • 系统预置的有 LinearInterpolator(线性插值器:匀速动画)AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)DecelerateInterpolator(减速插值器:动画越来越慢)
  • TypeEvaluator类型估值器,作用是根据当前属性改变的百分比来计算改变后的属性值

    • 系统预置的有 IntEvaluator(针对整型属性)FloatEvaluator(针对浮点型属性)ArgbEvaluator(针对 Color 属性)
  • 线性插值器 LinearInterpolator 源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class LinearInterpolator implements Interpolator {
    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
    return input; // 线性插值器的返回值和输入值一样
    }
    }
  • 整型估值器 IntEvaluator 源码:

    1
    2
    3
    4
    5
    6
    public class IntEvaluator implements TypeEvaluator<Integer> {
    public Ingeter evaluate(float fraction, Integer startValue, Integer endValue) {
    int startInt = startValue;
    return (int) (startInt + fraction * (endValue - startInt));
    }
    }
  • 自定义插值器和估值器

    • 属性动画要求对象的该属性有 set 方法和 get 方法(可选)
    • 插值器和估值器都是一个接口,且内部都只有一个方法
    • 自定义插值器需要实现 Interpolator 或者 TimeInterpolator,自定义估值器需要实现 TypeEvaluator
    • 如果要对其他类型(非 intfloatColor)做动画,必须要自定义类型估值器

4. 属性动画的监听器

  • 属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorListenerAnimatorUpdateListener

  • AnimatorListener 定义:

    1
    2
    3
    4
    5
    6
    public static interface AnimatorListener {
    void onAnimationStart(Animator animation);
    void onAnimationEnd(Animator animation);
    void onAnimationCancel(Animator animation);
    void onAnimationRepeat(Animator animation);
    }
  • AnimatorUpdateListener 定义:

    1
    2
    3
    public static interface AnimatorUpdateListener {
    void onAnimationUpdate(ValueAnimator animation);
    }
    • AnimatorUpdateListener 会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate() 就会被调用一次,利用这个特性,可以做一些特殊的逻辑

5. 对任意属性做动画

  • object 的属性 abc 做动画,如果想让动画生效,要同时满足两个条件

    1. object 必须要提供 setAbc() 方法,如果动画的时候没有传递初始值,那么还要提供 getAbc() 方法,因为系统要去读取 abc 属性的初始值(如果这条不满足,程序直接 crash)
    2. objectsetAbc() 对属性 abc 所做的改变必须能够通过某种方法反映出来,比如会带来 UI 的改变之类(如果这条不满足,动画无效果但不会 crash)
  • 上述条件 2 的反例:对 Button 的 width 属性做动画没有效果

    • 因为 Button 内部虽然提供了 getWidth()setWidth() 方法,但是这个 setWidth() 方法并不是改变视图的大小,它是 TextView 新添加的方法,View 是没有这个 setWidth() 方法的。由于 Button 继承了 TextView,所以 Button 也就有了 setWidth() 方法
    • 分析 getWidth()setWidth() 源码可知,getWidth() 的确是获取 View 的宽度的,但 setWidth() 是 TextView 及其子类的专属方法,它的作用不是设置 View 的宽度,而是设置 TextView 的最大宽度和最小宽度的,这和 TextView 的宽度不是一个概念
    • 具体来说,TextView 的宽度对应 XML 中的 android:layout_width 属性,而 TextView 还有一个属性 android:width 对应了 TextView 的 setWidth() 方法
  • 官方文档提供解决上面反例的 3 种方法

    • 如果有权限的话,给带操作对象加上 getXxx()setXxx() 方法

      • 但很多时候我们并没有这个权限,比如我们无法给 Button 加上一个合乎要求的 setWidth() 方法,因为这是 Android SDK 内部实现的。这个方法最简单,但往往是不可行的
    • 用一个类来包装原始对象,间接为其提供 getXxx()setXxx() 方法

      • Demo:

        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
        private void performAnimate() {
        ViewWrapper wrapper = new ViewWrapper(mButton);
        ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start();
        }

        @Override
        public void onClick(View v) {
        if (v == mButton) {
        performAnimate();
        }
        }

        private static class ViewWrapper {
        private View mTarget;

        public ViewWrapper(View target) {
        mTarget = target;
        }

        public int getWidth() {
        return mTarget.getLayoutParams().width();
        }

        public void setWidth(int width) {
        mTarget.getLayoutParams().width = width;
        mTarget.requestLayout();
        }
        }
    • 采用 ValueAnimator,监听动画过程,自己实现属性的改变

      • ValueAnimator 本身不作用于任何对象,即直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改对象的属性值,这样也就相当于对对象做了动画

      • Demo:

        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
        private void performAnimate(final View target, final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
        // 持有一个 IntEvaluator 对象,方面下面估值的时候使用
        private IntEvaluator mEvaluator = new IntEvaluator();

        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
        // 获得当前动画的进度值,整型,1~100 之间
        int currentValue = (Integer) animator.getAnimatedValue();
        Log.d(TAG, "current value: " + currentValue);

        // 获得当前进度占整个动画过程的比例,浮点型,0~1 之间
        float fraction = animator.getAnimatedFraction();

        // 直接调用整型估值器,通过比例计算出宽度,然后再设给 Button
        target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
        target.requestLayout();
        }
        });

        valueAnimator.setDuration(5000).start();
        }

        @Override
        public void onClick(View v) {
        if (v == mButton) {
        performAnimate(mButton, mButton.getWidth(), 500);
        }
        }

6. 属性动画的工作原理

  • 属性动画的原理

    • 属性动画要求动画作用的对象提供该属性的 get 方法set 方法
    • 属性动画根据外界传递的该属性的初始值最终值以动画的效果多次去调用 set 方法
    • 每次传递给 set 方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值
  • 源码分析

    • 分析ObjectAnimatorstart() 方法的源码可知,首先会判断当前动画等待的动画(Pending)延迟的动画(delay)中有和当前动画相同的动画,那么就把相同的动画给取消掉
    • ObjectAnimator 继承了 ValueAnimator,分析 ValueAnimatorstart() 方法源码可知:属性动画需要运行在 Looper 的线程中,然后会调到 JNI
    • 最终通过一系列调用,属性值的 get() 方法和 set() 方法都是通过反射来调用的
-------------------- 本文结束感谢您的阅读 --------------------