属性动画在Android自定义View中的应用场景

王春蕾
Android中动画的类型,按照系统版本可以简单的分为两大类型,一种是传统的动画,也就是Android中最常用的View动画,即帧动画和补间动画;另一种是Android3.0以后支持的PropertyAnimation,即属性动画。这两大类型的动画虽然在实现一些动画效果上有异曲同工之处,但实现方式和使用场景还是有较大的差别。  

View动画

帧动画和补间动画是安卓中最常见的动画,表现形式有AlphaAnimation(透明度动画)、TranslateAnimation(位移动画)、ScaleAnimation(缩放动画)、RotateAnimation(旋转动画)、AnimationSet(动画集合)以及帧动画这些类型。可以通过xml方式定义动画动作,也可以通过代码方式定义动画动作。以位移动画为例,这两种动画方式的典型使用方式如下:

1.xml定义动画

Step.1:在res/anim资源文件夹下创建translate动画xml文件,代码如下:

<translate xmlns:android="http://schemas.android.com/apk/res/android"  
        android:duration="2000"
        android:fillAfter="true"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:repeatCount="3"
        android:toXDelta="0"
        android:toYDelta="100"/>

Step.2:代码中引入xml动画,设置到对应的View上并启动动画,关键代码如下:

Animation translateAnimation = AnimationUtils.loadAnimation(MainActivity.this,R.anim.translation);  
        imageView.setAnimation(translateAnimation);
        imageView.startAnimation(translateAnimation);

以上为xml声明动画的典型使用方式。

2.代码声明动画

代码声明动画的步骤相对xml声明动画的方式,不需要额外再res/anim中声明动画xml文件,而是通过代码的方式直接声明动画及播放动画时的相关动作,虽然步骤貌似比xml文件更少,但是代码中声明动画相对于xml声明动画会更加复杂一下。代码如下:

TranslateAnimation translateAnimation1 = new TranslateAnimation(TranslateAnimation.ABSOLUTE, mIvImg.getWidth(), TranslateAnimation.ABSOLUTE, mIvImg  
                .getWidth() * 2f, TranslateAnimation.RELATIVE_TO_SELF, 0f, TranslateAnimation.RELATIVE_TO_SELF, 0);
        //设置动画持续时长
        translateAnimation1.setDuration(3000);
        //设置动画结束之后的状态是否是动画的最终状态,true,表示是保持动画结束时的最终状态
        translateAnimation1.setFillAfter(true);
        //设置动画结束之后的状态是否是动画开始时的状态,true,表示是保持动画开始时的状态
        translateAnimation1.setFillBefore(true);
        //设置动画的重复模式:反转REVERSE和重新开始RESTART
        translateAnimation1.setRepeatMode(ScaleAnimation.REVERSE);
        //设置动画播放次数
        translateAnimation1.setRepeatCount(ScaleAnimation.INFINITE);
        //开始动画
        mIvImg.startAnimation(translateAnimation1);

对于帧动画和补间动画,对于Android开发者应该是非常熟悉,在此对使用方式和相关的api不做过多介绍,后续会分析帧动画/补间动画和属性动画的不同之处及各自的使用场景等。


属性动画

1.什么是属性动画?

属性动画是Android3.0以后系统提供的另外一种“动画”方式,这种“动画”方式并不是直接对页面或者相关的View展示动态效果,它适用的对象也不局限在View上,可以理解为它可以使用在任何有“渐进”数据变化的对象上面,它作用的也不是对象本身而是对象中的相关提供了Getter和Setter的属性。所以严格意义上来说,PropertyAnimation并非是一个View动画支持库,而是数据在t秒内从x转变成y的过程数据渐进变更库。 正因为PropertyAnimator并非严格意义上的View动画支持库,它可以监控一个属性在特定时间内的“演进过程”,就可以做一些View动画(帧动画/补间动画)做不到的事情,在实现特定的动画效果上也有非常优秀的表现能力。

2.属性动画的分类

属性动画有ObjectAnimator、ValueAnimator以及AnimatorSet三种实现形式。在使用方式上,属性动画和View动画一样有xml声明和代码声明两种方式。

3.常用语法及API

3.1 xml声明动画 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>  

其中,标签、标签、标签分别对应着AnimatorSet、ObjectAnimator和ValueAnimator三种类型,这三者直接的关系主要是从属关系和继承关系,AnimatorSet是一个动画集,可以包含若干个动画并操作这些动画的执行顺序等。ObjectAnimator是ValueAnimator的子类,在使用上比ValueAnimator更方便,但同时也会屏蔽到ValueAnimator内部实现和数据演进过程。

3.2 代码声明动画

代码声明动画并设置到View上实现动画效果,以ObjectAnimator为例,最简单也是最典型的代码如下:

ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 500f);  
            animator.setDuration(trasitionDuration);
            animator.start();

ObjectAnimator支持链式变成,所以代码可以直接精简成一行,如下:

ObjectAnimator.ofFloat(view, "translationX", 0f, 500f)  
                    .setDuration(500)
                    .start();

这段代码可以实现将View在水平方向上500毫秒内从x=0的位置移动到x=500的位置的动画。这个效果使用补间动画中的位移动画也可以实现,但是实现方式较ObjectAnimator会略复杂,不管是xml声明的方式还是代码声明的方式。 下面简单介绍下ObjectAnimator、ValueAnimator以及AnimatorSet类中常用的一些API。

ObjectAnimator

(1)public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)
    这个是ObjectAnimator初始化入口之一,还有ofFloat/ofInt/ofArgb/ofMultiInt等入口,且ObjectAnimator不支持new ObjectAnimator来初始化对象,因为构造函数是私有的。
参数说明:
  ①target:该动画作用的对象,类型是Object而不是View,说明ObjectAnimator并不仅仅是为了实现View的动画效果而设计的。
  ②propertyName:属性动画作用的属性名称,一般是对象内提供了Getter和Setter方法的属性,上面代码的"translationX"就是View类中提供了Getter和Setter方法的属性名,类型的属性名还有"translationY"、"translationZ"等。
  ③values:为float类型的不定参,一般是传入两个数值,第一个数值为动画开始时propertyName属性对应的数值,第二个参数为动画结束时propertyName属性对应的数值,也就是动画作用的属性的开始和结束的数值。

(2)public static ObjectAnimator ofInt(Object target, String propertyName, int... values)
  参数说明见(1),不同之处就是需要传入int类型的不定参数。

(3)public ObjectAnimator setDuration(long duration)
   设置动画时长。

(4)public void setEvaluator(TypeEvaluator value)
   设置动画插值器

(5)public void start()
   启动动画

ValueAnimator

(1)public static ValueAnimator ofInt(int... values)
   动画初始化入口函数,入参为动画的开始和结束的数值,类似的入口函数还有ofFloat/ofArgb等等。

(2)public ValueAnimator setDuration(long duration)
   设置动画时长

(3)public void setEvaluator(TypeEvaluator value)
   设置动画插值器

(4)public void addUpdateListener(AnimatorUpdateListener listener)
   设置动画监听回调,可以在回调中监听动画的执行过程,以便在动画执行过程中进行一些除动画以外的操作。

(5)public void setRepeatCount(int value)
   设置动画执行的次数

(6)public void setRepeatMode(@RepeatMode int value)
   设置动画重复的模式,可以设置重新开始动画或者反转动画模式。

(7)public void start()
   启动动画

AnimatorSet

AnimatorSet本质是一个属性动画的容器,它可以设置多个属性动画作为自身动画效果的一部分,并可以设置这些属性动画是同时执行还是顺序执行。  
(1)public AnimatorSet()
   构造函数,用来创建AnimatorSet对象

(2)public Builder play(Animator anim)
   播放一个属性动画

(3)public void playTogether(Animator... items)
   同时播放多个属性动画

(4)public void playSequentially(Animator... items)
   顺序执行多个属性动画

(5)public AnimatorSet setDuration(long duration)
   设置动画时长

(6)public void start()
   开始执行AnimatorSet中的动画
4.属性动画在开发中的使用
4.1 Loading动画效果

上图为点我达骑手曾经使用过的Loading页面动效,这种动效已经超出了常规动画(帧动画/补间动画)能够实现的效果范围,这时候属性动画就要上场表演了!

简单的整理下实现思路,中间那个类似水波纹一样的有渐变颜色的圆圈,其实是需要确定两个因素,一个是圆圈的半径,另一个就是圆圈的颜色的透明度。也就是在使用属性动画的过程中,需要同时监听半径和颜色两个属性的数值,然后实时绘制圆圈并重复执行该动画集合,就可以实现这种特效。主要代码如下:

播放动画

public void playAnimation() {  
        if (animatorSet == null) {
            animatorSet = new AnimatorSet();
            animatorSet.start();
            //圆圈半径变更动画
            ValueAnimator anim1 = ValueAnimator.ofInt(0, backBitmap.getWidth() / 2);
            //圆圈颜色透明度变更动画
            ValueAnimator anim2 = ValueAnimator.ofInt(256, 0);
            anim1.setRepeatMode();
            animatorSet.playTogether(
                    anim1, anim2
            );

            anim1.setRepeatCount(ValueAnimator.INFINITE);
            anim2.setRepeatCount(ValueAnimator.INFINITE);

            anim1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    if (animation != null) {
                        //圆圈半径
                        animCircleRadius = (int) animation.getAnimatedValue();
                    }
                }
            });
            anim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    if (animation != null) {
                        //圆圈透明度
                        currentOpacity = (int) animation.getAnimatedValue();
                    }
                }
            });
        }

        animatorSet.setDuration(2000).start();

        if (hander == null)
            hander = new Handler();

        hander.postDelayed(circleAnimRunnable, 50);
    }
    private Runnable circleAnimRunnable = new Runnable() {
        @Override
        public void run() {
            animCircleColor = Color.argb(currentOpacity, 255, 143, 5);
            gradientPaint.setColor(animCircleColor);

            postInvalidate();
            //50毫秒刷新一次
            hander.postDelayed(this, 50);
        }
    };

播放动画的过程,其实就是监听圆圈半径和颜色透明度渐变的过程,在当前时刻圆圈的半径和颜色透明度就是固定的,然后再使用Canvas实时绘制圆圈就可以了。

4.2 展开-关闭效果

上图是商家发单页面最新的交互效果,点击更换动画的方式展开图标,然后选中图标后以动画的方式回收图标。这种动效很常见,貌似使用普通的位移动画也可以很轻松实现,但使用位移动画实现这种动效并支持图标的点击交互并没有想象中的容易。

上图中,当View从A位置通过位移动画移动到B位置时,在没有交互的情况下是可以满足动画效果的,但是如果View有一些点击动作的交互,这时候使用位移动画就会有一个非常严重的问题存在,就是View从A虽然”移动“到了B的位置,但是View的点击区域还停留在A的位置,这时候你在B的位置点击View是响应不到View的点击事件的。

为了解决位移动画在这种场景下使用时出现的问题,大概的思路有两种,一种就是使用位移动画移动到B位置时,在B的位置克隆一份完整的View放到该位置然后将原来的View设置GONE。另外一种,就是通过位移动画的方式将View从A移动到B时,B位置的点击事件是可以响应的。显而易见,使用属性动画在View有交互的场景下更合适。

4.3 View翻转效果

这种View的翻转效果使用常规动画是无从下手的,但使用属性动画一行代码就可以轻松实现。

ObjectAnimator.ofFloat(imageView, "rotationY", 0f, 360f)  
                .setDuration(1000)
                .start();

View动画 vs 属性动画

1.View动画

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

2.属性动画

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

小结:

属性动画是功能更强大、实现方式更优雅的动画解决方案,在为自定义View设置动效上有着非常强大的表现能力,可以实现View动画实现不了的更加炫酷的动画效果。自定义动画是Android开发者的基本功,配合属性动画在实现一些动效上可以达到事半功倍的效果,也是Android开发者必须掌握的一向基本技能。