Android动画下篇(属性动画)

属性动画

属性动画出现的原因

  • 属性动画(Property Animation)是在 Android 3.0(API 11)后才提供的一种全新动画模式。
  • 那么为什么要提供属性动画(Property Animation)?
  1. 为了弥补逐帧动画和补间动画只能作用在视图View上的缺点,在实际使用中有些情况下的动画效果只是针对视图的某个属性或对象而不是整个视图。
  • 如,现需要实现视图的颜色动态变化,那么就需要操作视图的颜色属性从而实现动画效果,而不是针对整个视图进行动画操作。
  1. 没有改变View的属性,只是改变视觉效果。
  • 如,将屏幕左上角的按钮通过补间动画移动到屏幕的右下角,点击当前按钮位置(屏幕右下角)是没有效果的,因为实际上按钮还是停留在屏幕左上角,补间动画只是将这个按钮绘制到屏幕右下角,改变了视觉效果而已。
  1. 动画效果单,补间动画只能实现平移、旋转、缩放和透明度这些简单的动画需求,一旦遇到相对复杂的动画效果,即超出了上述4种动画效果,那么补间动画则无法实现,功能和可扩展性低。

简介

  • 作用对象:任意 Java 对象,不再局限于视图View对象,甚至没对象也可以。
  • 实现的动画效果:可自定义各种动画效果,不再局限于4种基本变换(平移、旋转、缩放 & 透明度)。

工作原理

-在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,从而实现该对象在该属性上的动画效果。

注:可以是任意对象的任意属性

  • 具体的工作原理逻辑如下图:


    Android动画下篇(属性动画)_第1张图片
    属性动画工作原理.png
  • 从上述工作原理可以看出属性动画有两个非常重要的类:ValueAnimator类 & ObjectAnimator类。
  • 其实属性动画的使用基本都是依靠这两个类,所以,在下面介绍属性动画的具体使用时,我会着重介绍这两个类。

具体使用

1. ValueAnimator类
  • 实现动画的原理:通过不断控制 值 的变化,再不断手动赋给对象的属性,从而实现动画效果。如下图:
    Android动画下篇(属性动画)_第2张图片
    ValueAnimator.png
  • 从上面原理可以看出:ValueAnimator类中有3个重要方法:
  1. ValueAnimator.ofInt(int values)
  2. ValueAnimator.ofFloat(float values)
  3. ValueAnimator.ofObject(int values)
  • 下面将逐一介绍。
1.1、ValueAnimator.ofInt(int values)
  • 作用:将初始值以整型数值的形式过渡到结束值,ValueAnimator本质只是一种值的操作机制,所以下面的介绍先是展示如何改变一个值的过程(下面的实例主要讲解:如何将一个值从0平滑地过渡到3)。

设置方式1:Java代码设置

  • 实际开发中,建议使用Java代码实现属性动画:因为很多时候属性的起始值是无法提前确定的(无法使用XML设置),需要在Java代码里动态获取。
  // 步骤1:设置动画属性的初始值 & 结束值
         /*ofInt()作用有两个
         1. 创建动画实例
         2. 将传入的多个Int参数进行平滑过渡:此处传入0和1,表示将值从0平滑过渡到1
         如果传入了3个Int参数 a,b,c ,则是先从a平滑过渡到b,再从b平滑过渡到c,以此类推
         ValueAnimator.ofInt()内置了整型估值器,直接采用默认的,不需要设置,即默认设置了如何从初始值过渡到结束值
         关于自定义插值器我将在后面进行讲解*/
        ValueAnimator anim = ValueAnimator.ofInt(0, 3);
        // 步骤2:设置动画的播放各种属性
        anim.setDuration(500);// 设置动画运行的时长
        anim.setStartDelay(500);// 设置动画延迟播放时间
        anim.setRepeatCount(0);// 设置动画重复播放次数 = 重放次数+1;当动画播放次数 = infinite时,动画无限重复
        /*设置重复播放动画模式:
        ValueAnimator.RESTART(默认):正序重放
        ValueAnimator.REVERSE:倒序回放  */
        anim.setRepeatMode(ValueAnimator.RESTART);
        // 步骤3:设置值的更新监听器
        /*注:值每变化一次,该方法就会被调用一次。
        将改变的值手动赋值给对象的属性值就是通过动画的更新监听器,这部分会在下面设置动画中讲到*/
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 获得改变后的值
                int currentValue = (Integer) animation.getAnimatedValue();
                // 输出改变后的值
                System.out.println(currentValue);
                // 步骤4:将改变后的值赋给对象的属性值,下面设置动画中会详细说明
                // 步骤5:刷新视图,即重新绘制,从而实现动画效果,放到设置动画中说明
            }
        });
        // 步骤6:启动动画
        anim.start();
  • 值从初始值过度到结束值的日志如下:
10-28 12:18:10.357 I/System.out: 0
10-28 12:18:10.374 I/System.out: 0
10-28 12:18:10.391 I/System.out: 0
10-28 12:18:10.407 I/System.out: 0
10-28 12:18:10.423 I/System.out: 0
10-28 12:18:10.442 I/System.out: 0
10-28 12:18:10.458 I/System.out: 0
10-28 12:18:10.474 I/System.out: 0
10-28 12:18:10.491 I/System.out: 0
10-28 12:18:10.507 I/System.out: 0
10-28 12:18:10.526 I/System.out: 0
10-28 12:18:10.540 I/System.out: 1
10-28 12:18:10.558 I/System.out: 1
10-28 12:18:10.574 I/System.out: 1
10-28 12:18:10.590 I/System.out: 1
10-28 12:18:10.607 I/System.out: 1
10-28 12:18:10.625 I/System.out: 1
10-28 12:18:10.640 I/System.out: 1
10-28 12:18:10.657 I/System.out: 2
10-28 12:18:10.674 I/System.out: 2
10-28 12:18:10.692 I/System.out: 2
10-28 12:18:10.707 I/System.out: 2
10-28 12:18:10.723 I/System.out: 2
10-28 12:18:10.742 I/System.out: 2
10-28 12:18:10.757 I/System.out: 2
10-28 12:18:10.774 I/System.out: 2
10-28 12:18:10.792 I/System.out: 2
10-28 12:18:10.807 I/System.out: 2
10-28 12:18:10.824 I/System.out: 2
10-28 12:18:10.840 I/System.out: 3

设置方式2:在XML 代码中设置

  • 具备重用性,即将通用的动画写到XML里,可在各个界面中去重用它。
  • 在路径 res/animator的文件夹里创建相应的动画 .xml文件




  • 在Java代码中启动动画
  // 载入xml动画
        Animator animator = AnimatorInflater.loadAnimator(MainActivity.this, R.animator.value_animator);
        // 设置动画对象
        animator.setTarget(view);
        // 启动动画
        animator.start();
  • 手动赋值给对象属性,从而实现一个完整的动画效果。实现的动画效果是按钮的宽度从150px放大到 500px。
// 创建动画作用对象:此处以Button为例
        final Button mButton = (Button) findViewById(R.id.btn_animator);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 步骤1:设置属性数值的初始值 & 结束值
                // 初始值 = 当前按钮的宽度,此处在xml文件中设置为150
                // 结束值 = 500
                ValueAnimator valueAnimator = ValueAnimator.ofInt(mButton.getLayoutParams().width, 500);
                // ValueAnimator.ofInt()内置了整型估值器,直接采用默认的.不需要设置
                // 即默认设置了如何从初始值150过渡到结束值500
                // 步骤2:设置动画的播放各种属性
                // 设置动画运行时长:2s
                valueAnimator.setDuration(2000);
                // 步骤3:将属性数值手动赋值给对象的属性:此处是将 值 赋给 按钮的宽度
                // 设置更新监听器:即数值每次变化更新都会调用该方法
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animator) {
                        // 获得每次变化后的属性值
                        int currentValue = (Integer) animator.getAnimatedValue();
                        // 输出每次变化后的属性值进行查看
                        System.out.println(currentValue);
                        // 每次值变化时,将值手动赋值给对象的属性
                        // 即将每次变化后的值 赋 给按钮的宽度,这样就实现了按钮宽度属性的动态变化
                        mButton.getLayoutParams().width = currentValue;
                        // 步骤4:刷新视图,即重新绘制,从而实现动画效果
                        mButton.requestLayout();
                    }
                });
                // 启动动画
                valueAnimator.start();
            }
        });
  • 效果图
    Android动画下篇(属性动画)_第3张图片
    属性动画1.gif
1.2、ValueAnimator.oFloat(float values)

设置方式1:Java代码设置

  • ValueAnimator anim = ValueAnimator.ofFloat(0, 3);
  • 其他使用类似ValueAnimator.ofInt(int values),此处不作过多描述。

设置方式2:在XML 代码中设置



  • 值从初始值过度到结束值的日志如下:
10-28 21:07:06.929 I/System.out: 0.010676086
10-28 21:07:06.945 I/System.out: 0.038209677
10-28 21:07:06.961 I/System.out: 0.08240533
10-28 21:07:06.980 I/System.out: 0.14275941
10-28 21:07:06.996 I/System.out: 0.21858394
10-28 21:07:07.013 I/System.out: 0.30901426
10-28 21:07:07.026 I/System.out: 0.38108826
10-28 21:07:07.042 I/System.out: 0.4939717
10-28 21:07:07.059 I/System.out: 0.61832196
10-28 21:07:07.075 I/System.out: 0.7445653
10-28 21:07:07.093 I/System.out: 0.8870261
10-28 21:07:07.108 I/System.out: 1.0364745
10-28 21:07:07.127 I/System.out: 1.1819896
10-28 21:07:07.142 I/System.out: 1.3400831
10-28 21:07:07.160 I/System.out: 1.5
10-28 21:07:07.175 I/System.out: 1.650543
10-28 21:07:07.193 I/System.out: 1.8087938
10-28 21:07:07.209 I/System.out: 1.9635255
10-28 21:07:07.225 I/System.out: 2.1043596
10-28 21:07:07.242 I/System.out: 2.2472777
10-28 21:07:07.259 I/System.out: 2.381678
10-28 21:07:07.276 I/System.out: 2.4990177
10-28 21:07:07.292 I/System.out: 2.6126127
10-28 21:07:07.308 I/System.out: 2.7135253
10-28 21:07:07.326 I/System.out: 2.795885
10-28 21:07:07.342 I/System.out: 2.8690374
10-28 21:07:07.360 I/System.out: 2.9265847
10-28 21:07:07.375 I/System.out: 2.965902
10-28 21:07:07.392 I/System.out: 2.9914513
10-28 21:07:07.408 I/System.out: 3.0
  • 从上面可以看出,ValueAnimator.ofInt()与ValueAnimator.oFloat()仅仅只是在估值器上的区别:(即如何从初始值过渡到结束值)。
1.3、ValueAnimator.ofObject()
  • 作用:将初始值以对象的形式过渡到结束值,即通过操作对象实现动画效果。
// 创建初始动画时的对象  & 结束动画时的对象
myObject object1 = new myObject();  
myObject object2 = new myObject();  
ValueAnimator anim = ValueAnimator.ofObject(new myObjectEvaluator(), object1, object2);  
// 创建动画对象 & 设置参数
// 参数说明
// 参数1:自定义的估值器对象(TypeEvaluator 类型参数)
// 参数2:初始动画的对象
// 参数3:结束动画的对象
anim.setDuration(5000);  
anim.start();
实例说明
  • 下面我将用实例说明该如何自定义TypeEvaluator接口并通过ValueAnimator.ofObject()实现动画效果。
  • 实现的动画效果:一个圆从一个点移动到另外一个点,效果图如下所示。


    Android动画下篇(属性动画)_第4张图片
    实例.gif
  1. 定义对象类
  • 因为ValueAnimator.ofObject()是面向对象操作的,所以需要自定义对象类。
  • 本例需要操作的对象是圆的点坐标。
    Point.java
public class Point {
    // 设置两个变量用于记录坐标的位置
    private float x;
    private float y;

    // 构造方法用于设置坐标
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    // get方法用于获取坐标
    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}
  1. 根据需求实现TypeEvaluator接口
  • 实现TypeEvaluator接口的目的是自定义如何从初始点坐标过渡到结束点坐标。
  • 本例实现的是一个从左上角到右下角的坐标过渡逻辑。
import android.animation.TypeEvaluator;
// 实现TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {
    // 复写evaluate()
    // 在evaluate()里写入对象动画过渡的逻辑
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        // 将动画初始值startValue 和 动画结束值endValue 强制类型转换成Point对象
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;
        // 根据fraction来计算当前动画的x和y的值
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
        // 将计算后的坐标封装到一个新的Point对象中并返回
        Point point = new Point(x, y);
        return point;
    }
}
  • 上面步骤是根据需求自定义TypeEvaluator的实现
  • 下面将讲解如何通过对Point对象进行动画操作,从而实现整个自定义View的动画效果。
  1. 将属性动画作用到自定义View当中
    MyView.java
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class MyView extends View {
    // 需要用到的变量
    public static final float RADIUS = 70f;// 圆的半径 = 70
    private Point currentPoint;// 当前点坐标
    private Paint mPaint;// 绘图画笔

    // 构造方法(初始化画笔)
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 复写onDraw()从而实现绘制逻辑
    /*绘制逻辑:先在初始点画圆,通过监听当前坐标值(currentPoint)的变化,
      每次变化都调用onDraw()重新绘制圆,从而实现圆的平移动画效果*/
    @Override
    protected void onDraw(Canvas canvas) {
        // 如果当前点坐标为空(即第一次)
        if (currentPoint == null) {
            // 创建一个点对象(坐标是(70,70))
            currentPoint = new Point(RADIUS, RADIUS);
            // 在该点画一个圆:圆心 = (70,70),半径 = 70
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);

            // (重点关注)将属性动画作用到View中
            // 步骤1:创建初始动画时的对象点  & 结束动画时的对象点
            Point startPoint = new Point(RADIUS, RADIUS);// 初始点为圆心(70,70)
            Point endPoint = new Point(700, 1000);// 结束点为(700,1000)
            // 步骤2:创建动画对象 & 设置初始值 和 结束值
            /*参数说明
              参数1:TypeEvaluator类型参数 - 使用自定义的PointEvaluator(实现了TypeEvaluator接口)
              参数2:动画初始的对象点
              参数3:动画结束的对象点*/
            ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
            // 步骤3:设置动画参数
            // 设置动画时长
            anim.setDuration(5000);
            // 步骤4:通过值的更新监听器,将改变的对象手动赋值给当前对象
            /*此处是将改变后的坐标值对象赋给当前的坐标值对象。
              设置值的更新监听器,即每当坐标值(Point对象)更新一次,该方法就会被调用一次*/
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    // 将每次变化后的坐标值(估值器PointEvaluator中evaluate()返回的Piont对象值)到当前坐标值对象(currentPoint)
                    // 从而更新当前坐标值(currentPoint)
                    currentPoint = (Point) animation.getAnimatedValue();
                    // 步骤5:每次赋值后就重新绘制,从而实现动画效果
                    // 调用invalidate()后,就会刷新View,即才能看到重新绘制的界面,即onDraw()会被重新调用一次
                    // 所以坐标值每改变一次,就会调用onDraw()一次
                    invalidate();
                }
            });
            // 步骤6:启动动画
            anim.start();
        } else {
            // 如果坐标值不为0,则画圆
            // 所以坐标值每改变一次,就会调用onDraw()一次,就会画一次圆,从而实现动画效果
            // 在该点画一个圆:圆心 = (currentPoint.getX(),currentPoint.getY()),半径 = 70
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);
        }
    }
}
  1. 在布局文件加入自定义View控件



    


  • 特别注意

从上面可以看出,其实ValueAnimator.ofObject()的本质还是操作值 ,只是是采用将多个值封装到一个对象里的方式同时对多个值一起操作而已。就像上面的例子,本质还是操作坐标中的x,y两个值,只是将其封装到Point对象里,方便同时操作x,y两个值而已。

2. ObjectAnimator类
  • 直接对对象的属性值进行改变操作,从而实现动画效果。
  1. 如直接改变 View的alpha属性,从而实现透明度的动画效果。
  2. 继承自ValueAnimator类,即底层的动画实现机制是基于ValueAnimator类。
  • 本质原理: 通过不断控制值的变化,再不断自动赋给对象的属性,从而实现动画效果。如下图:


    Android动画下篇(属性动画)_第5张图片
    ObjectAnimator.png
  • 从上面的工作原理可以看出:ObjectAnimator与 ValueAnimator类的区别:
    ValueAnimator类是先改变值,然后手动赋值给对象的属性从而实现动画;是间接对对象属性进行操作。
    ObjectAnimator类是先改变值,然后自动赋值给对象的属性从而实现动画;是直接对对象属性进行操作。
  • 至于是如何自动赋值给对象的属性,下面将详细说明。
具体使用
  • 由于是继承了ValueAnimator类,所以使用的方法也十分类似,有XML 设置 / Java设置两种方式。
  1. Java 设置
// ofFloat()作用有两个
// 1. 创建动画实例
// 2. 参数设置:参数说明如下
// Object object:需要操作的对象
// String property:需要操作的对象的属性
// float ....values:动画初始值 & 结束值(不固定长度)
// 若是两个参数a,b,则动画效果则是从属性的a值到b值
// 若是三个参数a,b,c,则则动画效果则是从属性的a值到b值再到c值
// 以此类推
// 至于如何从初始值 过渡到 结束值,同样是由估值器决定,此处ObjectAnimator.ofFloat()是有系统内置的浮点型估值器FloatEvaluator,同ValueAnimator讲解
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);
// 设置动画运行的时长
animator.setDuration(500);
// 设置动画延迟播放时间
animator.setStartDelay(500);
// 设置动画重复播放次数 = 重放次数+1
// 动画播放次数 = infinite时,动画无限重复
animator.setRepeatCount(0);
// 设置重复播放动画模式
// ValueAnimator.RESTART(默认):正序重放
// ValueAnimator.REVERSE:倒序回放
animator.setRepeatMode(ValueAnimator.RESTART);
// 启动动画
animator.start();
  1. 在XML 代码中设置
  • 在路径 res/animator 的文件夹里创建动画效果.xml文件
// ObjectAnimator 采用  标签

  • 使用实例
    此处先展示四种基本变换:平移、旋转、缩放 & 透明度
  1. 平移
                // 获得当前按钮的X轴平移值,未平移时初始值为0
                float curTranslationX = button.getTranslationX();
                // 表示的是:
                // 动画作用对象是button
                // 动画作用的对象的属性是X轴平移(在Y轴上平移同理,采用属性"translationY")
                // 动画效果是:从当前位置向右平移200px再平移到初始位置(正值右移,负值左移)
                ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(button, "translationX", curTranslationX, 200,curTranslationX);
                translationAnimator.setDuration(5000);
                translationAnimator.start();       
  • 效果图


    Android动画下篇(属性动画)_第6张图片
    平移动画.gif
  1. 旋转
                // 表示的是:
                // 动画作用对象是button
                // 动画作用的对象的属性是旋转rotation
                // 动画效果是:0°- 360°
                ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(button, "rotation", 0f, 360f);
                rotationAnimator.setDuration(5000);
                rotationAnimator.start();     

3.缩放

                // 表示的是:
                // 动画作用对象是button
                // 动画作用的对象的属性是X轴缩放
                // 动画效果是:放大到3倍,再缩小到初始大小
                ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(button, "scaleX", 1f, 3f, 1f);
                button.setPivotX(100);
                scaleAnimator.setDuration(5000);
                scaleAnimator.start();

要重点注意:button.setPivotX(100)
这里的prvotX缩放轴点是相对于控件自身而言的,
如上面的按钮的宽为200px,设置PivotX为100就是在X轴上按中心点进行缩放
X轴缩放时设置povotX:View.setPivotX(float povotX)
Y轴缩放时设置povotY:View.setPivotY(float povotY)

  • 效果图
    Android动画下篇(属性动画)_第7张图片
    缩放动画.gif
  1. 透明度
                // 表示的是:
                // 动画作用对象是button
                // 动画作用的对象的属性是透明度alpha
                // 动画效果是:常规 - 全透明 - 常规
                ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(button, "alpha", 1f, 0f, 1f);
                alphaAnimator.setDuration(5000);
                alphaAnimator.start();
  • 在上面的讲解,我们使用了属性动画最基本的四种动画效果:透明度、平移、旋转 & 缩放

即在ObjectAnimator.ofFloat()的第二个参数String property传入alpha、rotation、translationX 和 scaleY 等。

属性 作用 数值类型
Alpha 控制View的透明度 float
TranslationX 控制X方向的位移 float
TranslationY 控制Y方向的位移 float
ScaleX 控制X方向的缩放倍数 float
ScaleY 控制Y方向的缩放倍数 float
Rotation 控制以屏幕方向为轴的旋转度数 float
RotationX 控制以X轴为轴的旋转度数 float
RotationY 控制以Y轴为轴的旋转度数 float

问题:那么ofFloat()的第二个参数还能传入什么属性值呢?
答案:任意属性值,因为ObjectAnimator 类是对对象属性值进行改变从而实现动画效果的,本质是通过不断控制值的变化,再不断自动赋给对象的属性,从而实现动画效果的。

  • 自动赋给对象的属性的本质是调用该对象属性的set()和get()方法进行赋值。
  • 所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名去寻找该对象对应属性名的 set()和 get()方法,从而进行对象属性值的赋值,如上面的例子:
ObjectAnimator animator = ObjectAnimator.ofFloat(button, "rotation", 0f, 360f);
// 其实Button对象中并没有rotation这个属性值
// ObjectAnimator并不是直接对我们传入的属性名进行操作
// 而是根据传入的属性值"rotation" 去寻找对象对应属性名对应的get和set方法,从而通过setRotation和getRotation对属性进行赋值
// 因为Button对象中有rotation属性所对应的get和set方法
// 所以传入的rotation属性是有效的
// 所以才能对rotation这个属性进行操作赋值
public void setRotation(float value);  
public float getRotation();  
// 实际上,这两个方法是由View对象提供的,所以任何继承自View的对象都具备这个属性
  • 至于是如何进行自动赋值的,我们直接来看源码分析:
// 使用方法
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);  
anim.setDuration(500);
animator.start();  
// 启动动画,源码分析就直接从start()开始
<--  start()  -->
@Override  
public void start() {  
    AnimationHandler handler = sAnimationHandler.get();  
    if (handler != null) {  
        // 判断等待动画(Pending)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉 
        numAnims = handler.mPendingAnimations.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
      // 判断延迟动画(Delay)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉 
        numAnims = handler.mDelayedAnims.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
    }  
   // 调用父类的start()
   super.start();  
   // 因为ObjectAnimator类继承ValueAnimator类,所以调用的是ValueAnimator的start()
   // 经过层层调用,最终会调用到 自动赋值给对象属性值的方法
   // 下面就直接看该部分的方法,如下面代码所示
}  

<-- 自动赋值给对象属性值的逻辑方法 ->>
// 步骤1:初始化动画值
private void setupValue(Object target, Keyframe kf) {  
    if (mProperty != null) {  
        kf.setValue(mProperty.get(target));  
    }else 
        // 初始化时,如果属性的初始值没有提供,则反射调用对象的get()方法进行取值
        kf.setValue(mGetter.invoke(target));   
}  
// 步骤2:更新动画值
// 当动画下一帧来时(即动画更新的时候),setAnimatedValue()都会被调用
void setAnimatedValue(Object target) {  
    if (mProperty != null) {  
        mProperty.set(target, getAnimatedValue());  
        // 内部调用对象该属性的set()方法,从而从而将新的属性值设置给对象属性
    }   
}
  • 自动赋值的逻辑:

初始化时,如果属性的初始值没有提供,则调用对象的 get()方法进行取值;
当值变化时,用对象该属性的 set()方法,从而从而将新的属性值设置给对象属性。
所以ObjectAnimator 类针对的是任意对象 & 任意属性值,并不是单单针对于View对象。
如果需要采用ObjectAnimator类实现动画效果,那么需要操作的对象就必须有该属性的set() & get()。
同理,针对上述另外的三种基本动画效果,View也存在着setRotation()、getRotation()、setTranslationX()、getTranslationX()、setScaleY()、getScaleY()等set() & get() 。

自定义对象属性实现动画效果
  • 对于属性动画,其拓展性在于:不局限于系统限定的动画,可以自定义动画,即自定义对象的属性,并通过操作自定义的属性从而实现动画。
  • 那么,该如何自定义属性呢?本质上,就是:
    为对象设置需要操作属性的set()& get()方法;
    通过实现TypeEvaluator类从而定义属性变化的逻辑,类似于ValueAnimator的过程。
  • 下面,我将用一个实例来说明如何通过自定义属性实现动画效果。
    实现的动画效果,一个圆的颜色渐变,效果图如下所示:


    Android动画下篇(属性动画)_第8张图片
    颜色变化属性动画.gif
  • 自定义属性的逻辑如下:(需要自定义属性为圆的背景颜色)


    Android动画下篇(属性动画)_第9张图片
    自定义颜色属性.png
  1. 设置对象类属性的set() & get()方法

设置对象类属性的set() & get()有两种方法:
1、通过继承原始类,直接给类加上该属性的 get() & set(),从而实现给对象加上该属性的 get() & set()
2、通过包装原始动画对象,间接给对象加上该属性的 get() & set()。即用一个类来包装原始对象

  • 此处使用第一种方式进行展示,关于第二种方式的使用,会在下面进行详细介绍。
    MyColorView.java
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

public class MyColorView extends View {
    // 设置需要用到的变量
    public static final float RADIUS = 100f;// 圆的半径 = 100
    private Paint mPaint;// 绘图画笔
    // 设置背景颜色属性
    private String color;

    // 设置背景颜色的get() & set()方法
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
        // 将画笔的颜色设置成方法参数传入的颜色
        mPaint.setColor(Color.parseColor(color));
        // 调用了invalidate()方法,即画笔颜色每次改变都会刷新视图,然后调用onDraw()方法重新绘制圆
        // 而因为每次调用onDraw()方法时画笔的颜色都会改变,所以圆的颜色也会改变
        invalidate();
    }

    public MyColorView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        // 初始化画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 复写onDraw()画圆
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(500, 500, RADIUS, mPaint);
    }
}
  1. 在布局文件加入自定义View控件,这部分代码和上面ValueAnimator.ofObject中实例类似,这里就省略了。
  2. 根据需求实现TypeEvaluator接口
    此处实现估值器的本质是:实现颜色过渡的逻辑。
    ColorEvaluator.java
import android.animation.TypeEvaluator;

// 实现TypeEvaluator接口
public class ColorEvaluator implements TypeEvaluator {
    private int mCurrentRed;
    private int mCurrentGreen;
    private int mCurrentBlue;

    // 复写evaluate(),在evaluate()里写入对象动画过渡的逻辑,此处是写颜色过渡的逻辑
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        // 获取到颜色的初始值和结束值
        String startColor = (String) startValue;
        String endColor = (String) endValue;
        // 通过字符串截取的方式将初始化颜色分为RGB三个部分,并将RGB的值(16进制)转换成十进制数字
        // 那么每个颜色的取值范围就是0-255
        int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
        int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
        int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);

        int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
        int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
        int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);
        // 开始执行时将初始化颜色值赋值给当前需要操作的颜色值
        if (fraction==0){
            mCurrentRed = startRed;
            mCurrentGreen = startGreen;
            mCurrentBlue = startBlue;
        }
        // 计算初始颜色和结束颜色之间的差值
        // 该差值决定着颜色变化的快慢:初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢;否则,变化则很快
        // 具体如何根据差值来决定颜色变化快慢的逻辑写在getCurrentColor()里.
        int redDiff = Math.abs(startRed - endRed);
        int greenDiff = Math.abs(startGreen - endGreen);
        int blueDiff = Math.abs(startBlue - endBlue);
        if (mCurrentRed != endRed) {
            mCurrentRed = getCurrentColor(startRed, endRed, redDiff, fraction);
        } else if (mCurrentGreen != endGreen) {
            mCurrentGreen = getCurrentColor(startGreen, endGreen, greenDiff, fraction);
        } else if (mCurrentBlue != endBlue) {
            mCurrentBlue = getCurrentColor(startBlue, endBlue, blueDiff, fraction);
        }
        // 将计算出的当前颜色的值组装返回
        // 由于我们计算出的颜色是十进制数字,所以需要转换成十六进制字符串:调用getHexString()
        // 最终将RGB颜色拼装起来,并作为最终的结果返回
        String currentColor = "#" + getHexString(mCurrentRed)
                + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
        return currentColor;
    }


    // 根据fraction值来计算当前的颜色。
    private int getCurrentColor(int startColor, int endColor, int colorDiff, float fraction) {
        int currentColor;
        if (startColor > endColor) {
            currentColor = (int) (startColor - (fraction * colorDiff));
            if (currentColor < endColor) {
                currentColor = endColor;
            }
        } else {
            currentColor = (int) (startColor + (fraction * colorDiff));
            if (currentColor > endColor) {
                currentColor = endColor;
            }
        }
        return currentColor;
    }

    // 将10进制颜色值转换成16进制。
    private String getHexString(int value) {
        String hexString = Integer.toHexString(value);
        if (hexString.length() == 1) {
            hexString = "0" + hexString;
        }
        return hexString;
    }
}
  1. 调用ObjectAnimator.ofObject()方法
import android.animation.ObjectAnimator;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button button;
    private MyColorView mcv_anim;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.btn_anim);
        button.setOnClickListener(this);
        mcv_anim = findViewById(R.id.mcv_anim);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_anim:
                // 设置自定义View对象、背景颜色属性值 & 颜色估值器
                // 本质逻辑:
                // 步骤1:根据颜色估值器不断改变值
                // 步骤2:调用set()设置背景颜色的属性值(实际上是通过画笔进行颜色设置)
                // 步骤3:调用invalidate()刷新视图,即调用onDraw()重新绘制,从而实现动画效果
                ObjectAnimator objectAnimator = ObjectAnimator.ofObject(mcv_anim, "color", new ColorEvaluator(),
                        "#0000FF", "#FF0000");
                objectAnimator.setDuration(6000);
                objectAnimator.start();
                break;
        }
    }
}
  • ObjectAnimator类自动赋给对象的属性 的本质是调用该对象属性的set() & get()方法进行赋值。
    所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名去寻找该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值。
  • 从上面的原理可知,如果想让对象的属性a的动画生效,属性a需要同时满足下面两个条件:
    对象必须要提供属性a的set()方法

a. 如果没传递初始值,那么需要提供get()方法,因为系统要去拿属性a的初始值
b. 若该条件不满足,程序直接Crash

对象提供的 属性a的set()方法 对 属性a的改变 必须通过某种方法反映出来

a. 如带来ui上的变化
b. 若这条不满足,动画无效,但不会Crash

通过包装原始动画对象,间接给对象加上该属性的 get() & set()
  • 背景
    比如说:由于View的setWidth()并不是设置View的宽度,而是设置View的最大宽度和最小宽度的;所以通过setWidth()无法改变控件的宽度;所以对View视图的width做属性动画没有效果。
    具体请看下面Button按钮的例子:
// 创建动画作用对象:此处以Button为例
// 此Button的宽高设置具体为具体宽度200px
Button  mButton = (Button) findViewById(R.id.Button);    
// 设置动画的对象
ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
  • 效果图


    Android动画下篇(属性动画)_第10张图片
    Button.gif
  • 为什么没有动画效果呢?我们来看下View的setWidth方法
public void setWidth(int pixels) {  
    // 因为setWidth()并不是设置View的宽度,而是设置Button的最大宽度和最小宽度的
    // 所以通过setWidth()无法改变控件的宽度
   // 所以对width属性做属性动画没有效果
    mMaxWidth = mMinWidth = pixels;  
    mMaxWidthMode = mMinWidthMode = PIXELS;  

    requestLayout();  
    invalidate();  
}  
  
@ViewDebug.ExportedProperty(category = "layout")  
public final int getWidth() {  
    // getWidth的确是获取View的宽度
    return mRight - mLeft;
}
  • 那么,针对上述对象属性的set()不是设置属性或根本没有set()/ get()的情况应该如何处理?
    手动设置对象类属性的set() & get()。有两种方法,上面已经讲了一种了,这里我们将采取另一种用一个类来包装原始动画对象,间接给对象加上该属性的get() & set(),本质上是采用了设计模式中的装饰模式,即通过包装类从而扩展对象的功能。
public class MainActivity extends AppCompatActivity {
    Button mButton;
    ViewWrapper wrapper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.Button);
        // 创建动画作用对象:此处以Button为例

        wrapper = new ViewWrapper(mButton);
        // 创建包装类,并传入动画作用的对象
        
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(3000).start();
                // 设置动画的对象是包装类的对象
            }
        });
    }

    // 提供ViewWrapper类,用于包装View对象
    // 本例:包装Button对象
    private static class ViewWrapper {
        private View mTarget;

        // 构造方法:传入需要包装的对象
        public ViewWrapper(View target) {
            mTarget = target;
        }

        // 为宽度设置get() & set()
        public int getWidth() {
            return mTarget.getLayoutParams().width;
        }

        public void setWidth(int width) {
            mTarget.getLayoutParams().width = width;
            mTarget.requestLayout();
        }
    }
}
  • 效果图


    Android动画下篇(属性动画)_第11张图片
    button宽度动画.gif
属性动画的内存泄露问题
  • 许多时候用到了无限循环的动画,我们会这样写:animator.setRepeatCount(ValueAnimator.INFINITE);
    这样写如果没有及时取消,会导致此属性动画持有被动画对象的引用而导致内存泄露,故在activity生命周期结束时,如onDestroy方法中,及时的cancel掉这种动画。
    补间动画无此问题,它是给view做动画,在view的onDetachedFromWindow方法会取消掉动画,所以不会导致内存泄露。

总结

  • 对比ValueAnimator类 & ObjectAnimator 类,其实二者都属于属性动画,本质上都是一致的:先改变值,然后赋值给对象的属性从而实现动画效果。
  • 但二者的区别在于:
    ValueAnimator 类是先改变值,然后手动赋值给对象的属性从而实现动画;是间接对对象属性进行操作;
    ValueAnimator 类本质上是一种改变值的操作机制。
    ObjectAnimator类是先改变值,然后自动赋值给对象的属性从而实现动画;是直接对对象属性进行操作;
    ObjectAnimator更加智能、自动化程度更高。

属性动画其它的使用方法

组合动画(AnimatorSet 类)

  • 单一动画实现的效果相当有限,更多的使用场景是同时使用多种动画效果,即组合动画。
  • 实现组合动画的功能类:AnimatorSet类。
  • 具体使用:
AnimatorSet.play(Animator anim)   :播放当前动画
AnimatorSet.after(long delay)   :将现有动画延迟x毫秒后执行
AnimatorSet.with(Animator anim)   :将现有动画和传入的动画同时执行
AnimatorSet.after(Animator anim)   :将现有动画插入到传入的动画之后执行
AnimatorSet.before(Animator anim) :  将现有动画插入到传入的动画之前执行
  • 实例
    主要动画是平移,平移过程中伴随旋转动画,平移完后进行透明度变化
    实现方式有 XML设置 / Java代码设置两种
  1. java代码设置
                // 步骤1:设置需要组合的动画效果
                Float curTranslationX = button.getTranslationX();
                ObjectAnimator translation = ObjectAnimator.ofFloat(button, "translationX", curTranslationX, 300,curTranslationX);
                // 平移动画
                ObjectAnimator rotate = ObjectAnimator.ofFloat(button, "rotation", 0f, 360f);
                // 旋转动画
                ObjectAnimator alpha = ObjectAnimator.ofFloat(button, "alpha", 1f, 0f, 1f);
                // 透明度动画
                // 步骤2:创建组合动画的对象
                AnimatorSet animSet = new AnimatorSet();
                // 步骤3:根据需求组合动画
                animSet.play(translation).with(rotate).before(alpha);
                animSet.setDuration(5000);
                // 步骤4:启动动画
                animSet.start();
  1. XML设置,在 res/animator的文件夹里创建动画.xml文件
    set_animation.xml


    // 表示Set集合内的动画按顺序进行
    // ordering的属性值:sequentially & together
    // sequentially:表示set中的动画,按照先后顺序逐步进行(a完成之后进行b)
    // together:表示set中的动画,在同一时间同时进行,为默认值

    
        // 下面的动画同时进行
        
        
        
        
        
    

    
        // 下面的动画按序进行
        
        
        
        
    

  • 在Java代码中启动动画
        AnimatorSet animator = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.set_animation);
        // 创建组合动画对象  &  加载XML动画
        animator.setTarget(button);
        // 设置动画作用对象
        animator.start();
        // 启动动画
  • 效果图


    Android动画下篇(属性动画)_第12张图片
    组合动画.gif

PropertyValuesHolder优化组合动画

  • 上面使用AnimatorSet实现组合动画会产生三个ObjectAnimator对象和一个AnimatorSet,效率较低,官方建议使用PropertyValuesHolder将同时执行的ObjectAnimator组合在一起进行优化,可以这样写:
        Float curTranslationX = button.getTranslationX();
        // 平移动画
        PropertyValuesHolder pvhTranslation = PropertyValuesHolder.ofFloat("translationX",curTranslationX, 300,curTranslationX);
        // 旋转动画
        PropertyValuesHolder pvhRotate = PropertyValuesHolder.ofFloat("rotation", 0f, 360f);
        //通过PropertyValuesHolder将同时执行的动画组合成一个ObjectAnimator,减少ObjectAnimator的创建,从而提高效率
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(button, pvhTranslation, pvhRotate);
        // 透明度动画
        ObjectAnimator alpha = ObjectAnimator.ofFloat(button, "alpha", 1f, 0f, 1f);
        AnimatorSet animSet = new AnimatorSet();
        animSet.play(objectAnimator).before(alpha);
        animSet.setDuration(5000);
        animSet.start();

Keyframe

  • 通过前面几篇的讲解,我们知道如果要控制动画速率的变化,我们可以通过自定义插值器或来实现。但如果真的让我们为了速率变化效果而自定义插值器或者估值器的话,恐怕大部分人是拒绝的,因为大部分的人的数学知识已经还给老师了。
  • 为了解决方便的控制动画速率的问题,谷歌为了我们定义了一个KeyFrame的类,KeyFrame直译过来就是关键帧。
  • 关键帧这个概念是从动画里学来的,我们知道视频里,一秒要播放24帧图片,对于制作flash动画的同学来讲,是不是每一帧都要画出来呢?当然不是了,如果每一帧都画出来,那估计做出来一个动画片都得要一年时间;比如我们要让一个球在30秒时间内,从(0,0)点运动到(300,200)点,那flash是怎么来做的呢,在flash中,我们只需要定义两个关键帧,在动画开始时定义一个,把球的位置放在(0,0)点;在30秒后,再定义一个关键帧,把球的位置放在(300,200)点。在动画开始时,球初始在是(0,0)点,30秒时间内就adobe flash就会自动填充,把球平滑移动到第二个关键帧的位置(300,200)点;
  • 通过上面分析flash动画的制作原理,我们知道,一个关键帧必须包含两个原素,第一个是时间点,第二个是位置。即这个关键帧是表示的是某个物体在哪个时间点应该在哪个位置上。
  • 所以谷歌的KeyFrame也不例外,KeyFrame的用法如下:
        //第一个参数表示当前动画的进度,即从加速器中getInterpolation()函数的返回值;第二个参数表示当前进度应该在的位置
        Keyframe k0 = Keyframe.ofFloat(0f, 0);
        Keyframe k1 = Keyframe.ofFloat(0.5f, 100);
        Keyframe k2 = Keyframe.ofFloat(1f, 0);
        PropertyValuesHolder p = PropertyValuesHolder.ofKeyframe("translationY", k0, k1, k2);
        /*所以动画效果就是:
        开始时位置为0;
        动画开始1/2时位置为100;
        动画结束时位置为0;
        中间其它时候的位置会自动补全*/
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(button, p);
        objectAnimator.setDuration(5000);
        objectAnimator.start();

ViewPropertyAnimator用法

  • 从上面可以看出,属性动画的本质是对值操作,但Java是面向对象的,所以Google团队添加面向对象操作的属性动画使用类 - ViewPropertyAnimator类,可认为是属性动画的一种简写方式。
  • 具体使用
                // 使用解析
                //View.animate().xxx().xxx();
                // ViewPropertyAnimator的功能建立在animate()上
                // 调用animate()方法返回值是一个ViewPropertyAnimator对象,之后的调用的所有方法都是通过该实例完成
                // 调用该实例的各种方法来实现动画效果
                // ViewPropertyAnimator所有接口方法都使用连缀语法来设计,每个方法的返回值都是它自身的实例
                // 因此调用完一个方法后可直接连缀调用另一方法,即可通过一行代码就完成所有动画效果

                // 以下是例子
                // 单个动画设置:将按钮变成透明状态
                button.animate().alpha(0f);
                // 单个动画效果设置 & 参数设置
                button.animate().alpha(0f).setDuration(5000).setInterpolator(new BounceInterpolator());
                // 组合动画:将按钮变成透明状态再移动到(500,500)处
                button.animate().alpha(0f).x(500).y(500);

特别注意:
动画自动启动,无需调用start()方法.因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成后,动画就会自动启动。
该机制对于组合动画也同样有效,只要不断地连缀新的方法,那么动画就不会立刻执行,等到所有在ViewPropertyAnimator上设置的方法都执行完毕后,动画就会自动启动。

属性动画监听器

  • Animation类通过监听动画开始 / 结束 / 重复 / 取消时刻来进行一系列操作,如跳转页面等等。
  • 通过在Java代码里addListener()设置:
Animation.addListener(new AnimatorListener() {
          @Override
          public void onAnimationStart(Animation animation) {
              //动画开始时执行
          }
      
           @Override
          public void onAnimationRepeat(Animation animation) {
              //动画重复时执行
          }

         @Override
          public void onAnimationCancel()(Animation animation) {
              //动画取消时执行
          }
    
          @Override
          public void onAnimationEnd(Animation animation) {
              //动画结束时执行
          }
      });

特别注意:每次监听必须4个方法都需重写。

  • 因Animator类、AnimatorSet类、ValueAnimator、ObjectAnimator类存在以下继承关系:


    Android动画下篇(属性动画)_第13张图片
    属性动画继承关系.png
  • 所以AnimatorSet类、ValueAnimator、ObjectAnimator都可以使用addListener()监听器进行动画监听。
动画适配器AnimatorListenerAdapter
  • 背景:有些时候我们并不需要监听动画的所有时刻
  • 问题:但addListener(new AnimatorListener())监听器是必须重写4个时刻方法,这使得接口方法重写太累赘
  • 解决方案:采用动画适配器(AnimatorListenerAdapter)解决实现接口繁琐的问题。
animator.addListener(new AnimatorListenerAdapter() {  
// 向addListener()方法中传入适配器对象AnimatorListenerAdapter()
// 由于AnimatorListenerAdapter中已经实现好每个接口
// 所以这里不实现全部方法也不会报错
    @Override  
    public void onAnimationStart(Animator animation) {  
    // 如想只想监听动画开始时刻,就只需要单独重写该方法就可以
    }  
});  

结束语

Android动画系列到此也就总结完了,如果能坚持看完并理解了对Android动画也算是熟练掌握了,应对平时开发也基本足够了。这里面大部分内容都是自己看完其它几位前辈的博客摘录过来的,算是对Android动画做了个全面的总结,加深自己的理解,同时以后开发中忘记了也可以翻来看。

  • 感谢

Android 属性动画:这是一篇很详细的 属性动画 总结&攻略
自定义控件三部曲之动画篇(八)——PropertyValuesHolder与Keyframe
Android属性动画优化(更高效的使用属性动画)

你可能感兴趣的:(Android动画下篇(属性动画))