Property Animator详解

前言

Android的动画有两种:ViewAnimation(补间动画) 和 PropertyAnimator(属性动画)。其中ViewAnimation还包括了Frame Animation(帧动画),由于帧动画是对多张图片的播放形成的动画,对性能消耗比较大,这里不做介绍。

为什么在Android3.0后会增加PerpertyAnimator呢?原因有其下几点:

  • ViewAnimation只支持缩放、平移、旋转、透明度这四种动画,PropertyAnimator则可以对任何属性做动画,能实现补间动画无法实现的功能;
  • ViewAnimation仅能对指定的控件做动画,而Property Animator是通过改变控件某一属性值来做动画的;
  • 因为上一点,ViewAnimation能对控件做动画,但并没有改变控件内部的属性值,所以会出现一个奇怪的现象:我们将一个按钮从左上角利用补间动画将其移动到右下角,在移动过程中和移动后,这个按钮都是不会响应点击事件的。而Property Animator则是恰恰相反,Property Animator是通过改变控件内部的属性值来达到动画效果的

开始

那么我们现在就开始PropertyAnimator学习之路,本篇讲会带来以下内容的

  • ValueAnimator
  • Evaluator
  • ObjectAnimator
  • PropertyValuesHolder
  • Keyframe
  • AnimatorSet

ValueAnimator

从命名上可以知道,这是一个针对值的动画!ValueAnimator不会对控件做任何操作,我们可以给它设定从哪个值运动到哪个值,通过监听这些值的渐变过程来自己操作控件。

看看代码ValueAnimatorActivity.java

public class ValueAnimatorActivity extends AppCompatActivity implements View.OnClickListener {

    private ValueAnimator valueAnimator;
    private View mView;

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

        setTitle("ValueAnimator");
        Button btn_start = (Button) findViewById(R.id.btn_start);

        mView = findViewById(R.id.view);
        valueAnimator = ValueAnimator.ofInt(100, 500);
        valueAnimator.setDuration(1000);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value = (int) animation.getAnimatedValue();
                mView.layout(mView.getLeft(), value, mView.getRight(), mView.getHeight() + value);
            }
        });

        btn_start.setOnClickListener(this);
        mView.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_start:
                valueAnimator.start();
                break;
            case R.id.view:
                Toast.makeText(this, "view is clicked!", Toast.LENGTH_SHORT).show();
                break;

        }
    }
}

以上代码定义了一个View从屏幕高度100的地方移动到屏幕高度500的地方。

ValueAnimator的使用很简单:

第一步:创建ValueAnimator实例:

ValueAnimator valueAnimator = ValueAnimator.ofInt(100, 500);  
valueAnimator.setDuration(1000);  
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());  

这里定义了一个int值在1000ms内从100增加到500的动画,相应的也有ofFloat()方法。

第二部:添加监听

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value = (int) animation.getAnimatedValue();
                mView.layout(mView.getLeft(), value, mView.getRight(),       mView.getHeight() + value);
            }
        });

这里我们addUpdateListener添加了一个监听器,在监听传回的结果中,是表示当前状态的ValueAnimator实例,我们通过animation.getAnimatedValue()得到当前值,这样我们就可以通过该值对属性做动画了。

Evaluator

我们先不讲什么是Evaluator,我们先来看一张图:

这幅图讲述了从定义动画的数字区间到通过AnimatorUpdateListener中得到当前动画所对应数值的整个过程。下面我们对这四个步骤具体讲解一下:
(1)ofInt(0,400)表示指定动画的数字区间,是从0运动到400;
(2)、加速器:上面我们讲了,在动画开始后,通过加速器会返回当前动画进度所对应的数字进度,但这个数字进度是百分制的,以小数表示,如0.2
(3)、Evaluator:我们知道我们通过监听器拿到的是当前动画所对应的具体数值,而不是百分制的进度。那么就必须有一个地方会根据当前的数字进度,将其转化为对应的数值,这个地方就是Evaluator;Evaluator就是将从加速器返回的数字进度转成对应的数字值。
(4)、监听器:我们通过在AnimatorUpdateListener监听器使用animation.getAnimatedValue()函数拿到Evaluator中返回的数字值。
Evaluator其实就是一个转换器,他能把小数进度转换成对应的数值位置

系统内置有几种Evaluation:
IntEvaluator:ValueAnimator.ofInt()的默认Evaluation
FloatEvaluator:ValueAnimator.ofFloat()的默认Evaluation
ArgbEvalutor:用来做颜色值过渡转换的。

Evaluator的使用很简单:

ValueAnimator animator = ValueAnimator.ofInt(0,600);  

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
    @Override  
    public void onAnimationUpdate(ValueAnimator animation) {  
        int curValue = (int)animation.getAnimatedValue();  
        tv.layout(tv.getLeft(),curValue,tv.getRight(),curValue+tv.getHeight());  
    }  
});  
animator.setDuration(1000);  
animator.setEvaluator(new IntEvaluator());  
animator.setInterpolator(new BounceInterpolator());  
animator.start();  

让我们看看IntEvaluator的实现:

IntEvaluator.java

public class IntEvaluator implements TypeEvaluator<Integer> {  

    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {  
        int startInt = startValue;  
        return (int)(startInt + fraction * (endValue - startInt));  
    }  
}  

这里fraction就是插值器中返回的input时间进度,startValue和endValue是数值区间,那么 startInt + fraction * (endValue - startInt) 这个公式就很好理解了,它表示动画数值根据时间进度的变化,相信大家一看就明白了。

ObjectAnimator

好了,现在到我们属性动画的重头戏了,前面讲的ValueAnimator只能对数值进行动画,需要自己手动监听动画进度,Android当然不会让我们做这么繁琐的事情,为了能让动画直接与对应控件相关联,以使我们从监听动画过程中解放出来,谷歌的开发人员在ValueAnimator的基础上,又派生了一个类ObjectAnimator;

以ObjectAnimator的ofFloat方法为例:

public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)  

第一个参数用于指定这个动画要操作的是哪个控件;
第二个参数用于指定这个动画要操作这个控件的哪个属性;
第三个参数是可变长参数,这个就跟ValueAnimator中的可变长参数的意义一样了,就是指这个属性值是从哪变到哪。

实现旋转效果,从0旋转到180,再旋转到0:

ObjectAnimator.ofFloat(view, "rotation", 0, 180, 0).setDuration(2000);  

实现透明度从0渐变到1,再变为0:

mAlphaAnim = ObjectAnimator.ofFloat(view, "alpha", 1, 0, 1).setDuration(2000);  

实现X轴放大缩小

mScaleAnim = ObjectAnimator.ofFloat(view, "scaleX", 1, 2, -2, 1).setDuration(2000);  

实现X轴左右平移

mTranslateAnim = ObjectAnimator.ofFloat(view, "translationY", 0, -100, 100, 0).setDuration(2000);  
ObjectAnimator动画原理

由上图可知,ObjectAnimator的唯一不同就是最后调用了setProperty方法,实现View的刷新。这里的set方法的拼接是根据驼峰规则,比如:scaleX属性,那么它的set方法就是setScaleX,这个大家应该很熟悉了。这里需要强调的一点是:View对象可以没有作用的属性,但必须要有对应的setProperty方法,而且在setProperty里面会对View进行重绘。
属性动画的默认帧率是10ms/帧,意思就是每10ms就会调用一次setProperty()来刷新View。

关于getPropert()需要注意的地方
public static ObjectAnimator **ofFloat**(Object target, String propertyName, float... values)  
public static ObjectAnimator **ofInt**(Object target, String propertyName, int... values)  
public static ObjectAnimator **ofObject**(Object target, String propertyName,TypeEvaluator evaluator, Object... values)  

这三个方法的最后一个参数values是一个可变长度的类型,它们表示value的值从第一个变到第二个再到第三个...,那么如果只设置1个值的话,会是什么效果呢?这时候就会调用目标View的getProperty()方法,比如:
ObjectAnimator.ofFloat(view, "rotation", 180).setDuration(2000); 这样的话就会调用getRatation()来初始化动画默认值,如果这个View没有提供该方法,则使用参数类型的默认值,比如int就是0,同时控制台会报出一个错误:

    Method getProperty() with type int not found on target...

PropertyValuesHolder

PropertyValuesHolder保存了动画过程中所需要操作的属性和对应的值,然后通过ObjectAnimator的ofPropertyValuesHolder()讲这些定义好的PropertyValuesHolder作用到View上面,这个效果很像AnimatorSet(后面说到)。

public static PropertyValuesHolder ofFloat(String propertyName, float... values)  
public static PropertyValuesHolder ofInt(String propertyName, int... values)  

PropertyValuesHolder的使用方法和ObjectAnimator相似,只是少了一个Object参数,因为这个Object是要在后面统一设置的。

public static ObjectAnimator ofPropertyValuesHolder(Object target,PropertyValuesHolder... values)  

通过该方法把多个PropertyValuesHolder作用在TargetObject上,返回一个ObjectAnimator,之后就不再赘述了。

详见PropertyValuesHolderActivity.java

Keyframe(关键帧)

通过前面几篇的讲解,我们知道如果要控制动画速率的变化,我们可以通过自定义插值器,也可以通过自定义Evaluator来实现。但是自定义插值器或者估值器的话需要掌握一些数学变换的知识,那有没有办法可以方便实现呢?Keyframe就应运而生了。

关键帧这个概念是从动画里学来的,我们知道视频里,一秒要播放24帧图片,对于制作flash动画的同学来讲,是不是每一帧都要画出来呢?当然不是了,如果每一帧都画出来,那估计做出来一个动画片都得要一年时间;比如我们要让一个球在30秒时间内,从(0,0)点运动到(300,200)点,那flash是怎么来做的呢,在flash中,我们只需要定义两个关键帧,在动画开始时定义一个,把球的位置放在(0,0)点;在30秒后,再定义一个关键帧,把球的位置放在(300,200)点。在动画 开始时,球初始在是(0,0)点,30秒时间内就adobe flash就会自动填充,把球平滑移动到第二个关键帧的位置(300,200)点;
通过上面分析flash动画的制作原理,我们知道,一个关键帧必须包含两个原素,第一时间点,第二位置

public static Keyframe ofFloat(float fraction, float value);  

fraction是时间点
value是具体数值

通过定义多个KeyFrame,调用PropertyValuesHolder的ofKeyframe生成PropertyValuesHolder

PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values)  

后面的使用就不用我多说了吧,和PropertyValuesHolder的一致了。

下面看一下使用KeyFrame实现电话响铃的效果:

关键代码如下:

Keyframe frame1 = Keyframe.ofFloat(0.1f, 0);  
Keyframe frame2 = Keyframe.ofFloat(0.2f, 0);  
Keyframe frame3 = Keyframe.ofFloat(0.3f, 20);  
Keyframe frame4 = Keyframe.ofFloat(0.4f, -20);  
Keyframe frame5 = Keyframe.ofFloat(0.5f, 60);  
Keyframe frame6 = Keyframe.ofFloat(0.6f, -50);  
Keyframe frame7 = Keyframe.ofFloat(0.7f, 45);  
Keyframe frame8 = Keyframe.ofFloat(0.8f, -20);  
Keyframe frame9 = Keyframe.ofFloat(0.9f, -10);  
Keyframe frame10 = Keyframe.ofFloat(1.0f, 0);

Keyframe frame11 = Keyframe.ofFloat(0.1f, 1);  
Keyframe frame12 = Keyframe.ofFloat(0.4f, 1);  
Keyframe frame13 = Keyframe.ofFloat(0.5f, 1.2f);  
Keyframe frame14 = Keyframe.ofFloat(0.5f, 1.3f);  
Keyframe frame15 = Keyframe.ofFloat(1.0f, 1f);

PropertyValuesHolder rotationXHolder = PropertyValuesHolder.ofKeyframe("rotation", frame1, frame2, frame3, frame4, frame5, frame6, frame7, frame8, frame9, frame10);

PropertyValuesHolder scaleHolder = PropertyValuesHolder.ofKeyframe("scaleX", frame11, frame12, frame13, frame14, frame15);

valueAnimator = ObjectAnimator.ofPropertyValuesHolder(iv_tel, rotationXHolder,scaleHolder)  
                .setDuration(2000);

AnimatorSet

AnimatorSet是动画集合的意思,但我觉得它更像是一个动画播放器,有的人可能会问,之前提到的PropertyValuesHolder也可以实现多种动画吗?为什么还会有AnimatorSet这个东西?那这里就先说说它们有什么区别:

  • PropertyValuesHolder作用的View对象只能是同一个,而且每个PropertyValuesHolder的动画执行时间是一致的;
  • AnimatorSet 可以设置顺序播放和同时播放,还能自定义动画播放顺序。

playSequentially,playTogether

想必大家都看到赛马,在赛马开始前,每个马都会被放在起点的小门后面,到点了,门打开,马开始一起往前跑。而假如我们把每匹马看做是一个动画,那我们的playTogether就相当于赛马场里每个赛道上门的意义(当比赛开始时,每个赛道上的门会打开,马就可以开始比赛了);也就是说,playTogether只是一个时间点上的一起开始,对于开始后,各个动画怎么操作就是他们自己的事了,至于各个动画结不结束也是他们自已的事了。所以最恰当的描述就是门只负责打开,打开之后马咋跑,门也管不着,最后,马回不回来跟门也没啥关系。门的责任只是到点就打开而已。放在动画上,就是在激活动画之后,动画开始后的操作只是动画自己来负责。至于动画结不结束,也只有动画自己知道。
而playSequentially的意义就是当一匹马回来以后,再放另一匹。那如果上匹马永远没回来,那下一匹马也永远不会被放出来。放到动画上,就是把激活一个动画之后,动画之后的操作就是动画自己来负责了,这个动画结束之后,再激活下一个动画。如果上一个动画没有结束,那下一个动画就永远也不会被激活

使用方法也很简单:

AnimatorSet animatorSet = new AnimatorSet();

Animator animator1 = ObjectAnimator.ofArgb(btn_1, "backgroundColor", 0xffff00ff, 0xffffff00, 0xffff00ff).setDuration(2000);  
Animator animator2 = ObjectAnimator.ofFloat(btn_2, "scaleX", 1, 2, -2, 1).setDuration(2000);  
Animator animator3 = ObjectAnimator.ofFloat(btn_1, "translationY", 0, -100, 100, 0).setDuration(2000);

animatorSet.playSequentially(animator1,animator2,animator3);  
animatorSet.start();  

AnimatorSet.Builder

AnimatorSet.Builder可以自由设置动画的顺序,上面我们讲了playTogether和playSequentially,分别能实现一起开始动画和逐个开始动画。但并不是非常自由的组合动画,比如我们有三个动画A,B,C我们想先播放C然后同时播放A和B。利用playTogether和playSequentially是没办法实现的,所以为了更方便的组合动画,谷歌的开发人员另外给我们提供一个类AnimatorSet.Builder;

主要方法:
调用AnimatorSet中的play方法是获取AnimatorSet.Builder对象的唯一途径

public Builder play(Animator anim)  表示要播放哪个动画  
public Builder with(Animator anim)  和前面动画一起执行  
public Builder before(Animator anim)  执行前面的动画后才执行该动画  
public Builder after(Animator anim)  执行先执行这个动画再执行前面动画  
public Builder after(long delay)  //延迟n毫秒之后执行动画  

使用:

ObjectAnimator tv1BgAnimator = ObjectAnimator.ofInt(mTv1, "BackgroundColor",  0xffff00ff, 0xffffff00, 0xffff00ff);  
ObjectAnimator tv1TranslateY = ObjectAnimator.ofFloat(mTv1, "translationY", 0, 400, 0);  
ObjectAnimator tv2TranslateY = ObjectAnimator.ofFloat(mTv2, "translationY", 0, 400, 0);

AnimatorSet animatorSet = new AnimatorSet();  
animatorSet.play(tv1TranslateY).with(tv2TranslateY).after(tv1BgAnimator);  
animatorSet.setDuration(2000);  
animatorSet.start();  

主要看这句代码:

    animatorSet.play(tv1TranslateY).with(tv2TranslateY).after(tv1BgAnimator);  

首先先定义执行tv1TranslateY;接着定义tv2TranslateY,让它和前面的tv1TranslateY一起执行;最后在它们前面执行tv1BgAnimator。跟着with、after、before字面上的意思很容易就能理解了。

总结

到这里我们就把属性动画的常用类给讲了一遍,没有什么特别难理解的地方,但内容比较多需要大家好好消化一下,这里奉上Demo代码。
Demo GitHub
参考 Android自定义控件三部曲文章索引