在android中实现动画是非常容易的,ViewPropertyAnimator提供了开箱即用的解决方案能够非常容易的创建属性动画。将它与RxJava结合起来你将得到可以链接不同动画,产生不同随机行为等功能的强大工具。
开始之前需要注意:这篇博客的目的是向你展示在android中怎样把RxJava与动画结合起来在不用写太多嵌套代码的情况下创建一个良好的用户界面。为了掌握这篇博客对于RxJava基础知识的了解是必要的。即使你对RxJava不是太了解读完这篇博客你也应该能够了解到RxJava的强大与灵活性。怎样有效的使用他。本篇文章的所有代码均使用kotlin语言编写所以为了能够有效的运行例子代码,你需要在android studio上安装kotlin插件。本文的源码地址:https://github.com/chenyi2013/RxJavaAndAnimation
属性动画的基础
整篇文章,我们将使用通过调用ViewCompat.animate(targetView)函数来得到ViewPropertyAnimatorCompat。这个类能够自动优化在视图上选择的属性动画。它的语法方便为视图动画提供了极大的灵活性。
让我们来看看怎样使用他来为一个简单的视图添加动画。我们缩小一个按钮(通过缩放他到0)当动画结束的时候将它从父布局中移除。
ViewCompat.animate(someButton)
.scaleX(0f)// Scale to 0 horizontally
.scaleY(0f)// Scale to 0 vertically
.setDuration(300)// Duration of the animation in milliseconds.
.withEndAction{removeView(view)}// Called when the animation ends successfully.
这很方便和简单,但是在更加复杂的场景下事情可能会变得非常混乱,尤其是在withEndAction{}中使用嵌套回调的时候(当然你也可以使用 setListener() 来为每一个动画场景提供回调例如开始动画、取消动画)
添加RxJava
使用RxJava,我们将这个嵌套的listener转换为发送给observers的事件。因此对于每一个view我们都能够进行动画,例如调用onNext(view) 让他按顺序对view进行处理。
一种选择是通过创建简单的自定义操作符来为我们处理各种动画,例如创建水平或垂直方向上的平移动画。
接下来的例子在实际开发中可能不会用到,但是他将展示RxJava动画的强大威力,
如下图所示,在界面的左右两端分别放置一个正方形,初始的时候在左方的正方形中有一组圆,当点击下方的 “Animation"按钮的时候我们想让在左方的正方形中的圆依次移动到右方的正方形中,当再一次按下这个按钮的时候,动画应该逆转,圆应该从右边移动到左边。这些圆应该在相同的时间间隔内按顺序依次进行移动。
让我们来创建一个operator, 他将接收一个view然后执行它的动画并将这个view传递到subscriber的onNext方法,在这种情况下,RxJava会按顺序一个一个的执行每一个view的动画,如果前一个view的动画没有执行完,RxJava会处于等待状态直到之前的view的动画被传递完成才会执行当前view的动画。当然你也可以自定义操作符让当前的view不必等待上一个view的动画播放完成就立即执行动画播放。
TranslateViewOperator.kt
import android.support.v4.view.ViewCompat
import android.view.View
import android.view.animation.Interpolator
import rx.Observableimport rx.Subscriber
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicIntegerclass
TranslateViewOperator(private val translationX: Float,
private val translationY: Float,
private val duration: Long,
private val interpolator: Interpolator) : Observable.Operator<View, View> {
// Counts the number of animations in progress.
// Used for properly propagating onComplete() call to the subscriber.
private val numberOfRunningAnimations = AtomicInteger(0)
// Indicates whether this operator received the onComplete() call or not.
private val isOnCompleteCalled = AtomicBoolean(false)
override fun call(subscriber: Subscriber<in View>) = object : Subscriber<View>() {
override fun onError(e: Throwable?) {
// In case of onError(), just pass it down to the subscriber.
if (!subscriber.isUnsubscribed) {
subscriber.onError(e)
}
}
override fun onNext(view: View) {
// Don't start animation if the subscriber has unsubscribed.
if (subscriber.isUnsubscribed) return
// Run the animation.
numberOfRunningAnimations.incrementAndGet()
ViewCompat.animate(view)
.translationX(translationX)
.translationY(translationY)
.setDuration(duration)
.setInterpolator(interpolator)
.withEndAction {
numberOfRunningAnimations.decrementAndGet()
// Once the animation is done, check if the subscriber is still subscribed
// and pass the animated view to onNext().
if (!subscriber.isUnsubscribed) {
subscriber.onNext(view)
// If we received the onComplete() event sometime while the animation was running,
// wait until all animations are done and then call onComplete() on the subscriber.
if (numberOfRunningAnimations.get() == 0 && isOnCompleteCalled.get()) {
subscriber.onCompleted()
}
}
}
}
override fun onCompleted() {
isOnCompleteCalled.set(true)
// Call onComplete() immediately if all animations are finished.
if (!subscriber.isUnsubscribed && numberOfRunningAnimations.get() == 0) {
subscriber.onCompleted()
}
}
}
}
现在在ViewGroup中放置了一些圆(CircleView)和正方形(RectangleView),我们能够非常容易的创建一个方法来平移这些view。
AnimationViewGroup.kt
fun Observable<View>.translateView(translationX: Float,
translationY: Float,
duration: Long,
interpolator: Interpolator): Observable<View>
= lift<View>(TranslateViewOperator(translationX, translationY, duration, interpolator))
我们将圆用list保存,声明两个变量分别用于保存左边的正方形和右边的正方形。
AnimationViewGroup.kt
fun init() {
rectangleLeft = RectangleView(context)
rectangleRight = RectangleView(context)
addView(rectangleLeft)
addView(rectangleRight)
// Add 10 circles.
for (i in 0..9) {
val cv = CircleView(context);
circleViews.add(cv)
addView(cv)
}
}
//onLayout and other code omitted..
让我们来编写一个播放动画的方法,通过timer Observable发射Observable每隔一段时间我们能够得到圆views。
AnimationViewGroup.kt
fun startAnimation() { // First, unsubscribe from previous animations.
animationSubscription?.unsubscribe()
// Timer observable that will emit every half second. val
timerObservable = Observable.interval(0, 500, TimeUnit.MILLISECONDS)
// Observable that will emit circle views from the list.
val viewsObservable = Observable.from(circleViews) // As each circle view is emitted, stop animations on it.
.doOnNext { v -> ViewCompat.animate(v).cancel() } // Just take those circles that are not already in the right rectangle.
.filter { v -> v.translationX < rectangleRight!!.left } // First, zip the timer and circle views observables, so that we get one circle view every half a second.
animationSubscription = Observable.zip(viewsObservable, timerObservable) { view, time -> view } // As each view comes in, translate it so that it ends up inside the right rectangle.
.translateView(rectangleRight!!.left.toFloat(), rectangleRight!!.top.toFloat(), ANIMATION_DURATION_MS, DecelerateInterpolator())
.subscribe()}
你可以进行无限可能的扩展,例如,通过移除timer你能够同时移动所有view,当动画完成的时候你也可以处理下游的每一个view。
自定义操作符是件非常酷的事情,实现也很简单,但是创建自定义操作符并不总是一件好的事情他能导致挫折和问题例如不当的背压处理。
在实际的开发中,大多数时候我们需要一种稍微不同的方式来处理动画,通常我们需要组合不同的动画,先播放什么动画,然后播放什么的动画,最后播放什么动画。
初识Completable
Completable是在RxJava1.1.1版本引入的,那么到底什么是Completable。
以下是来自RxJava wiki上的解释:
我们可以认为Completable对象是Observable的简化版本,他仅仅发射onError和onCompleted这两个终端事件。他看上去像Observable.empty()的一个具体类但是又不像empty(),Completable是一个活动的类。
我们可以使用Completable来执行一个动画,当这个动画执行完成的时候调用onComplete(),同时,另外的动画和任意的其它操作也都可以被执行。
现在让我们使用Completable来替代操作符,我们将使用一个简化版的Obserable以便当动画完成的时候我们不必不断的处理这些view,仅仅只需要通知这些observers被请求的动画已经完成了。
让我们来创建另外一个实用性更强的例子,我们有一个填充了一些图标的toolbar,我们想要提供一个setMenuItems()方法来折叠所有的图标到toolbar的左边,缩放他们直到他们消失,从父布局中移除他们。增加一组新的icons添加到父布局,然后放大他们,最后展开他们。
我们将从Completable.CompletableOnSubscribe的实现类来创建Completable。
ExpandViewsOnSubscribe.kt
class ExpandViewsOnSubscribe(private val views:List<FloatingActionButton>,
private val animationType:AnimationType,
private val duration: Long,
private val interpolator: Interpolator,
private val paddingPx:Int): Completable.CompletableOnSubscribe {
lateinit private var numberOfAnimationsToRun: AtomicInteger enum class AnimationType {
EXPAND_HORIZONTALLY, COLLAPSE_HORIZONTALLY,
EXPAND_VERTICALLY, COLLAPSE_VERTICALLY
}
override fun call(subscriber: Completable.CompletableSubscriber?) {
if (views.isEmpty()) {
subscriber!!.onCompleted()
return
// We need to run as much as animations as there are views.
}
numberOfAnimationsToRun = AtomicInteger(views.size)
// Assert all FABs are the same size, we could count each item size if we're making
// an implementation that possibly expects different-sized items.
val fabWidth = views[0].width
val fabHeight = views[0].height
val horizontalExpansion = animationType == AnimationType.EXPAND_HORIZONTALLY
val verticalExpansion = animationType == AnimationType.EXPAND_VERTICALLY
// Only if expanding horizontally, we'll move x-translate each of the FABs by index * width.
val xTranslationFactor = if (horizontalExpansion) fabWidth else 0
// Only if expanding vertically, we'll move y-translate each of the FABs by index * height.
val yTranslationFactor = if (verticalExpansion) fabHeight else 0
// Same with padding.
val paddingX = if (horizontalExpansion) paddingPx else 0
val paddingY = if (verticalExpansion) paddingPx else 0
for (i in views.indices) {
views[i].setImageResource(R.drawable.right_arrow)
ViewCompat.animate(views[i])
.translationX(i * (xTranslationFactor.toFloat() + paddingX))
.translationY(i * (yTranslationFactor.toFloat() + paddingY))
.setDuration(duration)
.setInterpolator(interpolator)
.withEndAction {
// Once all animations are done, call onCompleted().
if (numberOfAnimationsToRun.decrementAndGet() == 0) {
subscriber!!.onCompleted()
}
}
}
}
}
现在我们创建一个方法从Completable.CompletableOnSubscribe的实现类中返回Completable
AnimationViewGroup2.kt
fun expandMenuItemsHorizontally(items: MutableList<FloatingActionButton>): Completable =
Completable.create(ExpandViewsOnSubscribe(items, ExpandViewsOnSubscribe.AnimationType.EXPAND_HORIZONTALLY, 300L, AccelerateDecelerateInterpolator(), 32))
fun collapseMenuItemsHorizontally(items: MutableList<FloatingActionButton>): Completable =
Completable.create(ExpandViewsOnSubscribe(items, ExpandViewsOnSubscribe.AnimationType.COLLAPSE_HORIZONTALLY, 300L, AccelerateDecelerateInterpolator(), 32))
初始的时候我们添加了一些items到这个布局中现在我们可以添加如下的代码对他们进行测试。
AnimationViewGroup2.kt
fun startAnimation() {
expandMenuItemsHorizontally(currentItems).subscribe()}
fun reverseAnimation() {
collapseMenuItemsHorizontally(currentItems).subscribe()}
运行效果如下:
动画链
使用相同的方式,通过实现Completable.CompletableOnSubscribe我们可以实现缩放和旋转。以下是简化过的代码详细实现请查看源码:
AnimationViewGroup2.kt
fun expandMenuItemsHorizontally(items: MutableList<FloatingActionButton>): Completable =
Completable.create(ExpandViewsOnSubscribe(items, ExpandViewsOnSubscribe.AnimationType.EXPAND_HORIZONTALLY, 300L, AccelerateDecelerateInterpolator(), 32))
fun collapseMenuItemsHorizontally(items: MutableList<FloatingActionButton>): Completable =
Completable.create(ExpandViewsOnSubscribe(items, ExpandViewsOnSubscribe.AnimationType.COLLAPSE_HORIZONTALLY, 300L, AccelerateDecelerateInterpolator(), 32))
fun rotateMenuItemsBy90(items: MutableList<FloatingActionButton>): Completable =
Completable.create(RotateViewsOnSubscribe(items, RotateViewsOnSubscribe.AnimationType.ROTATE_TO_90, 300L, DecelerateInterpolator()));
fun rotateMenuItemsToOriginalPosition(items: MutableList<FloatingActionButton>): Completable =
Completable.create(RotateViewsOnSubscribe(items, RotateViewsOnSubscribe.AnimationType.ROTATE_TO_0, 300L, DecelerateInterpolator()))
fun scaleDownMenuItems(items: MutableList<FloatingActionButton>): Completable =
Completable.create(ScaleViewsOnSubscribe(items, ScaleViewsOnSubscribe.AnimationType.SCALE_DOWN, 400L, DecelerateInterpolator()))
fun scaleUpMenuItems(items: MutableList<FloatingActionButton>): Completable =
Completable.create(ScaleViewsOnSubscribe(items, ScaleViewsOnSubscribe.AnimationType.SCALE_UP, 400L, DecelerateInterpolator()))
fun removeMenuItems(items: MutableList<FloatingActionButton>): Completable =
Completable.fromAction {
removeAllViews()
}
fun addItemsScaledDownAndRotated(items: MutableList<FloatingActionButton>): Completable =
Completable.fromAction {
this.currentItems = items
for (item in items) {
item.scaleX = 0.0f
item.scaleY = 0.0f
item.rotation = 90f
item.setImageResource(R.drawable.square_72px)
addView(item)
}
}
实现setMenuItems方法
fun setMenuItems(newItems: MutableList<FloatingActionButton>) {
collapseMenuItemsHorizontally(currentItems)
.andThen(rotateMenuItemsBy90(currentItems))
.andThen(scaleDownMenuItems(currentItems))
.andThen(removeMenuItems(currentItems))
.andThen(addItemsScaledDownAndRotated(newItems))
.andThen(scaleUpMenuItems(newItems))
.andThen(rotateMenuItemsToOriginalPosition(newItems))
.andThen(expandMenuItemsHorizontally(newItems))
.subscribe()}
设置新的菜单项后的运行效果:
局限性
记住不能使用mergeWith()来执行这些组合的动画,因为这些动画作用在同一个view上,前一个动画的监听器会被后一个动画的监听器覆盖因此merge操作将永远不会完成因为它在等待这些动画的Completable
s执行完成。如果你执行的动画是作用在不同的view上你可以正常的使用mergeWith()方法,被创建的Completable将会一直等待直到这些动画调用onComplete()完成。
如果想在一个view上执行多个动画,一种解决方案是实现OnSubscribe例如RotateAndScaleViewOnSubscribe就实现了旋转和缩放动画。
本文写到这儿就告一段落了,由于水平有限写得不好的地方欢迎大家指出。本文主要内容均参考于:https://pspdfkit.com/blog/2016/android-animations-powered-by-rx-java/