reddit 上刚看到这篇文章标题时我是这么想的,写点动画犯得着用 rx 吗?读完表示这么干确实不错。
原文地址: http://zenandroid.io/building-complex-android-animations-with-rxjava/
本文将介绍如何使用 RxJava 优雅的设计 Android 动画,并结合 kotlin 的扩展方法,摆脱重复代码。效果就是下面这个动图。
[toc]
What we are trying to achieve
目标效果如图:
慢放:
简单描述下:
- 两组菜单,分别包含三个子项 fab,每个子项都有对应的描述信息
- 一个大的 fab ,用于开启或关闭第一组菜单
- 用户点击第一组 fab 某项之后,显示第二组 fab 菜单
- 小 fab 入场动画: fab滑入的同时由小变大,就位后描述文字 fadein 出现
- 大 fab 自己还有旋转动画
The classic way: ViewPropertyAnimator
实现动画最简单的方的就是使用 ViewPropertyAnimator
框架。它提供一系列简便的方式对 单个 view
执行动画。比如 fade out 一个 view:1
view.animate().alpha(0)
缩放:1
2
3
4view.animate()
.alpha(0)
.scaleX(0.5f)
.scaleY(0.5f)
非常方便,代码清晰明了。那如何将几个动画按顺序执行呢?
1 | view.animate() |
是不是不那么优雅了。就算用了 kotlin 提供的 lambda 表达式也没多大帮助。而我们将要实现的动画涉及到更多的步骤,如果光靠 withEndAction
嵌套来嵌套去一定会把人整疯。总之在这种复杂场景用起来相当费劲。
Enter RxJava
进入正题啦。
每个动画自己就是一个异步操作。你启动了一个 animation,在几毫秒内执行一些任务,然后结束。我们当前只关心动画结束后启动另一个 animation 或是其他操作。
对应到 RxJava2 里,Completable
就是为这种操作而生的。
RxJava2 直接提供了
Completable
,使用 RxJava 1 也可以非常简单的实现这个组件。
基本要点就是用 Completable
来 warp 你的 animation,并在 withEndAction
里调用 onComplete
。看代码:1
2
3
4
5
6val animation = Completable.create {
view.animate()
.alpha(0f)
.withEndAction(it::onComplete)
}
animation.subscribe()
包装后的代码功能上与原来相同,好处就是我们可以 RxJava提供的操作符将 animation (或者其他 RxJava 操作,比如网络请求)结合在一起。比如你想让两个动画先后执行可以使用 andThen
,同时执行可以使用 Completable.mergeArray
。
比如在执行完一个 fade 后 fade 另一个 view1
2
3
4
5
6
7
8
9
10
11val animation = Completable.create {
view.animate()
.alpha(0f)
.withEndAction(it::onComplete)
}
val animation1 = Completable.create {
view1.animate()
.alpha(0f)
.withEndAction(it::onComplete)
}
animation.andThen(animation1).subscribe()
同时 fade 两个 view1
2
3
4
5
6
7
8
9
10
11val animation = Completable.create {
view.animate()
.alpha(0f)
.withEndAction(it::onComplete)
}
val animation1 = Completable.create {
view1.animate()
.alpha(0f)
.withEndAction(it::onComplete)
}
Completable.mergeArray(animation, animation1).subscribe()
当两个 view 的动画结束后,对另外两个 view 执行动画1
2
3
4
5val animation = ...
val animation1 = ...
Completable.mergeArray(animation, animation1)
.andThen { Completable.mergeArray(animation2, animation3) }
.subscribe()
看看,只要你熟悉 rxjava 的操作符,写这种动画变得多简单。一定记得调用 subscribe ,不然就不会动了。
本文的第一个任务已经完成了,但是从中还是能看到不少重复的代码,使用 kotlin 可以做到更好的效果。
Bonus: using Kotlin extension functions for animations
作者使用以下代码移除样板代码。
1 | fun View.fadeOut(duration: Long = 30): Completable { |
使用如下1
myView.fadeOut().subscribe()
可以到作者的这个项目里看看更多方法 ViewExtensions.kt
1 | longFab.slideIn(fabMiniSize).andThen(longLabel.fadeIn()) |
Putting it all together
让我们回到最开始的目标,实现这个动画。
分别命名两组 fab 为 Speed Menu
和 Size Menu
1
2
3
4
5
6private fun showSpeedMenu() = Completable.mergeArray(
fab.rotate(45f),
longFab.slideIn(fabMiniSize).andThen(longLabel.fadeIn()),
normalFab.slideIn(2 * fabMiniSize).andThen(normalLabel.fadeIn()),
blitzFab.slideIn(3 * fabMiniSize).andThen(blitzLabel.fadeIn())
)
首先,同时启动了4个动画,并等待其结束:
- 旋转大的 fab
- 第一个小 fab 滑入,label 渐入
- 第二个小 fab 滑入,label 渐入
- 第三个小 fab 滑入,label 渐入
启动动画1
showSpeedMenu().subscribe()
hide 和 show 很相似
1 | private fun hideSpeedMenu() = Completable.mergeArray( |
最棒的地方来了,当你描述完所有动画后,可以将他们串在一起。下面这几行代码就实现了点击第一步显示的菜单后,隐藏菜单,显示子菜单的动画。
1 |
|
现在,我们终于就可以轻松的将动画结合在一起,同时设置好 menu 状态。要是用原来的方法可不会这么容易。
关于 menu 状态的处理,目前的实现不是很优雅,如果你有什么建议可以在评论里告诉我~1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun onFabClicked() {
menuState = when(menuState) {
FabMenuState.OFF -> {
showSpeedMenu().subscribe()
FabMenuState.SPEED
}
FabMenuState.SPEED -> {
hideSpeedMenu().subscribe()
FabMenuState.OFF
}
FabMenuState.SIZE -> {
hideSizeMenu().subscribe()
FabMenuState.OFF
}
}
fadeOutMask.showIf(menuState != FabMenuState.OFF)
}
完整代码 MainActivity.kt
reddit 里的一些讨论 link
- 一个新的基于响应式设计的动画框架 material-motion-android
- VanGogh 一个类似的动画库
- 也有人指出
withEndAction
只有在动画正常结束时才会被调用,有可能导致泄露 - 接上条,有人要加
CompositeDisposable
- 接上条,也许存在一个回调函数比
withEndAction
更合适