Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 在Android動畫中使用RxJava

在Android動畫中使用RxJava

編輯:關於Android編程

在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 {    
 // 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) = object : Subscriber() {         
          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.translateView(translationX: Float, 
                                  translationY: Float,
                                  duration: Long, 
                                  interpolator: Interpolator): Observable 
 = lift(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, 
                            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): Completable =
        Completable.create(ExpandViewsOnSubscribe(items, ExpandViewsOnSubscribe.AnimationType.EXPAND_HORIZONTALLY, 300L, AccelerateDecelerateInterpolator(), 32))

fun collapseMenuItemsHorizontally(items: MutableList): 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): Completable =
        Completable.create(ExpandViewsOnSubscribe(items, ExpandViewsOnSubscribe.AnimationType.EXPAND_HORIZONTALLY, 300L, AccelerateDecelerateInterpolator(), 32))
fun collapseMenuItemsHorizontally(items: MutableList): Completable =
        Completable.create(ExpandViewsOnSubscribe(items, ExpandViewsOnSubscribe.AnimationType.COLLAPSE_HORIZONTALLY, 300L, AccelerateDecelerateInterpolator(), 32))
fun rotateMenuItemsBy90(items: MutableList): Completable =
        Completable.create(RotateViewsOnSubscribe(items, RotateViewsOnSubscribe.AnimationType.ROTATE_TO_90, 300L, DecelerateInterpolator()));
fun rotateMenuItemsToOriginalPosition(items: MutableList): Completable =
        Completable.create(RotateViewsOnSubscribe(items, RotateViewsOnSubscribe.AnimationType.ROTATE_TO_0, 300L, DecelerateInterpolator()))
fun scaleDownMenuItems(items: MutableList): Completable =
        Completable.create(ScaleViewsOnSubscribe(items, ScaleViewsOnSubscribe.AnimationType.SCALE_DOWN, 400L, DecelerateInterpolator()))
fun scaleUpMenuItems(items: MutableList): Completable =
        Completable.create(ScaleViewsOnSubscribe(items, ScaleViewsOnSubscribe.AnimationType.SCALE_UP, 400L, DecelerateInterpolator()))
fun removeMenuItems(items: MutableList): Completable =
 Completable.fromAction {
    removeAllViews()
}
fun addItemsScaledDownAndRotated(items: MutableList): 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) {
    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就實現了旋轉和縮放動畫。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved