原文:Android Animation Tutorial with Kotlin
作者:Lisa Luo
译者:kmyhy更新说明:本教程由 Lisa Luo 更新至 Kotlin 和 Android Studio 3.0。原教程作者是 Artem Kholodnyi。
假若没有那些有趣的、漂亮的动画元素,很难想象手机的使用体验会是什么样子。这些动画不仅仅在整个 app 中负责引导用户,而且也丰富了我们的屏幕。
要创建让屏幕对象生动起来的动画看起来像是在制造飞机发动机,但不用害怕!Android 中有几个工具能够在你创建动画时变得轻松。
你将在本教程中学习一些基本的动画工具,同时将小狗通过火箭发送到太空(也可能是月亮),然后让它安全第回到地球上:]
要制作这个动画,你要学习如何:
让我们来学做一个火箭科学家吧 :]
预备知识:本教程和动画相关,因此必须熟悉基本的 Android 编程和 Kotlin,Android Studio 和 XML 布局。
如果你是一个 Android 新手,你可以阅读《Beginning Android Development Part One》。
几个动画、几句代码、疾驰的火箭。
动画是很有趣的研究话题!最好的学习制作动画的方式是动手编写代码:]
首先请下载 Rocket Launcher Starter。将它导入到 Android Studio 3.0 Beta 7 及更高版本,在设备上运行项目。你会很快就会发现你将要做些什么了。你的手机上将显示出你将实现的动画的列表。
随便点击一个列表项。
你会看到两张静态图片:一只小狗和火箭,小狗已经准备好接下来的旅程了。现在,所有的画面的是一样的,动画还没有实现呢。
在开始实现第一个动画之前,先来点理论,让你搞清楚魔术后面的伎俩 :]
假设你想将火箭从屏幕底部移动到屏幕顶部,同时火箭应该用 50 ms 的时间到达。
以下示意图显示火箭的位置应该如何变化:
上面的动画是平滑而且不间断的。但是智能机是数字系统,它使用的是不连续的数值。对于它们来说,时间并不是连续的,它是以每一次前进一小点的方式运行的。
动画由多个静止图像构成,也就是所谓的帧构成,它在特定的时间内显示其中一张图像。这个概念和卡通片第一次出现时没有不同,只不过绘图的方式上不同。
连续两帧之间的时间间隔叫做帧刷新时间——对于属性动画而言,这个值默认是 10 ms。
动画和早期的胶片电影的不同之处在于:因为火箭是以恒定速度运动的,你就可以计算出它在任意时间上的位置。
请看下图中显示的 6 个动画帧。注意:
You see six animation frames shown below. Notice that:
摘要:当绘制某一帧时,根据动画进行的时间和帧刷新率来算出火箭的位置。
幸好,你不需要完全自己来计算,因为 ValueAnimator 已经为你进行计算了。:]
要创建动画,你只需要指定要进行动画的属性的开始值和终止值,以及动画进行到的时间。你还需要添加一个监听者,它会在每一帧设置火箭的新位置。
你可能注意到火箭在整个动画过程中是以恒速运动的——这很不符合实情。材料设计建议你用更自然的方式创建生动的动画来吸引用户的注意。
Android 的 animation 框架使用了时间插值器。ValueAnimator 中包含了一个事件插值器——它是一个实现了 TimeInterpolator 接口的对象。时间插值器决定了属性值如何随时间变化。
再来看一眼那张位置在最简单情况下——即线性插值器随时间变化的图:
下面列出线性插值器如何根据时间来改变值:
根据时间的增长,火箭位置以一种恒速或线性的方式进行变化。
动画也可以使用非线性的插值器。例如 AccelerateInterpolator,它很有趣:
它会对输入值进行平方,导致火箭的速度一开始慢然后迅速加速——就像真实的火箭一样!
这就是一开始我们需要学习的理论知识了,现在是时候开始……
首先花点时间来熟悉这个项目。com.raywenderlich.rocketlauncher.animationactivities 这个包包含了 BaseAnimationActivity 及其子类。
打开 res/layout 文件夹下的 activity_base_animation.xml 文件。
在 root 节点下,你会看到一个 FrameLayout 包含了两个带图片的 ImageView:一个是 rocket.png,一个是 doge.png。它们的 android:layout_gravity 都被设置为 bottom|center_horizontal,这样图片会位于屏幕底部的中点。
注意:在本教程中,你会进行大量文件打开操作。在 Android Studio 中可以用下列快捷键:
打开任意文件用 command + shift + O ( Mac)或者 Ctrl + Shift + N ( Linux 和 Windows)
打开一个 Kotlin 类用 command + O(Mac) 或者 Ctrl + N(Linux 和 Windows)
在本 app 中,BaseAnimationActivity 是一个其它动画 activity 的父类。
打开 BaseAnimationActivity.kt 看看。在文件头部,是能被所有动画 activity 访问的成员变量:
注意火箭和狗狗两个都是 ImageView,但我们将它们声明为 View,因为属性动画对所有 Android 视图都能使用。视图以懒加载的方式声明,因为它们在布局 inflate 并绑定到对应的 Activity 生命周期事件比如 onCreate() 之前都是 null。
看一下 onCreate() 方法中的代码:
// 1
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_base_animation)
// 2
rocket = findViewById(R.id.rocket)
doge = findViewById(R.id.doge)
frameLayout = findViewById(R.id.container)
// 3
frameLayout.setOnClickListener { onStartAnimation() }
这段代码主要做了什么:
这段代码会被本教程中所有 activity 所继承。熟悉完它之后,让我们来开始编写自定义的代码。
如果火箭不被发射,狗狗哪也不能去,因此这非常适合作为开始动画,而且它也非常简单。没有想到,火箭科学家其实也这么简单吧?
打开 LaunchRocketValueAnimatorAnimationActivity.kt, 在 onStartAnimation() 中添加代码:
//1
val valueAnimator = ValueAnimator.ofFloat(0f, -screenHeight)
//2
valueAnimator.addUpdateListener {
// 3
val value = it.animatedValue as Float
// 4
rocket.translationY = value
}
//5
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
//6
valueAnimator.start()
Build & run。从列表中选择 Launch a Rocket。你会看到一个新的画面,点击它!
有意思吧?别担心狗狗被留在了原地,马上它就会乘坐着火箭飞到月亮上。
给火箭来点旋转怎么样?打开 RotateRocketAnimationActivity.kt 在 onStartAnimation() 中加入:
// 1
val valueAnimator = ValueAnimator.ofFloat(0f, 360f)
valueAnimator.addUpdateListener {
val value = it.animatedValue as Float
// 2
rocket.rotation = value
}
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
valueAnimator.start()
发现有什么变化了吗?
Build& run,选择 Spin a rocket。在新界面上点击:
打开 AccelerateRocketAnimationActivity.kt 仍然是 onStartAnimation(),添加:
// 1
val valueAnimator = ValueAnimator.ofFloat(0f, -screenHeight)
valueAnimator.addUpdateListener {
val value = it.animatedValue as Float
rocket.translationY = value
}
// 2 - Here set your favorite interpolator
valueAnimator.interpolator = AccelerateInterpolator(1.5f)
valueAnimator.duration = BaseAnimationActivity.DEFAULT_ANIMATION_DURATION
// 3
valueAnimator.start()
上述代码和 LaunchRocketValueAnimationActivity.kt 中的onStartAnimation() 一模一样,除了这句: 即设置 valueAnimator.interpolator 的这一句。
Build & run,选择 Accelerate a rocket。点击屏幕看看。
可怜的狗狗还是没能坐上飞往月球的火箭……可怜的狗狗。再忍耐一下,狗狗!
因为使用的是 AccelerateInterpolator,你会发现火箭点火之后以加速度前进。请随便尝试一下各种插值器。放心,我会等你的!
目前,我们对位置和角度进行过动画,但 ValueAnimator 并不关心你将它的值用在什么地方。
你可以告诉 ValueAnimator 去对下列类型的值进行动画:
你可以对 View 的任意属性进行动画。例如:
ObjectAnimator 是一个 ValueAnimator 的子类。如果你需要对单个对象的单个属性进行动画,就可以使用 ObjectAnimator 了。
和 ValueAnimator 不同,ValueAnimator 需要你设置一个监听器对某个值做某些事情,ObjectAnimator 几乎是自动地为你处理好这些事情:]
打开 LaunchRocketObjectAnimatorAnimationActivity.kt 类,编写如下代码:
// 1
val objectAnimator = ObjectAnimator.ofFloat(rocket, "translationY", 0f, -screenHeight)
// 2
objectAnimator.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
objectAnimator.start()
这段代码完成了如下功能:
创建一个 ObjectAnimator 对象(和 ValueAnimator 一样),只不过多了前两个参数:
设置动画的时长并开始动画。
运行项目,选择 Launch a rocket(ObjectAnimator)。点击屏幕。
火箭会向之前使用 ValueAnimator 一样运动,但代码更少:]
注意:ObjecdtAnimator 有一个限制——它不能同时驱动两个对象,你必须创建两个 ObjectAnimator 。
根据你自己的情况以及准备使用的代码量来决定要使用 ObjectAnimator 还是 ValueAnimator。
结合我们的例子,可以尝试一下对颜色进行动画。创建 animator 时,无论是 ofFloat() 还是 ofInt() 都不能使用颜色作为参数。你得使用 ArgbEvaluator。
打开 ColorAnimationActivity.kt 在 onStartAnimation() 添加代码:
//1
val objectAnimator = ObjectAnimator.ofObject(
frameLayout,
"backgroundColor",
ArgbEvaluator(),
ContextCompat.getColor(this, R.color.background_from),
ContextCompat.getColor(this, R.color.background_to)
)
// 2
objectAnimator.repeatCount = 1
objectAnimator.repeatMode = ValueAnimator.REVERSE
// 3
objectAnimator.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
objectAnimator.start()
在上面的代码中,我们:
调用 ObjectAnimator.ofObject() 方法并传入以下参数:
Build & run。选择 Background Color,然后点击屏幕。
太帅了!很快找到诀窍了吧。背景色的变化过程如黄油一般顺滑。
对一个 View 进行动画固然好,但你一次只能修改一个对象和一个属性。动画显然不仅限于此。
是时候将狗狗送上天了!
AnimatorSet 允许你将多个动画绑定到一起或者放到一个序列里。将你的的第一个动画传递给 play() 方法,这个方法接受一个 Animator 对象作为参数并返回一个 builder。
然后在 builder 上调用这些方法,每个方法都接受一个 Animator 作为参数:
你可以通过这些方法进行链式调用。
打开 LaunchAndSpinAnimatorSetAnimatorActivity.kt 在 onStartAnimation() 中编写代码:
// 1
val positionAnimator = ValueAnimator.ofFloat(0f, -screenHeight)
// 2
positionAnimator.addUpdateListener {
val value = it.animatedValue as Float
rocket.translationY = value
}
// 3
val rotationAnimator = ObjectAnimator.ofFloat(rocket, "rotation", 0f, 180f)
// 4
val animatorSet = AnimatorSet()
// 5
animatorSet.play(positionAnimator).with(rotationAnimator)
// 6
animatorSet.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
animatorSet.start()
在这段代码中,你进行了:
Build & run。选择 Launch an spin(AnimatorSet)。点击屏幕。
狗狗再次被物理法则无视了。
另外还有一个让你驱动同一对象多个属性的工具,这个工具就是……
在动画代码中最爽的莫过于使用 ViewPropertyAnimator 了,你会看到它的使用使得代码更易于读写。
打开 LaunchAndSpinViewPropertyAnimatorAnimationActivity.kt 在 onStartAnimation() 中加入:
rocket.animate()
.translationY(-screenHeight)
.rotationBy(360f)
.setDuration(BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION)
.start()
这里,animate() 返回了一个 ViewPropertyAnimator 实例,你可以对它进行链式调用。
Build & run,选择 Launch and spin (ViewPropertyAnimator),你会看到和上一节一样的动画。
比较一下上一节中使用 AnimatorSet 的代码:
val positionAnimator = ValueAnimator.ofFloat(0f, -screenHeight)
positionAnimator.addUpdateListener {
val value = it.animatedValue as Float
rocket?.translationY = value
}
val rotationAnimator = ObjectAnimator.ofFloat(rocket, "rotation", 0f, 180f)
val animatorSet = AnimatorSet()
animatorSet.play(positionAnimator).with(rotationAnimator)
animatorSet.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
animatorSet.start()
ViewPropertyAnimator 对多个同步动画提供了更好的效率。它减少了不必要的调用,因此多个属性的动画只发生了一次——和每个属性单独驱动相比而言,后者会导致每个属性都有一些无效的操作。
ValueAnimator 有一个好用的特点,就是你可以重用它的动画值,将它用到多个对象中。
打开 FlyWithDogeAnimationActivity.kt 在 onStartAnimation() 加入:
//1
val positionAnimator = ValueAnimator.ofFloat(0f, -screenHeight)
positionAnimator.addUpdateListener {
val value = it.animatedValue as Float
rocket.translationY = value
doge.translationY = value
}
//2
val rotationAnimator = ValueAnimator.ofFloat(0f, 360f)
rotationAnimator.addUpdateListener {
val value = it.animatedValue as Float
doge.rotation = value
}
//3
val animatorSet = AnimatorSet()
animatorSet.play(positionAnimator).with(rotationAnimator)
animatorSet.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
animatorSet.start()
上述代码中,你创建了 3 个 animator:
注意你在第一个 animator 中同时设置了两个对象的 translation。
运行 app,选择 Don’t leave Doge behind(Animating two objects)。你应该明白了什么了吧。登月开始了。
动画通常表示某个动作已经发生或者将会发生。通常,在动画结束,都会做点什么动作。
你不用去观察它,也能知道当动画结束时,火箭会在屏幕以外某个地方呆着。如果你不准备让它招着陆或者关闭 activity,你可以将它移除以节省资源。
AnimatorListener — 会在下列事件发生时接收到来自于 animator 的通知:
打开 WithListenerAnimationActivity.kt 在 onStartAnimation() 中添加:
//1
val animator = ValueAnimator.ofFloat(0f, -screenHeight)
animator.addUpdateListener {
val value = it.animatedValue as Float
rocket.translationY = value
doge.translationY = value
}
// 2
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
// 3
Toast.makeText(applicationContext, "Doge took off", Toast.LENGTH_SHORT)
.show()
}
override fun onAnimationEnd(animation: Animator) {
// 4
Toast.makeText(applicationContext, "Doge is on the moon", Toast.LENGTH_SHORT)
.show()
finish()
}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationRepeat(animation: Animator) {}
})
// 5
animator.duration = 5000L
animator.start()
上述代码中,除了多了几个监听器方法,与之前并无不同。这里我们做了这些事情:
运行 app。选择 Animation events。点击屏幕。看到这些消息了吗?
注意:你还可以在 ViewPropertyAnimator 上在调用链的 start() 之前添加一个 setListener :
rocket.animate().setListener(object : Animator.AnimatorListener { // Your action })
此外,可以在 animate() 之后通过 withStartAction(Runnable) 和 withEndAction(Runnable) 来设置 start 事件和 end 事件。它们和使用 AnimatorListener 设置这些事件是一样的。
Animation 不是黔之驴,只知道前进、停止。它们可以循环、反转、以某个指定时长运行等等。
在 Android 中,你可以用下列方法来调整动画:
打开 FlyThereAndBackAnimationActivity.kt 在 onStartAnimation() 中加入:
// 1
val animator = ValueAnimator.ofFloat(0f, -screenHeight)
animator.addUpdateListener {
val value = it.animatedValue as Float
rocket.translationY = value
doge.translationY = value
}
// 2
animator.repeatMode = ValueAnimator.REVERSE
// 3
animator.repeatCount = 3
// 4
animator.duration = 500L
animator.start()
在这里,你做了:
设置 repeatMode,可以是以下取值:
在这里,我们设置的是 REVERSE,因为我们想让火箭在起飞后又回到原来的起点。像 SpaceX 一样!:]
往返两次。
注意:为什么在 代码 3 的地方将重复次数指定为 3?每次往返需要循环两次,因此设置为循环次数 3 会让狗狗往返地球 2 次:1次用于第一次起飞后的着陆,另外 2 次则分别用于第 2 次的起飞和着陆。你喜欢让狗狗来回折腾几次?自己去试试呗!
运行 app。选择 Fly there and back(Animation Options),新的窗口打开,点击屏幕。
看到火箭像蚂蚱一样蹦跳了吧!伊隆.马斯克,接招!:]
我们来看教程中最精彩的部份。在最后一节中,你将学习如何在一个地方声明,在多个地方使用——是的,你完全可以毫无困难地重用你的动画。
通过在 XML 中定义动画,你可以在你的代码中随意重用它。在 XML 中定义动画和你构造视图布局有点类似。
在开始项目的 res/animator 中有一个 jump_and_blink.xml 文件。打开这个文件,你会看到:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
set>
你会用到的 XML 标签包括:
在 XML 中使用一个 AnimatorSet 时,它里面需要嵌套 ValueAnimator 和 ObjectAnimator 对象,就好比在进行布局时需要将 View 对象嵌套在 ViewGroup 对象(RelativeLayout,LinearLayout等)中一样。
将 jump_and_blink.xml 中的内容修改为:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<objectAnimator
android:propertyName="alpha"
android:duration="1000"
android:repeatCount="1"
android:repeatMode="reverse"
android:interpolator="@android:interpolator/linear"
android:valueFrom="1.0"
android:valueTo="0.0"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="translationY"
android:duration="1000"
android:repeatCount="1"
android:repeatMode="reverse"
android:interpolator="@android:interpolator/bounce"
android:valueFrom="0"
android:valueTo="-500"
android:valueType="floatType"/>
set>
这里定义了一个根元素,即 set 标签。它的 ordering 属性要么是 together 要么是 sequential。默认是 together,但为了明确起见,这里还是写明好些。set 标签中有两个子标签,每个都是一个 objectAnimator。
注意 objectAnimator 的如下属性:
总之,你添加了两个 objectAnimator 到这个 AnimatorSet 中,它们会同时播放。现在看看如何使用它们。
找到 XmlAnimationActivity.kt 在 onStartAnimation() 中加入:
// 1
val rocketAnimatorSet = AnimatorInflater.loadAnimator(this, R.animator.jump_and_blink) as AnimatorSet
// 2
rocketAnimatorSet.setTarget(rocket)
// 3
val dogeAnimatorSet = AnimatorInflater.loadAnimator(this, R.animator.jump_and_blink) as AnimatorSet
// 4
dogeAnimatorSet.setTarget(doge)
// 5
val bothAnimatorSet = AnimatorSet()
bothAnimatorSet.playTogether(rocketAnimatorSet, dogeAnimatorSet)
// 6
bothAnimatorSet.duration = BaseAnimationActivity.Companion.DEFAULT_ANIMATION_DURATION
bothAnimatorSet.start()
在上述代码中,你做了这些事情:
嘘!只剩下最后一小个地方了:]
Build & run。选择 Jump and blink (Animations in XML)。点击屏幕看看你努力的成果。
你会看到狗狗跳起来,消失,最后又安全返回地面!:]
在这里下载最终完成项目。
在本教程中,你学习了:
基本上,你已经领略了 Android 动画的强大。
如果你想学习更多,请阅读 Android 文档中的时间插值器(请看 Known Indirect Subclasses)。如果你对它们不感兴趣,你可以自己动手实现。你也可以设置动画的 Keyframes,让动画更加复杂。
Android 也有其它动画系统,比如 View 动画和 Drawable 动画。当然也可以用 Canvas 和 OpenGL ES API 去创建动画。尽请期待:]
希望你喜欢本教程。请在论坛中留下你的问题、看法和建议。