Android 控件——动画的深入分析

本文链接:https://rainmonth.github.io/posts/A180510.html

摘要

本文简单介绍了Android中的三种动画:View动画、帧动画和属性动画,对各种动画做了详细的说明,同时针对动画使用过程中可能出现的问题提出了相应的解决方案。

View动画

View动画的种类

主要有四个子类:TranslationAnimation、ScaleAnimation、RotateAnination和AlphaAnimation。四种动画既可以通过XML来定义,也可以通过代码来动态创建,对于View动画建议采用XML来定义,因为Xml格式的可读性更好。

名称 对应xml标签 对应的类 使用的效果
平移动画 <translate> TranslateAnimation 移动View
缩放动画 <scale> ScaleAnimation 放大或缩小View
旋转动画 <rotate> RotateAnimation 旋转View
透明度动画 <alpha> AlphaAnimation 改变View的透明度

View动画的使用

通过xml文件定义动画

在res目录下新建子目录anim,在里面新建我们的动画定义xml文件。xml文件具体语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fillAfter="true"
android:fillBefore="true"
android:repeatMode="reverse"
android:shareInterpolator="true"
android:startOffset="11">

<translate
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="0" />

<scale
android:duration="1000"
android:fromXScale="0"
android:fromYScale="0"
android:interpolator="@android:animator/fade_in"
android:pivotX="0"
android:pivotY="0"
android:toXScale="0"
android:toYScale="0" />

<rotate
android:fromDegrees="0"
android:pivotX="0"
android:pivotY="0"
android:toDegrees="0" />

<alpha
android:fromAlpha="0"
android:toAlpha="0" />

<set>
<alpha
android:fromAlpha="0"
android:toAlpha="0" />
</set>
</set>

上面只是给出编写的示例,请结合具体情况调整。这里对各种动画的属性含义不做一一解释,只对以下几个进行说明:

  • android:duration,动画持续时间;
  • android:fillBefore,动画结束时,停留在最初位置;
  • android:fillAfter,动画结束后,停留在最后位置;
  • android:shareInterpolator,各个子动画是否共享插值器;
  • android:interpolator,动画插值器;

假如用xml定义了一个动画,该xml文件为demo.xml,采用下面方法在代码中使用:

1
2
3
View view = findViewById(R.id.view_id);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.demo);
view.startAnimation(animation);

通过代码来使用动画

1
2
3
4
5
6
View view = findViewById(R.id.view_id);
// 创建一个动画
AlphaAnimation alphaAnim = new AlphaAnimation(0,1);
alphaAnim.setDuration(300);
// 使用动画
view.start(alphaAnim);

自定义View动画

View动画是可以自定义的,只要继承动画抽象类Animation,重写initialize方法和applyTransformation方法,其中重写initialize方法来做一些初始化方面的工作,而重写applyTransformation则是来应用矩阵变换(动画的核心实现其实就是应用矩阵变化)。

帧动画

帧动画(Frame Animation)就是有序的播放一组预先定义好的图片。系统提供来AnimationDrawable来供我们使用帧动画。具体使用步骤:

  • 定义好对应的xml文件,格式如下(在drawable目录下新建frame_anim.xml):

    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http:/schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/pic1" android:duration="200" />
    <item android:drawable="@drawable/pic3" android:duration="200" />
    <item android:drawable="@drawable/pic3" android:duration="200" />
    </animation-list>
  • 将上面定义好的drawable最为View的背景并通过AnimationDrawable来播放

    1
    2
    3
    4
    View view = findViewById(R.id.view_id);
    view.setBackgroundResource(R.drawable.frame_anim);
    AnimationDrawable drawable = (AnimationDrawable) view.getBackground();
    drawable.start();

    用于是播放动画是会加载一系列的图片,如果图片过大,内存消耗会很严重,容易导致OOM。

View动画的特殊使用场景

ViewGroup的LayoutAnimation

当你看到那些ListView绚丽的加载效果时,是不是迫切的想知道它是怎么实现的,其实很简单,它就是利用LayoutAnimation动画来实现的。那么LayoutAnimation动画具体怎么使用呢?其实主要分一下几部:

Step1:定义LayoutAnimation

在res的anim下新建list_layout_animation.xml文件,具体内容如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/abc_slide_in_bottom"
android:animationOrder="normal"
android:delay="0.5">

</layoutAnimation>

其中android:animation指定子元素的具体动画效果android:animationOrder指定动画的执行顺序,android:delay指定延时时间。

Step2:定义具体的动画

其实就是定义你要看到的具体的动画效果,也可以采用系统已经定义好的,上面采用的就是系统的。

Step3:应用LayoutAnimation

利用ViewGroup的android:layoutAnimation属性来应用动画,so easy!

当然也可以采用代码来实现了,具体代码如下:

1
2
3
4
5
6
7
// 得到动画
Animation itemAnim = AnimationUtils.loadAnimation(this, R.anim.abc_slide_in_bottom);
// 用itemAnim构造LayoutAnimationController
LayoutAnimationController controller = new LayoutAnimationController(itemAnim);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
lvDemo.setLayoutAnimation(controller);

Activity的入(出)场动画

在Activity中,调用startActivity和finish方法后,调用overridePendingTransition(int enterAnim, int exitAnim)这个方法就可以设置Activity的入(出)场动画了。

  • enterAnim,进入对应的动画
  • exitAnim,离开时对应的动画

Fragment也可以应用动画,我们可以使用support-V4的Fragment(兼容性考虑),利用FragmentTransaction的setCustomAnimations()方法来实现。

属性动画

使用属性动画

主要就是使用ValueAnimatorObjectAnimatorValueAnimator的子类)、AnimatorSet这三个类来完成动画

ObjectAnimator

不能被继承,可以针对任何对象应用动画(前提是这个对象拥有要产生动画的属性),如:

1
2
3
4
5
6
7
8
Button btn1 = (Button) findViewById(R.id.btn1);
View changeBgView = findViewById(R.id.change_bg_view);

// btn1向下移动100
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(btn1, "translationY", 200);
objectAnimator.setRepeatCount(1);
objectAnimator.setRepeatMode(ObjectAnimator.REVERSE);
objectAnimator.start();

ValueAnimator

使用也非常简单,如下:

1
2
3
4
5
6
7
8
// 改变对象背景颜色
ValueAnimator colorAnim = ObjectAnimator.ofInt(changeBgView, "backgroundColor",
0xFFFF8080, 0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();

上面代码将改变View的背景,并循环往复。

AnimatorSet

动画的集合,就是可以定义一系列的动画,然后按一定的方式播放(可以序列播放,也可以一起播放),使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 动画集合
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(changeBgView, "rotationX", 0, 360),
ObjectAnimator.ofFloat(changeBgView, "rotationY", 0, 180),
ObjectAnimator.ofFloat(changeBgView, "rotation", 0, -90),
ObjectAnimator.ofFloat(changeBgView, "translationX", 0, 90),
ObjectAnimator.ofFloat(changeBgView, "translationY", 0, 90),
ObjectAnimator.ofFloat(changeBgView, "scaleX", 1, 1.5f),
ObjectAnimator.ofFloat(changeBgView, "scaleY", 1, 0.5f),
ObjectAnimator.ofFloat(changeBgView, "alpha", 1, 0.25f, 1)
);
set.setDuration(5000).start();

同样,属性动画也可以通过xml文件来定义,不过要注意的是,这次的目录是res/animator,具体见下面的property_anim.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<set android:ordering="sequentially">

</set>
<animator
android:duration="5000"
android:interpolator="@integer/abc_config_activityDefaultDur"
android:repeatCount="2"
android:repeatMode="reverse"
android:startOffset="@integer/abc_config_activityDefaultDur"
android:valueFrom="2"
android:valueTo="4"
android:valueType="intType">

</animator>
<objectAnimator
android:duration="5000"
android:interpolator="@integer/abc_config_activityDefaultDur"
android:propertyName="translationX"
android:repeatCount="1"
android:repeatMode="reverse"
android:startOffset="10"
android:valueFrom="float|integer|color"
android:valueTo="float|integer|color">

</objectAnimator>
</set>

上面属性可能不全,具体可以自己写写看。

定义好后,使用也极其简单:

1
2
3
4
5
6
7
// 通过xml来使用得到属性动画
@SuppressLint("ResourceType")
AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.anim.property_anim);
// 设置要运行动画的目标
animatorSet.setTarget(changeBgView);
// 开启动画
set.start();

建议使用代码的方式来实现,而不要采用xml文件,因为xml文件有诸多局限(很多时候xml文件中不能方便的获取设备信息)。

属性动画的插值器和估值器

插值器

TimeInterpolator,所有的插值器都实现了该接口,其作用是根据时间流逝的百分比计算出当前属性改变的百分比。系统预置的插值器有:

  • LinearInterpolator,线性插值器,匀速动画;
  • AccelerateInterpolator,加速插值器,动画越来越快;
  • DecelerateInterpolator,减速插值器,动画越来越慢;
  • AccelerateDecelerateInterpolator,先加速后减速,动画开始和结尾慢,中间速度快;
  • BounceInterpolator,弹性插值器,动画结束时有弹簧效果;
  • AnticipateInterpolator,动画开始的时候向后,然后在向前;(类似橡皮筋往后拉然后放手的开始部分状态)
  • OvershootInterpolator,动画最后继续向前,然后在向后,最后回复到原位置;(类似橡皮筋往前拉然后放手的开始部分状态)
  • AnticipateInterpolator,开始时先向后,然后向前;最后时,先想后,然后向前,最后到最终位置。((类似橡皮筋往后拉然后放手的整个状态)
  • CycleInterpolator,循环插值器,循环播放动画若干次;

估值器

TypeEvaluator,所有的估值器都实现了该接口,其作用是根据当前属性改变的百分比来计算改变后的属性值。系统预制的估值器有:

  • IntEvaluator,针对int属性值的估值器;
  • FloatEvaluator,针对float属性值的估值器;
  • ArgbEvaluator,针对color属性值的估值器;

注意动画的默认刷新频率为10ms/帧

属性动画监听器

主要监听器有AnimatorListenerAnimatorPauseListenerAnimatorUpdateListener,定义分别如下:

1
2
3
4
5
6
public static interface AnimatorListener {
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}
1
2
3
4
public static interface AnimatorPauseListener {
void onAnimationPause(Animator animation);
void onAnimationResume(Animator animation);
}

这两个是Animator的静态内部类。

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

这个是ValueAnimator的静态内部类。

对任意属性做动画

前面说了,属性动画可以对任意对象的属性做动画(采用ObjectAnimator),但其实还是有一定的限制的,限制如下(这里假定要对A对象的abc属性做动画):

  1. A对象必须提供abc属性的setAbc方法,如果abc属性没有初始值,还要提供abc属性的getAbc方法;
  2. A对象提供的setAbc方法必须是能真正改变abc属性的,不能挂羊头卖狗肉,写的是这只abc的属性,但实际上对abc属性没有任何更改;

以上两个条件全部满足才可以对A对象的abc属性应用属性动画。如果1不满足,程序会直接crash;如果2不满足,动画就没有效果,但不会crash。

针对上面的1,提供setAbc方法很容易理解,因为动画的过程要不断改变abc属性的值;提供getAbc方法是因为我们在开始动画的时候可能要获取abc属性的默认值

比如你直接针对Button的width做属性动画就没有效果,因为它的setWidth方法并没有真真的改变width。

当出现条件不满足的时候,我们还是可以想些办法的,具体可以从以下几个方面入手:

  1. 给对象加上真正意义的get和set方法;

  2. 包装一下对象,在包装的那个类中间接的为对象提供get和set方法;

  3. 更直接的方法,就是自己利用ValueAnimator的AnimatorUpdateListener来直接改变属性,比如我要对Button的width做动画:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Button btn = (Button) findViewById(R.id.btn1);
    btn.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(Vier v) {
    performAnimate(btn, mButton.getWidth(), 500)
    }
    });

    private void performAnimate(View target, final int start, final int end) {
    ValueAnimator valueAnimator = ValueAnimator.ofInt(1,100);
    valueAnimator.addUpdateListener(new AnimatorUpdateListener(){
    private IntEvaluator mEvaluator = new IntEvaluator();

    @Override
    public void onAnimationUpdate(ValueAnimator animator) {
    int currentValue = (Integer) animator.getAnimatedValue();
    // 获取当前进度占整个动画过程的比例
    float fraction = animator.getAnimatedFraction();

    target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
    target.requestLayout();
    }
    });
    valueAnimator.setDuration(5000).start();
    }

使用动画注意事项

动画虽然很方便,能实现各种各样的效果,但我们在使用的时候仍要注意处理它们可能引发的问题。

  • OOM,主要由于使用帧动画导致,因为帧动画是将一系列的图片有序播放,容易OOM;
  • 内存泄漏,属性动画中的无线循环动画如果不在Activity退出或销毁时取消,容易引起内存泄漏;
  • 兼容性,属性动画是Android 3.0之后才引入的,使用动画要注意兼容性处理。
  • 交互问题,View动画平移后点击事件的处理;
  • 单位使用问题,建议使用设备相关单位dp而不是px;
  • 硬件加速,为保证动画流畅性,建议开启硬件加速。