在Android动画中使用RxJava

在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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,800评论 25 707
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,471评论 6 30
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,731评论 22 665
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,104评论 5 13
  • http://mp.weixin.qq.com/s/018WnqMFfnIlwYX9UsdRaw
    张晏阅读 168评论 0 0