«

Honeycomb 中引入的新 Animation —— Property Animation

时间:2024-3-2 19:56     作者:韩俊     分类: Android


Honeycomb 中引入的新 Animation

译自:Animation in Honeycomb


这篇博客的作者是 Chet Haase,它是一位专注于图像和动画领域的 Android 工程师。他比较偶然的在他自己的 CodeDependent 博客上(graphics-geek.blogspot.com)发表了与这个课题有关的视频和文章。 —— Tim Bray

在 Honeycomb 中引入的新特性中,其中一个就是新的动画系统,相关的 API 放在一个新的包下(android.animation),这个新的动画系统使得让对象(object)和属性(property)做动画比之前更加容易。

“但是,等等!”你脱口而出,差点一口咖啡喷到键盘上,“Android 里面不是已经有一个动画系统了吗?”

Honeycomb 之前的动画

确实,Android 已经拥有做动画的能力:在 android.view.animation 这个包中有一些类和大量的方法可供使用。例如,你可以移动、放缩、旋转和淡化(fade)View,并且你可以使用 AnimationSet 对象来把多个动画组合在一起使用。你可以在一个 LayoutAnimationController 中指定动画,使得当 View 容器(container)布局完成它自己和所有的子 View 的时候自动执行这些动画。并且你可以使用众多 Interpolator 实现中的一个(像 AccelerateInterpolator 和 Bounce)来使动画更加的自然和酷炫。

但是还有很多重要的功能是以前的动画系统所没有的。

简单来说,你可以对 View 做动画…仅此而已。在大部分场景下,这已经足够了。毕竟 Android 中的 GUI 对象是 View。所以如果你想要移动一个 Button、TextView 或者 LinearLayout 或者任何其他 GUI 对象,那么之前的动画系统对你来说够用了。但是如果你在自己的 View 里面有一些自定义的绘制,你要怎么对它们做动画呢?比如一个 Drawable 的位置或者是背景颜色的透明度。你只能自己去处理,因为之前的动画系统只会操作 View 对象。

而且之前的动画系统所涉及的范围是有限的(have a limited scope):你可以移动、旋转、放缩和淡化一个 View…仅此而已。那怎么样才能对一个 View 的背景颜色做动画呢?你又得靠自己,因为之前的动画系统只拥有一些硬编码的(hardcoded)功能,除此之外它们不能做任何事情。

最后,之前的动画只是改变目标 View 对象的视觉显示效果…但是实际上它们没有改变对象本身。你可能因此而遇到问题。假设你想要把一个 Button 从屏幕的以便移动到另一边。你可以使用 TranslateAnimation 来做这件事,然后这个 Button 就高高兴兴地从屏幕的一边滑动到另一边。然后当动画界结束之后,它会高高兴兴地一下子回到它原来的位置。然后你找到了一个叫做 setFillAfter 的方法,你决定再试一试。这一次 Button 留在了动画结束的位置。然后你可以通过点击来确认一下 —— 但是你发现 Button 点不到!问题在于, Animation 只是改变 Button 画在哪里,并不改变 Button 在它的父容器中的真实位置。如果你想点击到 Button,你不得不去点击它原来的位置。或者通过一个稍微好一点的方案(只是稍微好一点),你可以通过代码来使这个 Button 动画结束后移动位置到动画结束的位置。

因为以上所有原因(当然还有更多),我们决定在 Honeycomb 中提供一个新的动画系统,这个动画系统的构建想法来源于“属性动画(property animation)”。

Honeycomb 中的属性动画(property animation)

Honeycomb 中的新动画系统不是专门给 View 用的,不局限于对象中的特定属性,而且并不仅仅是视觉上的动画系统。它是一个基于时间来改变值(animate values over time),并将那些值赋给目标对象和属性 —— 任何对象和属性。所以你可以移动一个 View 或者使它渐隐(fade it in)。并且你可以移动 View 中的一个 Drawable。并且你可以使一个 Drawable 的背景颜色做动画。实际上,你可以使任何数据结构的值做动画;你只需要告诉动画系统动画持续多久、怎么样计算自定义类型的值以及在哪两个值之间做动画,然后动画系统就会搞定计算动画过程的值并把它赋值给目标对象的部分。

因为这个动画系统是真实地改变目标对象的属性,对象本身是在改变的,而不仅仅是展示出来的样子在改变。所以当你移动一个 Button 的时候它是真的在移动,而不仅仅是画在另一个位置。你甚至可以在它动画的过程中点击它。去试试点击它,我打赌你可以。

我会简单介绍一下这个新系统中的几个主要的类,并在需要的时候展示一些实例代码。但是如果你想了解系统工作原理的细节,下载 SDK 中关于新动画的 API Demos。那里面有很多为了展示新的动画系统而写的小 app(在 demo 列表的最顶上,在单词 App 之前。我喜欢研究动画就是因为它总是在字母表的最前面)。

实际上,这里有一个短视频展示了一些新的动画系统的实例。这个视频从设备的桌面开始,在桌面上你可以看到新的动画系统被用在两个屏之间做平移切换动画上。然后视频播放了一些 API Demo 应用来展示新的动画系统都能做些什么。这个视频是直接从一个运行 Honeycomb 的设备上录制的,所以如果你的设备安装了 API Demos 的话你的设备中可以看到的样子会和里面一样。

视频请在原文中查看

Animator

Animator 是所有新的动画类的超类(superclass),它拥有子类的一些共有属性和方法。它的子类包括 ValueAnimator,它是整个系统的核心计时引擎(core timing engine),我们会在下一节中介绍它;AnimatorSet,他被用来将许多 Animator 整合到一个单独的动画中。你并不会直接用到 Animator,但是子类的一些方法和属性都是在这个超类中被定义的,像是 duration、startDelay 以及监听器(listener)功能。

监听器是非常重要的,因为有时你会希望在动画结束的时候做一些事情,像是当淡出动画结束的时候移除 View。监听一个动画的生命周期事件(lifecycle events),你可以通过实现 AnimatorListener 接口并将你的监听器添加到 Animator 对象。例如,为了在动画结束的时候做一些操作,你可以这样做:

anim.addListener(new Animator.AnimatorListener() {
    public void onAnimationStart(Animator animation) {}
    public void onAnimationEnd(Animator animation) {
        // do something when the animation is done
    }
    public void onAnimationCancel(Animator animation) {}
    public void onAnimationRepeat(Animator animation) {}
});

为了方便,这里有一个适配器类(adapter class),叫做 AnimatorListenerAdapter,这个类为所有这些方法做了空的实现,因此你可以只是重写那些你关心的方法。

anim.addListener(new AnimatorListenerAdapter() {
    public void onAnimationEnd(Animator animation) {
    // do something when the animation is done
    }
});

ValueAnimator

ValueAnimator 是这个系统中的主要劳动力(main workhorse)。它执行内部的计时循环(internal timing loop)。这个计时循环会驱使动画去计算并设置中间值(causes all of a process’s animations to calculate and set values),并且拥有所有使它可以实现这项能力的核心功能(all of the core functionality that allows it to do this),包括每个动画的计时细节(timing details)、关于动画重复(repeats)的信息、接收更新事件的监听器、以及计算不同类型的值的能力(详见 TypeEvaluator)。新的动画系统包含两个部分:计算当前的值(animated values)和将那些值设置给目标对象和属性。ValueAnimator 关注第一个部分:计算当前值。而我们将在后面介绍的 ObjectAnimator 类是负责将值赋给目标对象的。

在大部分时间里,你会想去使用 ObjectAnimator,因为它使整个 计算值并把值赋给目标对象的 过程变得更简单。但是有时候你会希望直接使用 ValueAnimator。例如,你想要做动画的对象可能并没有暴露出 setter 方法,而这个方法是对于属性动画系统是必要的。或者可能你希望只做一个动画并将同一个计算值(animated value)同时赋给几个属性。或者可能你只是想要一个简单的计时机制。不论是哪一种情况,使用 ValueAnimator 都是非常容易的;你只需要将要做动画的属性和初始值结束值赋给它然后启动它(start it)。例如,想要在半秒钟内做0到1的数值动画,你可以这样做:

ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(500);
anim.start();

但是动画有点像名字叫做“树林里的树”的哲学问题(“如果树林里的一棵树倒下了,但是没有人听到它倒下,那么它是否发出了声音呢?”)。如果你在动画过程中不使用中间值做任何事情,那么这个动画真的在运行吗?与树的问题不同的是,这个是明确答案的:当然它在运行。但是如果你不使用中间值做任何事情,它也可能没有运行(it might as well not be running)。如果你启动了它,机会(chances)是你希望使用动画过程中计算的值来做一些事情。所以你添加了一个监听器来监听每一帧的更新。然后当你接受到回调的时候,你调用 getAnimatedValue(),这个方法会返回一个 Object 来指定当前的值。

anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    public void onAnimationUpdate(ValueAnimator animation) {
        Float value = (Float) animation.getAnimatedValue();
        // do something with value...
    }
});

当然,你并不会每次都是计算 float 值。可能你需要计算 integer 值:

ValueAnimator anim = ValueAnimator.ofInt(0, 100);

或者写在 XML 中:

<Animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:valueFrom="0"
    android:valueTo="100"
    android:valueType="intType"/>

实际上,可能你需要对一些完全不同的东西做动画,像是 Point、Rect或者一些自定义的数据结构。动画系统能够理解的类型只有 float 和 int,但是这并不意味着你只能使用这两种类型。你可以使用工厂方法中的 Object 版本,它带有一个 TypeEvaluator 参数来告诉系统如何为未知的类型计算中间值:

Point p0 = new Point(0, 0);
Point p1 = new Point(100, 200);
ValueAnimator anim = ValueAnimator.ofObject(pointEvaluator, p0, p1);

除了时长(duration)之外,你还可以给 ValueAnimator 设置其他的动画参数,包括:

setStartDelay(long):这个属性控制着动画在调用 start() 之后多久才会开始播放。
setRepeatCount(int) 和 setRepeatMode(int):这些方法控制着动画会重复几次以及动画重复的时候是循环(loop) 播放还是每次都反向(reverse direction each time)。
setInterpolator(TimeInterpolator):这个对象控制着动画的计时行为。在默认情况下,动画会 加速进入减速退出,但是你可以通过设置不同的插值器来进行改变。这个方法的作用和之前的 Animation 类中的同名方法作用一致;只是它的参数类型是 TimeInterpolator 而 Animation 中同名方法的参数是 Interpolator。但是 TimeInterpolator 接口是 android.view.animation 包中的 Interpolator 接口的父接口,所以你可以直接使用现有的任何 Interpolator 的实现类(像是 Bounce)作为 ValueAnimator 中方法的参数。

ObjectAnimator

ObjectAnimator 应该是新的动画系统中你会用到最多的类了。你使用这个类来创建拥有 ValueAnimator 所需要的计时器 和 值的动画,并给动画设置目标对象和属性名称。然后它就可以自动地去计算动画值并把动画值赋给指定的对象和属性。例如,让某个 myObject 对象做淡出动画,我们可以像这样对 alpha 值做动画:

ObjectAnimator.ofFloat(myObject, "alpha", 0f).start();

注意,在这个例子中展示了一个使你可以更方便地使用的特性,那就是你可以只告诉它你希望它做动画的结束值,然后它就会使用当前的属性值作为起始值。在这种情况下,这个动画会从当前的 alpha 值一直做动画到 alpha 值为0.

你可以向下面一样通过 XML 文件创建同样的动画:

<ObjectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:valueTo="0"
    android:propertyName="alpha"/>

注意,使用 XML 创建 ObjectAnimator 对象的时候,你无法设置目标对象;你必须通过 XML 将对象加载出来,然后通过 java 代码设置目标对象:

ObjectAnimator anim = AnimatorInflator.loadAnimator(context, resID);
anim.setTarget(myObject);
anim.start();

其实这里有一个你必须要理解的关于属性的隐含条件,那就是 getter/setter 方法:你的对象必须拥有一个公有的 set 方法,这个方法的名称和需要做动画的属性名称相对应,并拥有相应的的参数类型。然后,如果你只使用一个结束值,就像上面的例子那样,动画系统会需要自动去获取对象的动画起始值,所以你必须提供一个 get 方法来返回相应类型的值。例如,上面代码中的透明度动画要想成功执行,myObject 类必须拥有以下两个公有方法:

public void setAlpha(float value);
public float getAlpha();

所以如果你将某个类型的值以及某个叫做 foo 的属性设置给某个对象,那么你已经隐式地认为这个对象至少拥有一个 setFoo() 方法,有可能还有一个 getFoo() 方法(如果你使用一个值的话会用到),这两个方法所处理的类型都必须和动画中所声明的值的类型所一致。如果所有这些都符合,那么这个动画会找到目标对象中的那些 getter/setter 方法并在动画过程中进行调用。如果这些方法不存在,那么动画会在运行时失败(fail at runtime),因为它无法找到它要用到的方法。(使用 Proguard,或者其他代码混淆工具的开发者请注意:如果你的 setter/getter 方法在代码中其他地方都没有被调用,请明确地告诉混淆工具不要混淆这两个方法,因为你不这样做的话,混淆工具很有可能把它们混淆掉。在动画创建过程中进行的绑定是非常松散的(loose),那些工具无从知道这些方法会在运行时被调用)。

View 属性

善于观察的读者(至少是那些还没有浏览过其他相关文章的善于观察的读者)可能会发现新的动画系统的一个问题。既然新的动画框架是用来对属性进行动画的,而且在很大程度上它会被用来对 View 对象做动画,那么它怎么用在 View 这个没有通过 get/set 方法暴露任何属性的类呢?

这是一个非常棒的问题:你获得口头奖励一次,继续阅读下去吧。

为了使它可以工作,我们在 Honeycomb 中为 View 类加入了新的属性。旧的动画系统只是通过改变 View 画的方式来进行变换和淡入淡出。实际上可以用来真正进行变换的功能是被 View 的容器所拥有的,因为 View 本身是没有可以进行变换的属性的。但是现在它有了:我们向 View 中加入了一些属性,使得直接对 View 进行动画成为可能,它不只是改变 View 看起来的样子,而是真正的改变它的位置和方向。下面是你可以在 View 中 直接获取、设置并做动画 的新属性:

translationX 和 translationY:这些属性控制 View 实际摆放的位置和在父容器指定的位置之间的差值(左上角坐标)。你可以通过对这两个属性做动画来实现一个 Button 的移动动画,像这样:ObjectAnimator.ofFloat(view, "translationX", 0f, 100f)。
rotation、rotationX 和 rotationY:这些属性控制 View 相对于轴点(pivot point)的 2D 旋转值 和 3D 的旋转值。
scaleX 和 scaleY:这些属性控制 View 相对于轴点(pivot point)的 2D 的放缩值。
pivotX 和 pivotY:这些属性控制着轴点(pivot point)的位置,旋转和放缩的执行都会以此为轴。默认情况下,轴点位于 View 对象的中心。
x 和 y:这两个属性用来 描述 View 在它的父容器中的 最终 位置,它的值是 left/top 和 translationX/translationY 之和。
alpha:这是我个人最喜欢的属性。你不再需要通过在执行的过程中去改变一个值来对一个对象进行淡出动画(No longer is it necessary to fade out an object by changing a value on its transform)。取而代之的是,View 本身拥有了一个实际的 alpha 值。这个值在默认情况下是 1 (完全不透明),当值为 0 的时候会完全透明。想要让一个 View 做淡出动画,你可以这样做:ObjectAnimator.ofFloat(view, "alpha", 0f)

注意,以上所有的属性都是可以通过 set/get 方法去设置和获取的(例如:setRotation() 和 getRotation)。这些方法会使得新的动画系统在动画过程中可以去获取当前值,并在动画过程中去根据中间值做正确的事情。也就是说,你当然不希望当你放缩一个对象的时候你设了值但是没有任何反应,因为系统不知道这个对象需要重绘了;View 的每个 setter 方法会在内部适时调用 invalidate 来使渲染工作顺利进行。

AnimatorSet

这个类就像之前动画系统的 AnimationSet 类一样,是用来使多个动画进行组合变得简单的。假如你有一些动画需要组合执行,比如你想先让一些 View 做淡出动画,让另一些 View 在它们淡出的过程中平移过来。你可以分别用单独的动画来做这些,或者通过在正确的时间调用 start 方法,或者通过调用 startDelay 来设置有延迟的动画。或者,你可以使用 AnimatorSet 来帮助你更加高效的完成这些操作。AnimatorSet 允许你通过调用方法 playTogether(Animator...) 来同时执行多个动画;允许你通过调用 playSequentially(Animator...) 来一个接一个的执行动画;或者你可以自己构建一组动画使它们一起、一个接一个或者给它们指定播放延时(specify delays),通过调用 AnimatorSet.Build 类中的 with() 方法、 before() 方法和 after() 方法。例如,让 v1 做淡出动画,同时让 v2 从旁边平移进来,我们可以这样做:

ObjectAnimator fadeOut = ObjectAnimator.ofFloat(v1, "alpha", 0f);
ObjectAnimator mover = ObjectAnimator.ofFloat(v2, "translationX", -500f, 0f);
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(v2, "alpha", 0f, 1f);
AnimatorSet animSet = new AnimatorSet().play(mover).with(fadeIn).after(fadeOut);;
animSet.start();

像 ValueAnimator 和 ObjectAnimator 一样,你也可以在 XML 中定义 AnimatorSet。

TypeEvaluator

我再讨论最后一部分,然后就轮到你自己去探索代码和 API 示例了。我想要提到的最后一课是 TypeEvaluator。可能你在大多数动画中都不会直接用到这个类,但是你需要了解一些以防遇到你需要用到它的情况。正如我之前说的,动画系统知道如何处理 float 和 int 类型,如果要处理其他类型的话,就需要一些帮助来定义如何计算所给类型的差值了。例如,如果你想像上面的其中一个例子一样对 Point 对象做动画,系统怎样才能知道如何计算初始值和结束值之间的差值呢?答案是:你来告诉系统如何计算差值,通过 TypeEvaluator 类。

TypeEvaluator 是一个简单的接口,这个接口会在动画的每一帧被动画系统所回调来计算当前值(animated value)。它会收到三个参数,一个 float 值用来表示当前动画进行的百分比,另外两个是你创建动画的时候设置的起始值和结束值;然后它会返回一个你根据动画进行百分比计算得到的当前值。例如,内置的用来计算 float 值的 FloatEvaluator 类:

public class FloatEvaluator implements TypeEvaluator {
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        float startFloat = ((Number) startValue).floatValue();
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
    }
}

但是如果是更加复杂的类型呢?继续上面的例子,这里我们实现了一个 Point 类的计算类(evaluator):

public class PointEvaluator implements TypeEvaluator {
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;
        return new Point(startPoint.x + fraction * (endPoint.x - startPoint.x),
            startPoint.y + fraction * (endPoint.y - startPoint.y));
    }
}

通常的做法是,这个计算类只提供两个值之间的线性差值计算。在这个例子中,每一个“值”包含两个子值(x 和 y),所以它要做的是对这两个值进行线性差值的计算。

你可以通过 ValueAnimator 类的 setEvaluator() 方法或者使用带有 TypeEvaluator 参数的工厂方法来创建 ValueAnimator 的实例。继续我们之前关于 Point 类的例子,我们可以这样使用刚才创建的 PointEvaluator 类:

Point p0 = new Point(0, 0);
Point p1 = new Point(100, 200);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), p0, p1);

你使用这个接口的一个场景是使用一个叫做 ArgbEvaluator 的类,这个类存在于 SDK 中。当你需要对颜色属性做动画的时候,你可能会自动用到这个类(场景是当你在 XML 中创建一个 Animator 并把颜色值作为值设置进去)或者你可以在 java 代码中将它设置进去(就像上面提到的那样)。

但是等一等,这里还有更多!

在新的动画系统中还有很多特性是我没有提到的。像是重复方法(repetition functionality)、动画生命周期事件的监听器、向工厂方法中提供更多的值使得动画不止在两个值之间做动画、使用 Keyframe 类来指定更加复杂的时间/值序列(time/value sequence)、使用 PropertyValueHolder 来指定多个属性并行做动画(animate in parallel)、使用 LayoutTransition 来实现自动的布局动画、以及很多很多其他的特性。但是我不得不结束这篇文章去码代码了。以后我会尽量这方面的文章,想看接下来的文章、教程、视频的话请持续关注我在graphics-geek.blogspot.com上的博客。那么现在,下载 API Demos,开始浏览 3.0 SDK 中发布的 属性动画(Property Animation)部分,好好研究一下代码,跟它愉快地玩耍吧。

标签: android

热门推荐