Android的动画

376 阅读16分钟

View动画

View动画的四种变换效果对应着Animation的四个子类:TranslateAnimation、ScaleAnimation、RotateAnimation和AlphaAnimation,这四种动画既可以通过XML来定义,也可以通过代码来动态创建,对于View动画来说,建议采用XML来定义动画,这是因为XML格式的动画可读性更好。

要使用View动画,首先要创建动画的XML文件,这个文件的路径为:res/anim/filename.xml。View动画的描述文件是有固定的语法的,如下所示。

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="true">

    <alpha
        android:fromAlpha="float"
        android:toAlpha="float" />
    <scale
        android:fromXScale="float"
        android:fromYScale="float"
        android:pivotX="float"
        android:pivotY="float"
        android:toXScale="float"
        android:toYScale="float" />
    <translate
        android:fromXDelta="float"
        android:fromYDelta="float"
        android:toXDelta="float"
        android:toYDelta="float" />
    <rotate
        android:fromDegrees="float"
        android:pivotX="float"
        android:pivotY="float"
        android:toDegrees="float" />
</set>

从上面的语法可以看出,View动画既可以是单个动画,也可以由一系列动画组成。<set>标签表示动画集合,对应AnimationSet类,它可以包含若干个动画,并且它的内部也是可以嵌套其他动画集合的。

在<scale>标签中提到了轴点的概念,这里举个例子,默认情况下轴点是View的中心点,这个时候在水平方向进行缩放的话会导致View向左右两个方向同时进行缩放,但是如果把轴点设为View的右边界,那么View就只会向左边进行缩放,反之则向右边进行缩放,具体效果读者可以自己测试一下。

在旋转动画中也有轴点的概念,它也会影响到旋转的具体效果。在旋转动画中,轴点扮演着旋转轴的角色,即View是围绕着轴点进行旋转的,默认情况下轴点为View的中心点。考虑一种情况,View围绕着自己的中心点和围绕着自己的左上角旋转90度显然是不同的旋转轨迹,不同轴点对旋转效果的影响读者可以自己测试一下。

除了系统提供的四种View动画外,我们还可以自定义View动画。自定义动画是一件既简单又复杂的事情,说它简单,是因为派生一种新动画只需要继承Animation这个抽象类,然后重写它的initialize和applyTransformation方法,在initialize方法中做一些初始化工作,在applyTransformation中进行相应的矩阵变换即可,很多时候需要采用Camera来简化矩阵变换的过程。说它复杂,是因为自定义View动画的过程主要是矩阵变换的过程,而矩阵变换是数学上的概念,如果对这方面的知识不熟悉的话,就会觉得这个过程比较复杂了。

帧动画是顺序播放一组预先定义好的图片,类似于电影播放。不同于View动画,系统提供了另外一个类AnimationDrawable来使用帧动画。帧动画的使用比较简单,首先需要通过XML来定义一个AnimationDrawable,注意要定义在res/drawable/下面,如下所示。

<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/icon1"
        android:duration="500" />
    <item
        android:drawable="@drawable/icon2"
        android:duration="500" />
    <item
        android:drawable="@drawable/icon3"
        android:duration="500" />
    <item
        android:drawable="@drawable/icon4"
        android:duration="500" />
</animation-list>

然后将上述的Drawable作为View的背景并通过Drawable来播放动画即可:

        Button button = findViewById(R.id.btn1);
        button.setBackgroundResource(R.drawable.anim_list_1);
        AnimationDrawable drawable = (AnimationDrawable) button.getBackground();
        drawable.start();

View动画的特殊使用场景

LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时都会具有这种动画效果。这种效果常常被用在ListView上,我们时常会看到一种特殊的ListView,它的每个item都以一定的动画的形式出现,其实这并非什么高深的技术,它使用的就是LayoutAnimation。LayoutAnimation也是一个View动画,为了给ViewGroup的子元素加上出场效果,遵循如下几个步骤。

<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="0.5"
    android:animationOrder="random"
    android:animation="@anim/anim_item_1">

</layoutAnimation>

它的属性的含义如下所示。

  • android:delay表示子元素开始动画的时间延迟,比如子元素入场动画的时间周期为300ms,那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。总体来说,第一个子元素延迟150ms开始播放入场动画,第2个子元素延迟300ms开始播放入场动画,依次类推。
  • android:animationOrder表示子元素动画的顺序,有三种选项:normal、reverse和random,其中normal表示顺序显示,即排在前面的子元素先开始播放入场动画;reverse表示逆向显示,即排在后面的子元素先开始播放入场动画;random则是随机播放入场动画。

为ViewGroup指定android:layoutAnimation属性:android:layoutAnimation="@anim/anim_layout"。对于ListView来说,这样ListView的item就具有出场动画了,这种方式适用于所有的ViewGroup,如下所示。

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#fff4f7f9"
        android:cacheColorHint="#00000000"
        android:divider="#dddbdb"
        android:dividerHeight="1px"
        android:layoutAnimation="@anim/anim_layout_1"
        android:listSelector="@android:color/transparent"
        app:layout_constraintTop_toBottomOf="@+id/btn1">

    </ListView>

除了在XML中指定LayoutAnimation外,还可以通过LayoutAnimationController来实现,具体代码如下所示。

        ListView listView = findViewById(R.id.list_view);
        Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item_1);
        LayoutAnimationController controller = new LayoutAnimationController(animation);
        controller.setDelay(0.5f);
        controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
        listView.setLayoutAnimation(controller);

Activity有默认的切换效果,但是这个效果我们是可以自定义的,主要用到overridePendingTransition(int enterAnim, int exitAnim)这个方法,这个方法必须在startActivity(Intent)或者finish()之后被调用才能生效

Fragment也可以添加切换动画,由于Fragment是在API 11中新引入的类,因此为了兼容性我们需要使用support-v4这个兼容包,在这种情况下我们可以通过FragmentTransaction中的setCustomAnimations()方法来添加切换动画。这个切换动画需要是View动画,之所以不能采用属性动画是因为属性动画也是API 11新引入的。

属性动画

属性动画是API 11新加入的特性,和View动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至还可以没有对象。除了作用对象进行了扩展以外,属性动画的效果也得到了加强,不再像View动画那样只能支持四种简单的变换。属性动画中有ValueAnimator、ObjectAnimator和AnimatorSet等概念,其中ObjectAnimator继承自ValueAnimator, AnimatorSet是动画集合,可以定义一组动画。 如何使用属性动画呢?下面简单举几个小例子。

  1. 改变一个对象(myObject)的translationX属性,让其沿着X轴向右平移一段距离
ObjectAnimator.ofFloat(myObject, "translationX", 800).start();
  1. 改变一个对象的背景色属性,典型的情形是改变View的背景色,下面的动画可以让背景色在3秒内实现从0xFFFF8080到0xFF8080FF的渐变,动画会无限循环而且会有反转的效果。
        ValueAnimator colorAnimator = ObjectAnimator.ofInt(enter, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
        colorAnimator.setDuration(3000);
        colorAnimator.setEvaluator(new ArgbEvaluator());
        colorAnimator.setRepeatCount(ValueAnimator.INFINITE);
        colorAnimator.setRepeatMode(ValueAnimator.REVERSE);
        colorAnimator.start();
  1. 动画集合,5秒内对View的旋转、平移、缩放和透明度都进行了改变。
        TextView textView = findViewById(R.id.text_anim);
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(
                ObjectAnimator.ofFloat(textView, "rotationX", 0, 360),
                ObjectAnimator.ofFloat(textView, "rotationY", 0, 180),
                ObjectAnimator.ofFloat(textView, "translationX", 0, 90),
                ObjectAnimator.ofFloat(textView, "translationY", 0, 90),
                ObjectAnimator.ofFloat(textView, "scaleX", 1, 1.5f),
                ObjectAnimator.ofFloat(textView, "scaleY", 1, 0.5f),
                ObjectAnimator.ofFloat(textView, "alpha", 1, 0.25f, 1));
        animatorSet.setDuration(5000).start();

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

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="together">

    <objectAnimator
        android:duration="500"
        android:propertyName="translationX"
        android:repeatCount="100"
        android:repeatMode="reverse"
        android:startOffset="100"
        android:valueFrom="0"
        android:valueTo="500"
        android:valueType="floatType" />
    <animator
        android:duration="500"
        android:repeatCount="100"
        android:repeatMode="reverse"
        android:startOffset="100"
        android:valueFrom="0"
        android:valueTo="500"
        android:valueType="floatType" />

</set>

属性动画的各种参数都比较好理解,在XML中可以定义ValueAnimator、Object-Animator以及AnimatorSet,其中<set>标签对应AnimatorSet, <animator>标签对应ValueAnimator,而<objectAnimator>则对应ObjectAnimator。<set>标签的android:ordering属性有两个可选值:“together”和“sequentially”,其中“together”表示动画集合中的子动画同时播放,“sequentially”则表示动画集合中的子动画按照前后顺序依次播放,android:ordering属性的默认值是“together”。android:startOffset——表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正播放此动画;android:valueType——表示android:propertyName所指定的属性的类型,有“intType”和“floatType”两个可选项,分别表示属性的类型为整型和浮点型。另外,如果android:propertyName所指定的属性表示的是颜色,那么不需要指定android:valueType,系统会自动对颜色类型的属性做处理。

对于一个动画来说,有两个属性这里要特殊说明一下,一个是android:repeatCount,它表示动画循环的次数,默认值为0,其中-1表示无限循环;另一个是android:repeatMode,它表示动画循环的模式,有两个选项:“repeat”和“reverse”,分别表示连续重复和逆向重复。

理解插值器和估值器

TimeInterpolator中文翻译为时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和Decelerate-Interpolator(减速插值器:动画越来越慢)等。TypeEvaluator的中文翻译为类型估值算法,也叫估值器,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有IntEvaluator(针对整型属性)、FloatEvaluator(针对浮点型属性)和ArgbEvaluator(针对Color属性)。属性动画中的插值器(Interpolator)和估值器(TypeEvaluator)很重要,它们是实现非匀速动画的重要手段。

属性动画要求对象的该属性有set方法和get方法(可选)。插值器和估值算法除了系统提供的外,我们还可以自定义。实现方式也很简单,因为插值器和估值算法都是一个接口,且内部都只有一个方法,我们只要派生一个类实现接口就可以了,然后就可以做出千奇百怪的动画效果了。具体一点就是:自定义插值器需要实现Interpolator或者TimeInter-polator,自定义估值算法需要实现TypeEvaluator。另外就是如果要对其他类型(非int、float、Color)做动画,那么必须要自定义类型估值算法。

属性动画的监听器

属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListener和AnimatorListener。 AnimatorListener的定义如下:

public static interface AnimatorListener {
        void onAnimationStart(Animator animation);
        void onAnimationEnd(Animator animation);
        void onAnimationCancel(Animator animation);
        void onAnimationRepeat(Animator animation);
    }

从AnimatorListener的定义可以看出,它可以监听动画的开始、结束、取消以及重复播放。同时为了方便开发,系统还提供了AnimatorListenerAdapter这个类,它是Animator-Listener的适配器类,这样我们就可以有选择地实现上面的4个方法了,毕竟不是所有方法都是我们感兴趣的。 下面再看一下AnimatorUpdateListener的定义,如下所示。

   public static interface AnimatorUpdateListener {
        void onAnimationUpdate(ValueAnimator animation);
    }

AnimatorUpdateListener比较特殊,它会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate就会被调用一次,利用这个特性,我们可以做一些特殊的事情。

对任意属性做动画

下面分析属性动画的原理:属性动画要求动画作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对object的属性abc做动画,如果想让动画生效,要同时满足两个条件:

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

那么为什么我们对Button的width属性做动画会没有效果?这是因为Button内部虽然提供了getWidth和setWidth方法,但是这个setWidth方法并不是改变视图的大小,它是TextView新添加的方法,View是没有这个setWidth方法的。从上述源码可以看出,getWidth的确是获取View的宽度的,而setWidth是TextView和其子类的专属方法,它的作用不是设置View的宽度,而是设置TextView的最大宽度和最小宽度的,这个和TextView的宽度不是一个东西。具体来说,TextView的宽度对应XML中的android:layout_width属性,而TextView还有一个属性android:width,这个android:width属性就对应了TextView的setWidth方法。总之,TextView和Button的setWidth、getWidth干的不是同一件事情,通过setWidth无法改变控件的宽度,所以对width做属性动画没有效果。对应于属性动画的两个条件来说,本例中动画不生效的原因是只满足了条件1而未满足条件2。

针对上述问题,官方文档上告诉我们有3种解决方法:

  1. 给你的对象加上get和set方法,如果你有权限的话;

    如果你有权限的话,加上get和set就搞定了。但是很多时候我们没权限去这么做。

  2. 用一个类来包装原始对象,间接为其提供get和set方法; 这是一个很有用的解决方法,是笔者最喜欢用的,因为用起来很方便,也很好理解,下面将通过一个具体的例子来介绍它。

ViewWrapper wrapper = new ViewWrapper(button);
ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start();
    
public class ViewWrapper {
    private View target;

    public ViewWrapper(View target) {
        this.target = target;
    }

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

    public void setWidth(int width) {
        target.getLayoutParams().width = width;
        target.requestLayout();
    }
}

上述代码在5s内让Button的宽度增加到了500px,为了达到这个效果,我们提供了ViewWrapper类专门用于包装View,具体到本例是包装Button。然后我们对ViewWrapper的width属性做动画,并且在setWidth方法中修改其内部的target的宽度,而target实际上就是我们包装的Button。这样一个间接属性动画就搞定了,上述代码同样适用于一个对象的其他属性。

  1. 采用ValueAnimator,监听动画过程,自己实现属性的改变。

    首先说说什么是ValueAnimator, ValueAnimator本身不作用于任何对象,也就是说直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们的对象的属性值,这样也就相当于我们的对象做了动画。下面用例子来说明:


    /**
     * 动画的方式增加View的宽度
     */
    private void performAnimate(final View target, final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            //整型估值器
            private IntEvaluator intEvaluator = new IntEvaluator();

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获得当前动画的进度值,整型,1-100之间
                int value = (int) animation.getAnimatedValue();

                //获得当前进度占整个动画过程的比例,浮点型,0-1之间
                float fraction = animation.getAnimatedFraction();
                //直接调用整型估值器,通过比例计算出宽度,然后再设置给button
                target.getLayoutParams().width = intEvaluator.evaluate(fraction, start, end);
                target.requestLayout();

            }
        });
        valueAnimator.setDuration(5000).start();
    }
    
    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.enter) {
            performAnimate(enter, enter.getWidth(), 500);
    

。关于这个ValueAnimator要再说一下,拿上面的例子来说,它会在5000ms内将一个数从1变到100,然后动画的每一帧会回调onAnimationUpdate方法。在这个方法里,我们可以获取当前的值(1~100)和当前值所占的比例,我们可以计算出Button现在的宽度应该是多少。比如时间过了一半,当前值是50,比例为0.5,假设Button的起始宽度是100px,最终宽度是500px,那么Button增加的宽度也应该占总增加宽度的一半,总增加宽度是500-100=400,所以这个时候Button应该增加的宽度是400×0.5=200,那么当前Button的宽度应该为初始宽度 + 增加宽度(100+200=300)。上述计算过程很简单,其实它就是整型估值器IntEvaluator的内部实现,所以我们不用自己写了,直接用吧。

使用动画的注意事项

通过动画可以实现一些比较绚丽的效果,但是在使用过程中,也需要注意一些事情,主要分为下面几类。

  1. OOM问题这个问题主要出现在帧动画中,当图片数量较多且图片较大时就极易出现OOM,这个在实际的开发中要尤其注意,尽量避免使用帧动画。
  2. 内存泄露在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄露,通过验证后发现View动画并不存在此问题。
  3. 兼容性问题动画在3.0以下的系统上有兼容性问题,在某些特殊场景可能无法正常工作,因此要做好适配工作。
  4. View动画的问题View动画是对View的影像做动画,并不是真正地改变View的状态,因此有时候会出现动画完成后View无法隐藏的现象,即setVisibility(View.GONE)失效了,这个时候只要调用view.clearAnimation()清除View动画即可解决此问题。
  5. 不要使用px在进行动画的过程中,要尽量使用dp,使用px会导致在不同的设备上有不同的效果。
  6. 动画元素的交互将view移动(平移)后,在Android 3.0以前的系统上,不管是View动画还是属性动画,新位置均无法触发单击事件,同时,老位置仍然可以触发单击事件。尽管View已经在视觉上不存在了,将View移回原位置以后,原位置的单击事件继续生效。从3.0开始,属性动画的单击事件触发位置为移动后的位置,但是View动画仍然在原位置。
  7. 硬件加速使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性。