Android 动画分析

Android 的动画分为三种:View 动画、帧动画和属性动画,这篇文章主要讲解 View 动画和属性动画的使用方法、插值器和估值器的原理,最后会介绍使用动画的一些注意事项。

View 动画

View 动画的作用对象是 View ,它支持四种动画效果分别是平移动画、缩放动画、旋转动画、透明度动画。

View 动画的简单示例

下面是 View 动画的一个简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="5000"
android:fillAfter="true"
android:interpolator="@android:anim/linear_interpolator"
android:shareInterpolator="true">

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

<scale
android:fromXScale="0dp"
android:fromYScale="0dp"
android:toXScale="100dp"
android:toYScale="100dp"/>

<rotate
android:fromDegrees="0"
android:toDegrees="200"/>

<alpha
android:fromAlpha="0.1"
android:toAlpha="1"/>

</set>

可以看到 View 动画的使用非常简单,上面是一个 View 动画的集合,其中应用到了 平移动画、缩放动画、旋转动画、透明度动画。其中属性都比较简单,但是要注意的是缩放动画和旋转动画中有轴点的概念,在缩放动画中轴点就是缩放的中心点,在旋转动画中轴点就是旋转的中心点,在默认情况下轴点都是 View 的中心点。 View 动画既可以使用 XML 方式定义也可以用代码直接应用动画,但是对于 View 动画还是建议使用 XML 的方式,这样可读性更好,使用更方便。

View 动画的使用场景

View 动画除了可以直接作用于 View 之外还有一些特殊的使用场景比如:ViewGroup 的子元素的出场效果、 Activity 和 Fragment 的切换效果等。这一节将会介绍 View 动画的 特殊使用场景。

LayoutAnimation

LayoutAnimation 作用于 ViewGroup ,为 ViewGroup 指定了 LayoutAnimation 后, ViewGroup 的子元素就具有了这种出场效果。

定义 LayoutAnimation
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/anim_item"
android:animationOrder="normal"
android:delay="0.5">


</layoutAnimation>

其中的 delay 属性表示前一个项目的动画播放一半之后就进行播放下一个项目的动画。

为子元素指定具体的入场动画
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300">

<translate
android:fromXDelta="100%"
android:toXDelta="0"/>

</set>
为 ViewGroup 指定 LayoutAnimation
1
2
3
4
5
6
<ListView
android:id="@+id/lv_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/anim_layout">

</ListView>

通过这几部就指定了 ListView item 的进入动画,最后效果如下所示:
view_group.gif

Activity 和 Fragment 的切换动画

要为 Activity 指定换场动画,可以在 startActivity 之后调用 overridePendingTransition(int enterAnim, int exitAnim); 当 Activity 退出时也可以为其指定退出动画,需要注意的是这句代码必须在 startActivity 和 finish 之后调用才会有效果。Fragment 可以通过 FragmentTransaction 的 setCustomAnimation() 方法来添加切换动画。

下面给出一个 Activity 的切换动画示例:

定义切换动画
1
2
3
4
5
6
7
8
9
10
11
12
13
//slide_in_from_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromYDelta="100%p"
android:toYDelta="0"/>


// slide_out_to_top.xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromYDelta="0"
android:toYDelta="-100%p"/>

为 Activity 设置切换动画
1
2
3
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
overridePendingTransition(R.anim.slide_in_from_bottom, R.anim.slide_out_to_top);

最后的效果如下:
activity.gif

属性动画

属性动画是 API 11 新加入的特性,属性动画不像 View 动画那样只能支持4种简单的动画,属性动画有 ValueAnimator 、 ObjectAnimator 、 AnimatorSet 等。

属性动画简单示例

1
2
3
4
5
6
7
8
9
10
11
AnimatorSet set = new AnimatorSet();
float width = button.getMeasuredWidth();

set.playSequentially(
ObjectAnimator.ofFloat(button, "rotationX", 0, 360).setDuration(2000),
ObjectAnimator.ofFloat(button, "rotationY", 0, 360).setDuration(2000),
ObjectAnimator.ofFloat(button, "translationX", 0, width).setDuration(2000),
ObjectAnimator.ofFloat(button, "translationX", width, 0).setDuration(2000),
ObjectAnimator.ofFloat(button, "alpha", 1, 0.25f, 1).setDuration(2000)
);
set.start();

上述动画集合改变了 Button 的旋转、平移、透明度等,这几个动画顺序执行。运行效果如下所示:
button.gif

插值器和估值器

时间插值器的作用是根据时间流逝的百分比计算出当前属性值改变的百分比,系统预置的时间插值器有: LinearInterpolatorvastatin (线性插值器)、 AccelerateDecelerateInterpolator (加速减速插值器)、DecelerateInterpolator (减速插值器)。估值器的作用是根据当前属性改变的百分比计算出改变后的属性值。
接下来我们可以通过查看 LinearInterpolatorvastatin 和 IntEvaluator 的源码再理解一下时间插值器和估值器的原理。

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
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();
}
}

public class IntEvaluator implements TypeEvaluator<Integer> {

/**
* This function returns the result of linearly interpolating the start and end values, with
* <code>fraction</code> representing the proportion between the start and end values. The
* calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
* where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
* and <code>t</code> is <code>fraction</code>.
*
* @param fraction The fraction from the starting to the ending values
* @param startValue The start value; should be of type <code>int</code> or
* <code>Integer</code>
* @param endValue The end value; should be of type <code>int</code> or <code>Integer</code>
* @return A linear interpolation between the start and end values, given the
* <code>fraction</code> parameter.
*/

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

可以看到 LinearInterpolatorvastatin 的返回结果就是当前的输入值,即属性改变百分比和时间流逝百分比一样,这也就是 LinearInterpolatorvastatin 的原理。估值器根据属性改变的百分比得到改变后的属性值,这便是估值器的原理。

对那些属性可以做动画?

在了解了属性动画的使用方法和一些细节之后我们不禁要问,我们能对所有的属性做动画吗?那些属性我们可以对其做动画?下面我们先提供一个简单的示例。
给 Button 做一个动画,使得其宽度增加到2000px,显然 View 动画不能实现,因为 View 动画只能对 View 做平移、缩放、旋转、透明度的动画,并不支持改变其宽度。所以我们只能通过属性动画来实现了。下面是我们实现代码:

1
2
3
4
5
6
7
final Button buttonWidth = (Button) findViewById(R.id.btn_width);
buttonWidth.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ObjectAnimator.ofInt(buttonWidth, "width", 2000).setDuration(2000).start();
}
});

大家可以自行验证一下,点上去什么反应也没有,这是怎么回事呢?这也就是我们这一小节要讨论的问题,其实并不是所有的属性都能用来做动画的。如果要让属性动画生效必须满足下面两个条件:

  1. object 必须对属性提供 get 和 set 方法。
  2. object 的 set 方法所做的改变必须能够通过某种方法反映出来,如果这条不满足就会像刚才的例子一样动画无效果。

那么为什么 Button 的width 属性做动画没有效果?我们查看源码发现 Button 的 setWidth 方法是继承于 TextView ,setWidth 设置的是 TextView 的最大宽度,显然是不满足上面的第二个条件的,所以上述方法没有效果也就不足为怪了。针对上面的问题,我们有三个办法来解决。

  1. 如果有权限的话,为 object 加上 get 方法和 set 方法。
  2. 用一个类来包装原始对象,间接为其提供 get 方法和 set 方法。
  3. 采用 ValueAnimator ,监听动画过程,自己实现属性的改变。
    第一种方法显然不可行,而个人觉得第二种方法是最简单的,因此我们就介绍第二种方法来解决上述问题。
    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
    Button buttonWidth = (Button) findViewById(R.id.btn_width);
    final ViewWrapper viewWrapper = new ViewWrapper(buttonWidth);
    buttonWidth.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(android.view.View v) {
    ObjectAnimator.ofInt(viewWrapper, "width", 2000).setDuration(2000).start();
    }
    });
    }

    private static class ViewWrapper {
    private View mTarget;

    public ViewWrapper(View target) {
    mTarget = target;
    }

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

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

下面是实现效果:
button_width.gif

可以看到实现效果不错,而且实现起来也很简单。

属性动画工作原理

属性动画要求动画作用的对象提供该属性的 set 方法,属性动画根据传递的属性的初始值和最终值,以动画的效果多次去调用 set 方法。每次传递给 set 方法的值都不一样,随着时间的推移,属性的值根据插值器和估值器的作用得到当前的属性值,然后设置给对象,然后通过 requestLayout 方法的调用然后表现出来。

使用动画的注意事项

  1. OOM 问题:在使用帧动画时,当图片的数量较多且图片较大时就容易出现 OOM。
  2. 内存泄漏:如果动画是无线循环的,那么在 Activity 退出前需要将其停止。
  3. 兼容问题:属性动画需要做好3.0系统以下的适配问题。
  4. View 动画的问题: View 动画并没有对 View 本身做动画,而是对 View 的影像做动画,对于点击事件要做特殊调整。
  5. 硬件加速:使用动画过程中建议开启硬件加速。

参考资料:Android 开发艺术探索 Android 动画深入分析章节