参考文献:

Android属性动画完全解析(上),初识属性动画的基本用法
Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
Property Animation(属性动画)使用详解

一、引言

视图动画/补间动画:

  1. 补间动画只能为 View 添加动画效果,且不能监听 View 相关属性的变化过程。
  2. 补间动画提供的动画能力较为单一,目前只支持帧动画、缩放动画、位移动画、旋转动画、透明度动画以及这些动画的集合动画。
  3. 补间动画改变的是 View 的绘制效果,View 的真正位置和相关属性并不会改变,这也就造成了点击事件的触发区域是动画前的位置而不是动画后的位置的原因。

**属性动画: **

  1. 属性动画作用对象不局限在 View 上,而是任何提供了 Getter 和 Setter 方法的对象的属性上。
  2. 属性动画没有直接改变 View 状态的能力,而是通过动态改变 View 相关属性的方式来改变 View 的显示效果。
  3. 属性动画使用更方便,可以用更简洁的代码实现相关的动画效果。
  4. 属性动画上手难度较高,对于 propertyName 需要自己去挖掘,或者自己通过 Wrapper 的方式去自定义 propertyName。
  5. 属性动画是 Android3.0 以上系统提供的能力,在 3.0 以下需导入 nineoldandroids 三方库解决兼容性问题。

二、基本使用

2.1 ValueAnimator

ValueAnimator 是整个属性动画机制当中最核心的一个类,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由 ValueAnimator 这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给 ValueAnimator,并且告诉它动画所需运行的时长,那么 ValueAnimator 就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator 还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。

但是 ValueAnimator 的用法却一点都不复杂,我们先从最简单的功能看起吧,比如说想要将一个值从 0 平滑过渡到 1,时长 300 毫秒,就可以这样写:

ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(300);
anim.start();

用法就是这么简单,现在如果你运行一下上面的代码,动画就会执行了。可是这只是一个将值从 0 过渡到 1 的动画,又看不到任何界面效果,我们怎样才能知道这个动画是不是已经真正运行了呢?这就需要借助监听器来实现了,如下所示:

ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(300);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float currentValue = (float) animation.getAnimatedValue();
        Log.d("TAG", "cuurent value is " + currentValue);
    }
});
anim.start();

可以看到,这里我们通过 addUpdateListener() 方法来添加一个动画的监听器,在动画执行的过程中会不断地进行回调,我们只需要在回调方法当中将当前的值取出并打印出来,就可以知道动画有没有真正运行了。运行上述代码,控制台打印如下所示:

20150403174704189.png

从打印日志的值我们就可以看出,ValueAnimator 确实已经在正常工作了,值在 300 毫秒的时间内从 0 平滑过渡到了 1,而这个计算工作就是由 ValueAnimator 帮助我们完成的。另外 ofFloat() 方法当中是可以传入任意多个参数的,因此我们还可以构建出更加复杂的动画逻辑,比如说将一个值在 5 秒内从 0 过渡到 5,再过渡到 3,再过渡到 10,就可以这样写:

ValueAnimator anim = ValueAnimator.ofFloat(0f, 5f, 3f, 10f);
anim.setDuration(5000);
anim.start();

当然也许你并不需要小数位数的动画过渡,可能你只是希望将一个整数值从 0 平滑地过渡到 100,那么也很简单,只需要调用 ValueAnimator的ofInt() 方法就可以了,如下所示:

ValueAnimator anim = ValueAnimator.ofInt(0, 100);

那么除此之外,我们还可以调用:

  • setStartDelay() 方法设置动画延迟播放的时间
  • 调用setRepeatCount() 方法来设置动画循环播放的次数
  • setRepeatMode() 循环播放的模式,循环模式包括RESTART和REVERSE两种,分别表示重新播放和倒序播放的意思。

2.2 ObjectAnimator

ObjectAnimator 可以让 View 的某个属性按照某种变化规律来变化

相比于 ValueAnimator,ObjectAnimator 可能才是我们最常接触到的类,因为 ValueAnimator 只不过是对值进行了一个平滑的动画过渡,但我们实际使用到这种功能的场景好像并不多。而 ObjectAnimator 则就不同了,它是可以直接对任意对象的任意属性进行动画操作的,比如说 View 的 alpha 属性。

不过虽说 ObjectAnimator 会更加常用一些,但是它其实是继承自 ValueAnimator 的,底层的动画实现机制也是基于 ValueAnimator 来完成的,因此 ValueAnimator 仍然是整个属性动画当中最核心的一个类。那么既然是继承关系,说明 ValueAnimator 中可以使用的方法在 ObjectAnimator 中也是可以正常使用的,它们的用法也非常类似,这里如果我们想要将一个 TextView 在 5 秒中内从常规变换成全透明,再从全透明变换成常规,就可以这样写:

ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
animator.setDuration(5000);
animator.start();

再看几个例子:

ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
animator.setDuration(5000);
animator.start();
float curTranslationX = textview.getTranslationX();
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "translationX", curTranslationX, -500f, curTranslationX);
animator.setDuration(5000);
animator.start();
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "scaleY", 1f, 3f, 1f);
animator.setDuration(5000);
animator.start();

到目前为止,ObjectAnimator 的用法还算是相当简单吧,但是我相信肯定会有不少朋友现在心里都有同样一个疑问,就是 ofFloat() 方法的第二个参数到底可以传哪些值呢?目前我们使用过了 alpha、rotation、translationX 和 scaleY 这几个值,分别可以完成淡入淡出、旋转、水平移动、垂直缩放这几种动画,那么还有哪些值是可以使用的呢?其实这个问题的答案非常玄乎,就是我们可以传入任意的值到 ofFloat() 方法的第二个参数当中。任意的值?相信这很出乎大家的意料吧,但事实就是如此。因为 ObjectAnimator 在设计的时候就没有针对于 View 来进行设计,而是针对于任意对象的,它所负责的工作就是不断地向某个对象中的某个属性进行赋值,然后对象根据属性值的改变再来决定如何展现出来。

那么比如说我们调用下面这样一段代码:

ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f);

其实这段代码的意思就是ObjectAnimator会帮我们不断地改变textview对象中alpha属性的值,从1f变化到0f。然后textview对象需要根据alpha属性值的改变来不断刷新界面的显示,从而让用户可以看出淡入淡出的动画效果。

那么 textview 对象中是不是有 alpha 属性这个值呢?没有,不仅 textview 没有这个属性,连它所有的父类也是没有这个属性的!这就奇怪了,textview 当中并没有 alpha 这个属性,ObjectAnimator 是如何进行操作的呢?其实 ObjectAnimator 内部的工作机制并不是直接对我们传入的属性名进行操作的,而是会去寻找这个属性名对应的 get 和 set 方法,因此 alpha 属性所对应的 get 和 set 方法应该就是:

public void setAlpha(float value);
public float getAlpha();

那么 textview 对象中是否有这两个方法呢?确实有,并且这两个方法是由 View 对象提供的,也就是说不仅 TextView 可以使用这个属性来进行淡入淡出动画操作,任何继承自 View 的对象都可以的。

既然 alpha 是这个样子,相信大家一定已经明白了,前面我们所用的所有属性都是这个工作原理,那么 View 当中一定也存在着 setRotation()、getRotation()、setTranslationX()、getTranslationX()、setScaleY()、getScaleY() 这些方法,不信的话你可以到 View 当中去找一下。

PropertyValuesHolder:

多属性动画同时工作管理类。有时候我们需要同时修改多个属性,那就可以用到此类,具体如下:

PropertyValuesHolder a1 = PropertyValuesHolder.ofFloat("alpha", 0f, 1f);  
PropertyValuesHolder a2 = PropertyValuesHolder.ofFloat("translationY", 0, viewWidth);  
......
ObjectAnimator.ofPropertyValuesHolder(view, a1, a2, ......).setDuration(1000).start();

2.3 组合动画

实现组合动画功能主要需要借助 AnimatorSet 这个类,这个类提供了一个 play() 和 playTogether(a1, a2, …) 方法,如果我们向这个方法中传入一个 Animator 对象(ValueAnimator 或 ObjectAnimator) 将会返回一个 AnimatorSet.Builder 的实例,AnimatorSet.Builder 中包括以下四个方法:

  • after(Animator anim) 将现有动画插入到传入的动画之后执行
  • after(long delay) 将现有动画延迟指定毫秒后执行
  • before(Animator anim) 将现有动画插入到传入的动画之前执行
  • with(Animator anim) 将现有动画和传入的动画同时执行

好的,有了这四个方法,我们就可以完成组合动画的逻辑了,那么比如说我们想要让 TextView 先从屏幕外移动进屏幕,然后开始旋转 360 度,旋转的同时进行淡入淡出操作,就可以这样写:

ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, "translationX", -500f, 0f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).after(moveIn);
animSet.setDuration(5000);
animSet.start();

2.4 Animator 监听器

在很多时候,我们希望可以监听到动画的各种事件,比如动画何时开始,何时结束,然后在开始或者结束的时候去执行一些逻辑处理。这个功能是完全可以实现的,Animator 类当中提供了一个 addListener() 方法,这个方法接收一个 AnimatorListener,我们只需要去实现这个 AnimatorListener 就可以监听动画的各种事件了。

大家已经知道,ObjectAnimator 是继承自 ValueAnimator 的,而 ValueAnimator 又是继承 Animator 的,因此不管是 ValueAnimator 还是 ObjectAnimator 都是可以使用 addListener() 这个方法的。另外 AnimatorSet 也是继承自 Animator 的,因此 addListener() 这个方法算是个通用的方法。

anim.addListener(new AnimatorListener() {
	@Override
	public void onAnimationStart(Animator animation) {
	}
 
	@Override
	public void onAnimationRepeat(Animator animation) {
	}
 
	@Override
	public void onAnimationEnd(Animator animation) {
	}
 
	@Override
	public void onAnimationCancel(Animator animation) {
	}
});
  • onAnimationStart()方法会在动画开始的时候调用
  • onAnimationRepeat()方法会在动画重复执行的时候调用
  • onAnimationEnd()方法会在动画结束的时候调用
  • onAnimationCancel()方法会在动画被取消的时候调用。

但是也许很多时候我们并不想要监听那么多个事件,可能我只想要监听动画结束这一个事件,那么每次都要将四个接口全部实现一遍就显得非常繁琐。没关系,为此 Android 提供了一个适配器类,叫作 AnimatorListenerAdapter,使用这个类就可以解决掉实现接口繁琐的问题了,如下所示:

anim.addListener(new AnimatorListenerAdapter() {
});

这里我们向 addListener() 方法中传入这个适配器对象,由于 AnimatorListenerAdapter 中已经将每个接口都实现好了,所以这里不用实现任何一个方法也不会报错。那么如果我想监听动画结束这个事件,就只需要单独重写这一个方法就可以了,如下所示:

anim.addListener(new AnimatorListenerAdapter() {
	@Override
	public void onAnimationEnd(Animator animation) {
	}
});

2.5 XML 属性动画

2.5.1 xml 基本使用

通过 XML 来编写动画可能会比通过代码来编写动画要慢一些,但是在重用方面将会变得非常轻松,比如某个将通用的动画编写到 XML 里面,我们就可以在各个界面当中轻松去重用它。

如果想要使用XML来编写动画,首先要在 res 目录下面新建一个 animator 文件夹,所有属性动画的 XML 文件都应该存放在这个文件夹当中。然后在 XML 文件中我们一共可以使用如下三种标签:

  • <animator> 对应代码中的 ValueAnimator
  • <objectAnimator> 对应代码中的 ObjectAnimator
  • <set> 对应代码中的 AnimatorSet

那么比如说我们想要实现一个从 0 到 100 平滑过渡的动画,在 XML 当中就可以这样写:

<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:valueFrom="0"
    android:valueTo="100"
    android:valueType="intType"/>

而如果我们想将一个视图的 alpha 属性从 1 变成 0,就可以这样写:

<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:valueFrom="1"
    android:valueTo="0"
    android:valueType="floatType"
    android:propertyName="alpha"/>

另外,我们也可以使用 XML 来完成复杂的组合动画操作,比如将一个视图先从屏幕外移动进屏幕,然后开始旋转 360 度,旋转的同时进行淡入淡出操作,就可以这样写:

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially" >
 
    <objectAnimator
        android:duration="2000"
        android:propertyName="translationX"
        android:valueFrom="-500"
        android:valueTo="0"
        android:valueType="floatType" >
    </objectAnimator>
 
    <set android:ordering="together" >
        <objectAnimator
            android:duration="3000"
            android:propertyName="rotation"
            android:valueFrom="0"
            android:valueTo="360"
            android:valueType="floatType" >
        </objectAnimator>
 
        <set android:ordering="sequentially" >
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="1"
                android:valueTo="0"
                android:valueType="floatType" >
            </objectAnimator>
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="0"
                android:valueTo="1"
                android:valueType="floatType" >
            </objectAnimator>
        </set>
    </set>
 
</set>

最后 XML 文件是编写好了,那么我们如何在代码中把文件加载进来并将动画启动呢?只需调用如下代码即可:

Animator animator = AnimatorInflater.loadAnimator(context, R.animator.anim_file);
animator.setTarget(view);
animator.start();

调用 AnimatorInflater 的 loadAnimator 来将 XML 动画文件加载进来,然后再调用 setTarget() 方法将这个动画设置到某一个对象上面,最后再调用 start() 方法启动动画就可以了,就是这么简单。

2.5.2 xml 官方文档

<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=["repeat" | "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=["repeat" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <set>
        ...
    </set>
</set>

<set> 属性解释:

xml 属性 解释
android:ordering 控制子动画启动方式是先后有序的还是同时进行。sequentially: 动画按照先后顺序;together(默认): 动画同时启动;

<objectAnimator> 属性解释:

xml属性 解释
android:propertyName String 类型,必须要设置的节点属性,代表要执行动画的属性(通过名字引用),辟如你可以指定了一个View的”alpha” 或者 “backgroundColor” ,这个objectAnimator元素没有对外说明target属性,所以你不能在XML中设置执行这个动画,必须通过调用loadAnimator()方法加载你的XML动画资源,然后调用setTarget()应用到具备这个属性的目标对象上(譬如TextView)。
android:valueTo float、int 或者 color 类型,必须要设置的节点属性,表明动画结束的点;如果是颜色的话,由 6 位十六进制的数字表示。
android:valueFrom 相对应 valueTo,动画的起始点,如果没有指定,系统会通过属性的 get 方法获取,颜色也是 6 位十六进制的数字表示。
android:duration 动画的时长,int 类型,以毫秒为单位,默认为 300 毫秒。
android:startOffset 动画延迟的时间,从调用 start 方法后开始计算,int 型,毫秒为单位。
android:repeatCount 一个动画的重复次数,int 型,”-1“表示无限循环,”1“表示动画在第一次执行完成后重复执行一次,也就是两次,默认为0,不重复执行。
android:repeatMode 重复模式:int 型,当一个动画执行完的时候应该如何处理。该值必须是正数或者是-1,“reverse”会使得按照动画向相反的方向执行,可实现类似钟摆效果。“repeat”会使得动画每次都从头开始循环。
android:valueType 关键参数,如果该 value 是一个颜色,那么就不需要指定,因为动画框架会自动的处理颜色值。有 intType 和 floatType(默认)两种:分别说明动画值为 int 和 float 型。

三、属性动画高级用法

3.1 ValueAnimator的高级用法

比如说我们有一个自定义的 View,在这个 View 当中有一个 Point 对象用于管理坐标,然后在 onDraw() 方法当中就是根据这个 Point 对象的坐标值来进行绘制的。也就是说,如果我们可以对 Poin t对象进行动画操作,那么整个自定义 View 的动画效果就有了。OK,下面我们就来学习一下如何实现这样的效果。

在开始动手之前,我们还需要掌握另外一个知识点,就是 TypeEvaluator 的用法。可能在大多数情况下我们使用属性动画的时候都不会用到 TypeEvaluator,但是大家还是应该了解一下它的用法,以防止当我们遇到一些解决不掉的问题时能够想起来还有这样的一种解决方案。

那么 TypeEvaluator 的作用到底是什么呢?简单来说,就是告诉动画系统如何从初始值过度到结束值。我们在上一篇文章中学到的 ValueAnimator.ofFloat() 方法就是实现了初始值与结束值之间的平滑过度,那么这个平滑过度是怎么做到的呢?其实就是系统内置了一个 FloatEvaluator,它通过计算告知动画系统如何从初始值过度到结束值,我们来看一下 FloatEvaluator 的代码实现:

public class FloatEvaluator implements TypeEvaluator {
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        float startFloat = ((Number) startValue).floatValue();
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
    }
}

可以看到,FloatEvaluator 实现了 TypeEvaluator 接口,然后重写 evaluate() 方法。evaluate() 方法当中传入了三个参数,第一个参数 fraction 非常重要,这个参数用于表示动画的完成度的,我们应该根据它来计算当前动画的值应该是多少,第二第三个参数分别表示动画的初始值和结束值。那么上述代码的逻辑就比较清晰了,用结束值减去初始值,算出它们之间的差值,然后乘以 fraction 这个系数,再加上初始值,那么就得到当前动画的值了。

好的,那 FloatEvaluator 是系统内置好的功能,并不需要我们自己去编写,但介绍它的实现方法是要为我们后面的功能铺路的。前面我们使用过了 ValueAnimator 的 ofFloat() 和 ofInt() 方法,分别用于对浮点型和整型的数据进行动画操作的,但实际上 ValueAnimator 中还有一个 ofObject() 方法,是用于对任意对象进行动画操作的。但是相比于浮点型或整型数据,对象的动画操作明显要更复杂一些,因为系统将完全无法知道如何从初始对象过度到结束对象,因此这个时候我们就需要实现一个自己的 TypeEvaluator 来告知系统如何进行过度。

下面来先定义一个 Point 类,如下所示:

public class Point {
 
    private float x;
 
    private float y;
 
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }
 
    public float getX() {
        return x;
    }
 
    public float getY() {
        return y;
    }
 
}

Point 类非常简单,只有x和y两个变量用于记录坐标的位置,并提供了构造方法来设置坐标,以及 get 方法来获取坐标。接下来定义 PointEvaluator,如下所示:

public class PointEvaluator implements TypeEvaluator{
 
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
        Point point = new Point(x, y);
        return point;
    }
 
}

可以看到,PointEvaluator 同样实现了 TypeEvaluator 接口并重写了 evaluate() 方法。其实 evaluate() 方法中的逻辑还是非常简单的,先是将 startValue 和 endValue 强转成 Point 对象,然后同样根据 fraction 来计算当前动画的 x 和 y 的值,最后组装到一个新的 Point 对象当中并返回。

这样我们就将 PointEvaluator 编写完成了,接下来我们就可以非常轻松地对 Point 对象进行动画操作了,比如说我们有两个 Point 对象,现在需要将 Point1 通过动画平滑过度到 Point2,就可以这样写:

Point point1 = new Point(0, 0);
Point point2 = new Point(300, 300);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), point1, point2);
anim.setDuration(5000);
anim.start();

代码很简单,这里我们先是 new 出了两个 Point 对象,并在构造函数中分别设置了它们的坐标点。然后调用 ValueAnimator的ofObject() 方法来构建 ValueAnimator 的实例,这里需要注意的是,ofObject() 方法要求多传入一个 TypeEvaluator 参数,这里我们只需要传入刚才定义好的 PointEvaluator 的实例就可以了。

好的,这就是自定义 TypeEvaluator 的全部用法,掌握了这些知识之后,我们就可以来尝试一下如何通过对 Point 对象进行动画操作,从而实现整个自定义 View 的动画效果。

新建一个 MyAnimView 继承自 View,代码如下所示:

public class MyAnimView extends View {
 
    // 半径
    public static final float RADIUS = 50f;
    // 基点
    private Point currentPoint;
    // 画笔
    private Paint mPaint;
 
    public MyAnimView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }
 
    // 画圆
    @Override
    protected void onDraw(Canvas canvas) {
        if (currentPoint == null) {
            // 为空创建点,画圆,开启动画
            currentPoint = new Point(RADIUS, RADIUS);
            drawCircle(canvas);
            startAnimation();
        } else {
            drawCircle(canvas);
        }
    }
 
    private void drawCircle(Canvas canvas) {
        float x = currentPoint.getX();
        float y = currentPoint.getY();
        canvas.drawCircle(x, y, RADIUS, mPaint);
    }
 
    private void startAnimation() {
        // 起始点
        Point startPoint = new Point(RADIUS, RADIUS);
        // 结束点
        Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
        // 定义一个轨迹,从开始对象到结束对象
        ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 当前点等于轨迹的对象
                currentPoint = (Point) animation.getAnimatedValue();
                // 刷新圆
                invalidate();
            }
        });
        anim.setDuration(5000);
        anim.start();
    }
 
}

基本上还是很简单的,总共也没几行代码。首先在自定义 View 的构造方法当中初始化了一个 Paint 对象作为画笔,并将画笔颜色设置为蓝色,接着在 onDraw() 方法当中进行绘制。这里我们绘制的逻辑是由 currentPoint 这个对象控制的,如果 currentPoint 对象不等于空,那么就调用 drawCircle() 方法在 currentPoint 的坐标位置画出一个半径为 50 的圆,如果 currentPoint 对象是空,那么就调用 startAnimation() 方法来启动动画。

那么我们来观察一下 startAnimation() 方法中的代码,其实大家应该很熟悉了,就是对 Point 对象进行了一个动画操作而已。这里我们定义了一个 startPoint 和一个 endPoint,坐标分别是 View 的左上角和右下角,并将动画的时长设为 5 秒。然后有一点需要大家注意的,就是我们通过监听器对动画的过程进行了监听,每当 Point 值有改变的时候都会回调 onAnimationUpdate() 方法。在这个方法当中,我们对 currentPoint 对象进行了重新赋值,并调用了 invalidate() 方法,这样的话 onDraw() 方法就会重新调用,并且由于 currentPoint 对象的坐标已经改变了,那么绘制的位置也会改变,于是一个平移的动画效果也就实现了。

下面我们只需要在布局文件当中引入这个自定义控件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
 
    <com.example.tony.myapplication.MyAnimView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
 
</RelativeLayout>

20150504225258841.gif

3.2 ObjectAnimator的高级用法

那么大家应该都还记得,我们在吐槽补间动画的时候有提到过,补间动画是只能实现移动、缩放、旋转和淡入淡出这四种动画操作的,功能限定死就是这些,基本上没有任何扩展性可言。比如我们想要实现对 View 的颜色进行动态改变,补间动画是没有办法做到的。

但是属性动画就不会受这些条条框框的限制,它的扩展性非常强,对于动态改变 View 的颜色这种功能是完全可是胜任的,那么下面我们就来学习一下如何实现这样的效果。

大家应该都还记得,ObjectAnimator 内部的工作机制是通过寻找特定属性的 get 和 set 方法,然后通过方法不断地对值进行改变,从而实现动画效果的。因此我们就需要在 MyAnimView 中定义一个 color 属性,并提供它的 get 和 set 方法。这里我们可以将 color 属性设置为字符串类型,使用 #RRGGBB 这种格式来表示颜色值,代码如下所示:

public class MyAnimView extends View {
 
	...
 
	private String color;
 
	public String getColor() {
		return color;
	}
 
	public void setColor(String color) {
		this.color = color;
		mPaint.setColor(Color.parseColor(color));
		invalidate();
	}
 
	...
 
}

注意在 setColor() 方法当中,我们编写了一个非常简单的逻辑,就是将画笔的颜色设置成方法参数传入的颜色,然后调用了 invalidate() 方法。这段代码虽然只有三行,但是却执行了一个非常核心的功能,就是在改变了画笔颜色之后立即刷新视图,然后 onDraw() 方法就会调用。在 onDraw() 方法当中会根据当前画笔的颜色来进行绘制,这样颜色也就会动态进行改变了。

那么接下来的问题就是怎样让 setColor() 方法得到调用了,毫无疑问,当然是要借助 ObjectAnimator 类,但是在使用 ObjectAnimator 之前我们还要完成一个非常重要的工作,就是编写一个用于告知系统如何进行颜色过度的 TypeEvaluator。创建 ColorEvaluator 并实现 TypeEvaluator 接口,代码如下所示:

public class ColorEvaluator implements TypeEvaluator {

    // RGB 的值
    private int mCurrentRed = -1;
    private int mCurrentGreen = -1;
    private int mCurrentBlue = -1;

    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        String startColor = (String) startValue;
        String endColor = (String) endValue;
        // 开始和结束的 rgb 由 16 进制转换为 10 进制
        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 (mCurrentRed == -1) {
            mCurrentRed = startRed;
        }
        if (mCurrentGreen == -1) {
            mCurrentGreen = startGreen;
        }
        if (mCurrentBlue == -1) {
            mCurrentBlue = startBlue;
        }
        // 计算初始颜色和结束颜色之间的差值
        int redDiff = Math.abs(startRed - endRed);
        int greenDiff = Math.abs(startGreen - endGreen);
        int blueDiff = Math.abs(startBlue - endBlue);
        int colorDiff = redDiff + greenDiff + blueDiff;
        // 由进度计算当前 rgb 值
        if (mCurrentRed != endRed) {
            mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
                fraction);
        } else if (mCurrentGreen != endGreen) {
            mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
                redDiff, fraction);
        } else if (mCurrentBlue != endBlue) {
            mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
                redDiff + greenDiff, fraction);
        }
        // 将计算出的当前颜色的值组装返回
        String currentColor = "#" + getHexString(mCurrentRed)
            + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
        return currentColor;
    }

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

}

首先在 evaluate() 方法当中获取到颜色的初始值和结束值,并通过字符串截取的方式将颜色分为 RGB 三个部分,并将 RGB 的值转换成十进制数字,那么每个颜色的取值范围就是 0-255。接下来计算一下初始颜色值到结束颜色值之间的差值,这个差值很重要,决定着颜色变化的快慢,如果初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢,而如果颜色值相差很大,比如说从黑到白,那么就要经历 255*3 这个幅度的颜色过度,变化就会非常快。

那么控制颜色变化的速度是通过 getCurrentColor() 这个方法来实现的,这个方法会根据当前的 fraction 值来计算目前应该过度到什么颜色,并且这里会根据初始和结束的颜色差值来控制变化速度,最终将计算出的颜色进行返回。

最后,由于我们计算出的颜色是十进制数字,这里还需要调用一下 getHexString() 方法把它们转换成十六进制字符串,再将 RGB 颜色拼装起来之后作为最终的结果返回。

好了,ColorEvaluator 写完之后我们就把最复杂的工作完成了,剩下的就是一些简单调用的问题了,比如说我们想要实现从蓝色到红色的动画过度,历时 5 秒,就可以这样写:

ObjectAnimator anim = ObjectAnimator.ofObject(myAnimView, "color", new ColorEvaluator(), 
	"#0000FF", "#FF0000");
anim.setDuration(5000);
anim.start();

组合

接下来我们需要将上面一段代码移到 MyAnimView 类当中,让它和刚才的 Point 移动动画可以结合到一起播放,这就要借助我们在上篇文章当中学到的组合动画的技术了。修改 MyAnimView 中的代码,如下所示:

public class MyAnimView extends View {
 
    ...
 
    private void startAnimation() {
        Point startPoint = new Point(RADIUS, RADIUS);
        Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
        ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentPoint = (Point) animation.getAnimatedValue();
                invalidate();
            }
        });
        ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(), 
        		"#0000FF", "#FF0000");
        AnimatorSet animSet = new AnimatorSet();
        animSet.play(anim).with(anim2);
        animSet.setDuration(5000);
        animSet.start();
    }
 
}

可以看到,我们并没有改动太多的代码,重点只是修改了 startAnimation() 方法中的部分内容。这里先是将颜色过度的代码逻辑移动到了 startAnimation() 方法当中,注意由于这段代码本身就是在 MyAnimView 当中执行的,因此 ObjectAnimator.ofObject() 的第一个参数直接传 this 就可以了。接着我们又创建了一个 AnimatorSet,并把两个动画设置成同时播放,动画时长为五秒,最后启动动画。现在重新运行一下代码,效果如下图所示:
20150504225554203.gif

四、Interpolator

4.1 Interpolator

4.1.1 系统 Interpolator

Interpolator 这个东西很难进行翻译,直译过来的话是补间器的意思,它的主要作用是可以控制动画的变化速率,比如去实现一种非线性运动的动画效果。那么什么叫做非线性运动的动画效果呢?就是说动画改变的速率不是一成不变的,像加速运动以及减速运动都属于非线性运动。

不过 Interpolator 并不是属性动画中新增的技术,实际上从 Android 1.0 版本开始就一直存在 Interpolator 接口了,而之前的补间动画当然也是支持这个功能的。只不过在属性动画中新增了一个 TimeInterpolator 接口,这个接口是用于兼容之前的 Interpolator 的,这使得所有过去的 Interpolator 实现类都可以直接拿过来放到属性动画当中使用,那么我们来看一下现在 TimeInterpolator 接口的所有实现类,如下图所示
20150524114950506.png

可以看到,TimeInterpolator 接口已经有非常多的实现类了,这些都是 Android 系统内置好的并且我们可以直接使用的 Interpolator。每个 Interpolator 都有它各自的实现效果,比如说A ccelerateInterpolator 就是一个加速运动的 Interpolator,而 DecelerateInterpolator 就是一个减速运动的 Interpolator。

我觉得细心的朋友应该早已经发现了,在前面两篇文章当中我们所学到的所有属性动画,其实都不是在进行一种线程运动。比如说我们使用 ValueAnimator 所打印的值如下所示:
20150525114253542.png

可以看到,一开始的值变化速度明显比较慢,仅 0.0 开头的就打印了 4 次,之后开始加速,最后阶段又开始减速,因此我们可以很明显地看出这一个先加速后减速的 Interpolator。

从以上我们就可以总结出一个结论了,使用属性动画时,系统默认的 Interpolator 其实就是一个先加速后减速的 Interpolator,对应的实现类就是 AccelerateDecelerateInterpolator。

当然,我们也可以很轻松地修改这一默认属性,将它替换成任意一个系统内置好的 Interpolator。

我们如下调用即可

private void startAnimation() {
    Point startPoint = new Point(getWidth() / 2, RADIUS);
    Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS);
    ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            currentPoint = (Point) animation.getAnimatedValue();
            invalidate();
        }
    });
    anim.setInterpolator(new AccelerateInterpolator(2f));
    anim.setDuration(2500);
    anim.start();
}
  • AccelerateDecelerateInterpolator:先加速后减速。
  • AccelerateInterpolator:加速。
  • DecelerateInterpolator:减速。
  • AnticipateInterpolator:先向相反方向改变一段再加速播放。
  • AnticipateOvershootInterpolator:先向相反方向改变,再加速播放,会超出目标值然后缓慢移动至目标值,类似于弹簧回弹。
  • BounceInterpolator:快到目标值时值会跳跃。
  • CycleIinterpolator:动画循环一定次数,值的改变为一正弦函数:Math.sin(2 * mCycles * Math.PI * input)
  • LinearInterpolator:线性均匀改变。
  • OvershottInterpolator:最后超出目标值然后缓慢改变到目标值。
  • TimeInterpolator:一个允许自定义 Interpolator 的接口,以上都实现了该接口。

4.1.2 自定义 Interpolator

首先看一下 TimeInterpolator 的接口定义,代码如下所示:

/**
 * A time interpolator defines the rate of change of an animation. This allows animations
 * to have non-linear motion, such as acceleration and deceleration.
 */
public interface TimeInterpolator {
 
    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}

OK,接口还是非常简单的,只有一个 getInterpolation() 方法。大家有兴趣可以通过注释来对这个接口进行详解的了解,这里我就简单解释一下,getInterpolation() 方法中接收一个 input 参数,这个参数的值会随着动画的运行而不断变化,**不过它的变化是非常有规律的,就是根据设定的动画时长匀速增加,变化范围是 0 到 1。**也就是说当动画一开始的时候 input 的值是 0,到动画结束的时候 input 的值是1,而中间的值则是随着动画运行的时长在 0 到 1 之间变化的。

这里的 input 和 fraction 有什么关系或者区别呢?

答案很简单,input 的值决定了 fraction 的值。input 的值是由系统经过计算后传入到 getInterpolation() 方法中的,然后我们可以自己实现 getInterpolation() 方法中的算法,根据 input 的值来计算出一个返回值,而这个返回值就是 fraction 了。
可以理解为速度影响进度。

因此,最简单的情况就是 input 值和 fraction 值是相同的,这种情况由于 input 值是匀速增加的,因而 fraction 的值也是匀速增加的,所以动画的运动情况也是匀速的。系统中内置的 LinearInterpolator 就是一种匀速运动的 Interpolator,那么我们来看一下它的源码是怎么实现的:

/**
 * An interpolator where the rate of change is constant
 */
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
 
    public LinearInterpolator() {
    }
 
    public LinearInterpolator(Context context, AttributeSet attrs) {
    }
 
    public float getInterpolation(float input) {
        return input;
    }
 
    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createLinearInterpolator();
    }
}

这里我们只看 getInterpolation() 方法,这个方法没有任何逻辑,就是把参数中传递的 input 值直接返回了,因此 fraction 的值就是等于 input 的值的,这就是匀速运动的 Interpolator 的实现方式。

当然这是最简单的一种 Interpolator 的实现了,我们再来看一个稍微复杂一点的。既然现在大家都知道了系统在默认情况下使用的是 AccelerateDecelerateInterpolator,那我们就来看一下它的源码吧,如下所示:

/**
 * An interpolator where the rate of change starts and ends slowly but
 * accelerates through the middle.
 * 
 */
@HasNativeInterpolator
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {
    public AccelerateDecelerateInterpolator() {
    }
    
    @SuppressWarnings({"UnusedDeclaration"})
    public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
    }
    
    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }
 
    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
    }
}

代码虽然没有变长很多,但是 getInterpolation() 方法中的逻辑已经明显变复杂了,不再是简单地将参数中的 input 进行返回,而是进行了一个较为复杂的数学运算。那这里我们来分析一下它的算法实现,可以看到,算法中主要使用了余弦函数,由于 input 的取值范围是 0 到 1,那么 cos 函数中的取值范围就是 π 到 2π。而 cos(π) 的结果是 -1,cos(2π) 的结果是 1,那么这个值再除以 2 加上 0.5 之后, getInterpolation() 方法最终返回的结果值还是在 0 到 1 之间。只不过经过了余弦运算之后,最终的结果不再是匀速增加的了,而是经历了一个先加速后减速的过程。我们可以将这个算法的执行情况通过曲线图的方式绘制出来,结果如下图所示:20150530221909033.png

可以看到,这是一个 S 型的曲线图,当横坐标从 0 变化到 0.2 的时候,纵坐标的变化幅度很小,但是之后就开始明显加速,最后横坐标从 0.8 变化到 1 的时候,纵坐标的变化幅度又变得很小。

OK,通过分析 LinearInterpolator 和 AccelerateDecelerateInterpolator 的源码,我们已经对 Interpolator 的内部实现机制有了比较清楚的认识了,那么接下来我们就开始尝试编写一个自定义的 Interpolator。

编写自定义 Interpolator 最主要的难度都是在于数学计算方面的,由于我数学并不是很好,因此这里也就写一个简单点的 Interpolator 来给大家演示一下。既然属性动画默认的 Interpolator 是先加速后减速的一种方式,这里我们就对它进行一个简单的修改,让它变成先减速后加速的方式。新建 DecelerateAccelerateInterpolator 类,让它实现 TimeInterpolator 接口,代码如下所示:

public class DecelerateAccelerateInterpolator implements TimeInterpolator{
 
    @Override
    public float getInterpolation(float input) {
        float result;
        if (input <= 0.5) {
            result = (float) (Math.sin(Math.PI * input)) / 2;
        } else {
            result = (float) (2 - Math.sin(Math.PI * input)) / 2;
        }
        return result;
    }
 
}

这段代码是使用正弦函数来实现先减速后加速的功能的,因为正弦函数初始弧度的变化值非常大,刚好和余弦函数是相反的,而随着弧度的增加,正弦函数的变化值也会逐渐变小,这样也就实现了减速的效果。当弧度大于 π/2 之后,整个过程相反了过来,现在正弦函数的弧度变化值非常小,渐渐随着弧度继续增加,变化值越来越大,弧度到 π 时结束,这样从 0 过度到 π,也就实现了先减速后加速的效果。

同样我们可以将这个算法的执行情况通过曲线图的方式绘制出来,结果如下图所示:

20150530221803332.png

可以看到,这也是一个 S 型的曲线图,只不过曲线的方向和刚才是相反的。从上图中我们可以很清楚地看出来,一开始纵坐标的变化幅度很大,然后逐渐变小,横坐标到 0.5 的时候纵坐标变化幅度趋近于零,之后随着横坐标继续增加纵坐标的变化幅度又开始变大,的确是先减速后加速的效果。

使用

anim.setInterpolator(new DecelerateAccelerateInterpolator());

五、ViewPropertyAnimator

5.1 基本使用

我们都知道,属性动画的机制已经不是再针对于 View 而进行设计的了,而是一种不断地对值进行操作的机制,它可以将值赋值到指定对象的指定属性上。但是,在绝大多数情况下,我相信大家主要都还是对 View 进行动画操作的。Android 开发团队也是意识到了这一点,没有为 View 的动画操作提供一种更加便捷的用法确实是有点太不人性化了,于是在 Android 3.1 系统当中补充了 ViewPropertyAnimator 这个机制。

那我们先来回顾一下之前的用法吧,比如我们想要让一个 TextView 从常规状态变成透明状态,就可以这样写:

ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 0f);
animator.start();

看上去复杂吗?好像也不怎么复杂,但确实也不怎么容易理解。我们要将操作的 view、属性、变化的值都一起传入到 ObjectAnimator.ofFloat() 方法当中,虽然看上去也没写几行代码,但这不太像是我们平时使用的面向对象的思维。

那么下面我们就来看一下如何使用 ViewPropertyAnimator 来实现同样的效果,ViewPropertyAnimator 提供了更加易懂、更加面向对象的API,如下所示:

textview.animate().alpha(0f);

果然非常简单!不过 textview.animate() 这个方法是怎么回事呢?animate() 方法就是在 Android 3.1 系统上新增的一个方法,这个方法的返回值是一个 ViewPropertyAnimator 对象,也就是说拿到这个对象之后我们就可以调用它的各种方法来实现动画效果了,这里我们调用了 alpha() 方法并传入 0,表示将当前的 textview 变成透明状态。

怎么样?比起使用 ObjectAnimator,ViewPropertyAnimator 的用法明显更加简单易懂吧。除此之外,ViewPropertyAnimator 还可以很轻松地将多个动画组合到一起,比如我们想要让 textview 运动到 500,500 这个坐标点上,就可以这样写:

textview.animate().x(500).y(500);

可以看出,ViewPropertyAnimator 是支持连缀用法的,我们想让 textview 移动到横坐标 500 这个位置上时调用了 x(500) 这个方法,然后让 textview 移动到纵坐标 500 这个位置上时调用了 y(500) 这个方法,将所有想要组合的动画通过这种连缀的方式拼接起来,这样全部动画就都会一起被执行。

那么怎样去设定动画的运行时长呢?很简单,也是通过连缀的方式设定即可,比如我们想要让动画运行 5 秒钟,就可以这样写:

textview.animate().x(500).y(500).setDuration(5000);

除此之外,本篇文章第一部分所学的 Interpolator 技术我们也可以应用在 ViewPropertyAnimator 上面,如下所示:

textview.animate().x(500).y(500).setDuration(5000)
		.setInterpolator(new BounceInterpolator());

用法很简单,同样也是使用连缀的方式。相信大家现在都已经体验出来了,ViewPropertyAnimator 其实并没有什么太多的技巧可言,用法基本都是大同小异的,需要用到什么功能就连缀一下,因此更多的用法大家只需要去查阅一下文档,看看还支持哪些功能,有哪些接口可以调用就可以了。

那么除了用法之外,关于 ViewPropertyAnimator 有几个细节还是值得大家注意一下的:

  • 整个 ViewPropertyAnimator 的功能都是建立在 View 类新增的 animate() 方法之上的,这个方法会创建并返回一个 ViewPropertyAnimator 的实例,之后的调用的所有方法,设置的所有属性都是通过这个实例完成的。
  • 大家注意到,在使用 ViewPropertyAnimator 时,我们自始至终没有调用过 start() 方法,这是因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成之后,动画就会自动启动。并且这个机制对于组合动画也同样有效,只要我们不断地连缀新的方法,那么动画就不会立刻执行,等到所有在 ViewPropertyAnimator 上设置的方法都执行完毕后,动画就会自动启动。当然如果不想使用这一默认机制的话,我们也可以显式地调用 start() 方法来启动动画。
  • ViewPropertyAnimator 的所有接口都是使用连缀的语法来设计的,每个方法的返回值都是它自身的实例,因此调用完一个方法之后可以直接连缀调用它的另一个方法,这样把所有的功能都串接起来,我们甚至可以仅通过一行代码就完成任意复杂度的动画功能。

5.2 文档

Method Discription
alpha(float value) 设置透明度,value表示变化到多少,1不透明,0全透明。
scaleY(float value) 设置Y轴方向的缩放大小,value表示缩放到多少。1表示正常规格。小于1代表缩小,大于1代表放大。
scaleX(float value) 设置X轴方向的缩放大小,value表示缩放到多少。1表示正常规格。小于1代表缩小,大于1代表放大。
translationY(float value) 设置Y轴方向的移动值,作为增量来控制View对象相对于它父容器的左上角坐标偏移的位置,即移动到哪里。
translationX(float value) 设置X轴方向的移动值,作为增量来控制View对象相对于它父容器的左上角坐标偏移的位置。
rotation(float value) 控制View对象围绕支点进行旋转, rotation针对2D旋转
rotationX (float value) 控制View对象围绕X支点进行旋转, rotationX针对3D旋转
rotationY(float value) 控制View对象围绕Y支点进行旋转, rotationY针对3D旋转
x(float value) 控制View对象相对于它父容器的左上角坐标在X轴方向的最终位置。
y(float value) 控制View对象相对于它父容器的左上角坐标在Y轴方向的最终位置
void cancel() 取消当前正在执行的动画
setListener(Animator.AnimatorListener listener) 设置监听器,监听动画的开始,结束,取消,重复播放
setUpdateListener(ValueAnimator.AnimatorUpdateListener listener) 设置监听器,监听动画的每一帧的播放
setInterpolator(TimeInterpolator interpolator) 设置插值器
setStartDelay(long startDelay) 设置动画延长开始的时间
setDuration(long duration) 设置动画执行的时间
withLayer() 设置是否开启硬件加速
withStartAction(Runnable runnable) 设置用于动画监听开始(Animator.AnimatorListener)时运行的Runnable任务对象
withEndAction(Runnable runnable) 设置用于动画监听结束(Animator.AnimatorListener)时运行的Runnable任务对象

六、LayoutAnimator

Property 动画系统还提供了对 ViewGroup 中 View 添加时的动画功能,我们可以用 LayoutTransition 对 ViewGroup 中的 View 进行动画设置显示。LayoutTransition 的动画效果都是设置给 ViewGroup,然后当被设置动画的 ViewGroup 中添加删除 View 时体现出来。 该类用于当前布局容器中有 View 添加、删除、隐藏、显示等时候定义布局容器自身的动画和 View 的动画,也就是说当在一个 LinerLayout 中隐藏一个 View 的时候,我们可以自定义整个由于 LinerLayout 隐藏 View 而改变的动画,同时还可以自定义被隐藏的 View 自己消失时候的动画等。

我们可以发现 LayoutTransition 类中主要有五种容器转换动画类型,具体如下:

  • LayoutTransition.APPEARING:当View出现或者添加的时候View出现的动画。
  • LayoutTransition.CHANGE_APPEARING:当添加View导致布局容器改变的时候整个布局容器的动画。
  • LayoutTransition.DISAPPEARING:当View消失或者隐藏的时候View消失的动画。
  • LayoutTransition.CHANGE_DISAPPEARING:当删除或者隐藏View导致布局容器改变的时候整个布局容器的动画。
  • LayoutTransition.CHANGE:当不是由于View出现或消失造成对其他View位置造成改变的时候整个布局容器的动画。

6.1 xml 使用方式

我们可以通过如下方式使用系统提供的默认 ViewGroup的LayoutTransition 动画:

android:animateLayoutChanges=”true”

在 ViewGroup 添加如上 xml 属性默认是没有任何动画效果的,因为前面说了,该动画针对于 ViewGroup 内部东东发生改变时才有效, 所以当我们设置如上属性然后调运 ViewGroup 的 addView、removeView 方法时就能看见系统默认的动画效果了。

还有一种就是通过如下方式设置:

android:layoutAnimation=”@anim/customer_anim”

通过这种方式就能实现很多吊炸天的动画,并在加载布局的时候就会自动播放 layout-animtion。其中设置的动画位于 res/anim 目录下的动画资源(如下):

<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        android:delay="30%"
        android:animationOrder="reverse"
        android:animation="@anim/slide_right"/>

每个属性的作用:

  • android:delay 表示动画播放的延时,既可以是百分比,也可以是 float 小数。
  • android:animationOrder 表示动画的播放顺序,有三个取值 normal(顺序)、reverse(反序)、random(随机)。
  • android:animation 指向了子控件所要播放的动画。

6.2 java 使用方式

在使用 LayoutTransition 时,你可以自定义这几种事件类型的动画,也可以使用默认的动画,总之最终都是通过 setLayoutTransition(LayoutTransition lt) 方法把这些动画以一个 LayoutTransition 对象设置给一个 ViewGroup。

譬如实现如上 Xml 方式的默认系统 LayoutTransition 动画如下:

mTransitioner = new LayoutTransition();
mViewGroup.setLayoutTransition(mTransitioner);

如果在 xml 中文件已经写好 LayoutAnimation,可以使用 AnimationUtils 直接加载:

AnimationUtils.loadLayoutAnimation(context, id)

另外还可以手动 java 代码编写,如:

//通过加载XML动画设置文件来创建一个Animation对象;
Animation animation=AnimationUtils.loadAnimation(this, R.anim.slide_right);   //得到一个LayoutAnimationController对象;
LayoutAnimationController controller = new LayoutAnimationController(animation);   //设置控件显示的顺序;
controller.setOrder(LayoutAnimationController.ORDER_REVERSE);   //设置控件显示间隔时间;
controller.setDelay(0.3);   //为ListView设置LayoutAnimationController属性;
listView.setLayoutAnimation(controller);
listView.startLayoutAnimation();

6.3 LayoutTransition 的用法

稍微再高端一点吧,我们来自定义这几类事件的动画,分别实现他们,那么你可以像下面这么处理:

mTransitioner = new LayoutTransition();
......
ObjectAnimator anim = ObjectAnimator.ofFloat(this, "scaleX", 0, 1);
......//设置更多动画
mTransition.setAnimator(LayoutTransition.APPEARING, anim);
......//设置更多类型的动画
mViewGroup.setLayoutTransition(mTransitioner);