2018-02-23

[翻译]使用 RxJava 构建 Android 复杂动画

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
4
view.animate()  
.alpha(0)
.scaleX(0.5f)
.scaleY(0.5f)

非常方便,代码清晰明了。那如何将几个动画按顺序执行呢?

1
2
3
4
5
6
7
8
9
10
view.animate()  
.alpha(0)
.scaleX(0.5f)
.scaleY(0.5f)
.withEndAction {
secondView.animate()
.alpha(0)
.scaleX(0.5f)
.scaleY(0.5f)
}

是不是不那么优雅了。就算用了 kotlin 提供的 lambda 表达式也没多大帮助。而我们将要实现的动画涉及到更多的步骤,如果光靠 withEndAction 嵌套来嵌套去一定会把人整疯。总之在这种复杂场景用起来相当费劲。

Enter RxJava

进入正题啦。

每个动画自己就是一个异步操作。你启动了一个 animation,在几毫秒内执行一些任务,然后结束。我们当前只关心动画结束后启动另一个 animation 或是其他操作。

对应到 RxJava2 里,Completable 就是为这种操作而生的。

RxJava2 直接提供了 Completable,使用 RxJava 1 也可以非常简单的实现这个组件。

基本要点就是用 Completable 来 warp 你的 animation,并在 withEndAction 里调用 onComplete。看代码:

1
2
3
4
5
6
val animation = Completable.create {  
view.animate()
.alpha(0f)
.withEndAction(it::onComplete)
}
animation.subscribe()

包装后的代码功能上与原来相同,好处就是我们可以 RxJava提供的操作符将 animation (或者其他 RxJava 操作,比如网络请求)结合在一起。比如你想让两个动画先后执行可以使用 andThen ,同时执行可以使用 Completable.mergeArray

比如在执行完一个 fade 后 fade 另一个 view

1
2
3
4
5
6
7
8
9
10
11
val 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 两个 view

1
2
3
4
5
6
7
8
9
10
11
val 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
5
val animation = ...  
val animation1 = ...
Completable.mergeArray(animation, animation1)
.andThen { Completable.mergeArray(animation2, animation3) }
.subscribe()

看看,只要你熟悉 rxjava 的操作符,写这种动画变得多简单。一定记得调用 subscribe ,不然就不会动了。

本文的第一个任务已经完成了,但是从中还是能看到不少重复的代码,使用 kotlin 可以做到更好的效果。

Bonus: using Kotlin extension functions for animations

作者使用以下代码移除样板代码。

1
2
3
4
5
6
7
8
9
10
fun View.fadeOut(duration: Long = 30): Completable {  
return Completable.create {
animate().setDuration(duration)
.alpha(0f)
.withEndAction {
visibility = View.GONE
it.onComplete()
}
}
}

使用如下

1
myView.fadeOut().subscribe()  

可以到作者的这个项目里看看更多方法 ViewExtensions.kt

1
longFab.slideIn(fabMiniSize).andThen(longLabel.fadeIn())  

Putting it all together

让我们回到最开始的目标,实现这个动画。

分别命名两组 fab 为 Speed MenuSize Menu

1
2
3
4
5
6
private 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个动画,并等待其结束:

  1. 旋转大的 fab
  2. 第一个小 fab 滑入,label 渐入
  3. 第二个小 fab 滑入,label 渐入
  4. 第三个小 fab 滑入,label 渐入

启动动画

1
showSpeedMenu().subscribe()  

hide 和 show 很相似

1
2
3
4
5
6
private fun hideSpeedMenu() = Completable.mergeArray(
fab.rotate(0f),
longLabel.fadeOut().andThen(longFab.slideOut(fabMiniSize)),
normalLabel.fadeOut().andThen(normalFab.slideOut(2 * fabMiniSize)),
blitzLabel.fadeOut().andThen(blitzFab.slideOut(3 * fabMiniSize))
)

最棒的地方来了,当你描述完所有动画后,可以将他们串在一起。下面这几行代码就实现了点击第一步显示的菜单后,隐藏菜单,显示子菜单的动画。

1
2
3
4
5
6
7
@OnClick(R.id.blitz_fab, R.id.long_fab, R.id.normal_fab)
fun onSpeedClicked() {
hideSpeedMenu()
.doOnComplete { menuState = FabMenuState.SIZE }
.andThen(showSizeMenu())
.subscribe()
}

现在,我们终于就可以轻松的将动画结合在一起,同时设置好 menu 状态。要是用原来的方法可不会这么容易。

关于 menu 状态的处理,目前的实现不是很优雅,如果你有什么建议可以在评论里告诉我~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@OnClick(R.id.fab)
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

  • 一个新的基于响应式设计的动画框架 material-motion-android
  • VanGogh 一个类似的动画库
  • 也有人指出 withEndAction 只有在动画正常结束时才会被调用,有可能导致泄露
  • 接上条,有人要加 CompositeDisposable
  • 接上条,也许存在一个回调函数比 withEndAction 更合适