Android Compose 動畫使用詳解(八)Animatable的使用

語言: CN / TW / HK

theme: smartblue

本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

前面介紹了 Compose 的 animateXxxAsState動畫 Api 的使用,以及如何通過 animateValueAsState實現自定義 animateXxxAsState動畫 Api ,如何對動畫進行詳細配置從而達到靈活的實現各種動畫效果。

本篇將為大家介紹更底層的動畫 Api :Animatable

Animatable

在前面介紹 animateXxxAsState的時候我們跟蹤原始碼發現其內部呼叫的是 animateValueAsState,那麼 animateValueAsState 內部又是怎麼實現的呢?來看看 animateValueAsState 的原始碼:

```kotlin fun animateValueAsState( targetValue: T, typeConverter: TwoWayConverter, animationSpec: AnimationSpec = remember { spring(visibilityThreshold = visibilityThreshold) }, visibilityThreshold: T? = null, finishedListener: ((T) -> Unit)? = null ): State {

val animatable = remember { Animatable(targetValue, typeConverter) }
val listener by rememberUpdatedState(finishedListener)
val animSpec by rememberUpdatedState(animationSpec)
val channel = remember { Channel<T>(Channel.CONFLATED) }
SideEffect {
    channel.trySend(targetValue)
}
LaunchedEffect(channel) {
    for (target in channel) {
        val newTarget = channel.tryReceive().getOrNull() ?: target
        launch {
            if (newTarget != animatable.targetValue) {
                animatable.animateTo(newTarget, animSpec)
                listener?.invoke(animatable.value)
            }
        }
    }
}
return animatable.asState()

} ```

可以發現,animateValueAsState 的內部其實就是通過 Animatable 來實現的。實際上 animateValueAsState 是對 Animatable 的上層使用封裝,而 animateXxxAsState 又是對 animateValueAsState 的上層使用封裝,所以 Animatable 是更底層的動畫 api。

下面就來看一下如何使用 Animatable實現動畫效果。首先還是通過其構造方法定義瞭解建立 Animatable需要哪些引數以及各個引數的含義,構造方法定義如下:

kotlin class Animatable<T, V : AnimationVector>( initialValue: T, val typeConverter: TwoWayConverter<T, V>, private val visibilityThreshold: T? = null )

構造方法有三個引數,引數解析如下:

  • initialValue:動畫初始值,型別是泛型,即動畫作用的數值型別,如 Float、Dp 等
  • typeConverter:型別轉換器,型別是 TwoWayConverter,在 《自定義animateXxxAsState動畫》一文中我們對其進行了詳細介紹,作用是將動畫的數值型別與 AnimationVector進行互相轉換。
  • visibilityThreshold:可視閾值,即動畫數值達到設定的值時瞬間到達目標值停止動畫,可選引數,預設值為 null

瞭解了構造方法和引數,下面就來看一下怎麼建立一個 Animatable,假設我們要對一個 float 型別的資料做動畫,那麼 initialValue就應該傳入 float 的數值,那typeConverter傳啥呢?要去自定義實現 TwoWayConverter麼?大多數情況下並不用,因為 Compose 為我們提供了常用型別的轉換器,如下:

```kotlin // package : androidx.compose.animation.core.VectorConverters

// Float 型別轉換器 val Float.Companion.VectorConverter: TwoWayConverter

// Int 型別轉換器 val Int.Companion.VectorConverter: TwoWayConverter

// Rect 型別轉換器 val Rect.Companion.VectorConverter: TwoWayConverter

// Dp 型別轉換器 val Dp.Companion.VectorConverter: TwoWayConverter

// DpOffset 型別轉換器 val DpOffset.Companion.VectorConverter: TwoWayConverter

// Size 型別轉換器 val Size.Companion.VectorConverter: TwoWayConverter

// Offset 型別轉換器 val Offset.Companion.VectorConverter: TwoWayConverter

// IntOffset 型別轉換器 val IntOffset.Companion.VectorConverter: TwoWayConverter

// IntSize 型別轉換器 val IntSize.Companion.VectorConverter: TwoWayConverter

// package : androidx.compose.animation.ColorVectorConverter // Color 型別轉換器 val Color.Companion.VectorConverter: (colorSpace: ColorSpace) -> TwoWayConverter ```

注意: Color 的轉換器與其他轉換器不是在同一個包下。

我們要作用於 Float 型別時就可以直接使用 Float.VectorConverter即可,程式碼如下:

kotlin val animatable = remember { Animatable(100f, Float.VectorConverter) }

在 Compose 函式裡建立 Animatable 物件時必須使用 remember進行包裹,否則會報錯。

建立其他數值型別的動畫則傳對應的 VectorConverter即可,如 Dp、Size、Color,建立程式碼如下:

kotlin val animatable1 = remember { Animatable(100.dp, Dp.VectorConverter) } val animatable2 = remember { Animatable(Size(100f, 100f), Size.VectorConverter) } val animatable3 = remember { Animatable(Color.Blue, Color.VectorConverter(Color.Blue.colorSpace)) }

除此之外,Compose 還為 Float 和 Color 提供了簡便方法 Animatable,只需要傳入初始值即可,使用如下:

```kotlin // Float 簡便方法使用 import androidx.compose.animation.core.Animatable

val animatableFloat = remember { Animatable(100f) }

// Color 簡便方法使用 import androidx.compose.animation.Animatable

val animatable5 = remember { Animatable(Color.Blue) } ```

需要注意的是雖然都是叫 Animatable,但是引入的包是不一樣的,且這裡的 Animatable 不是建構函式而是一個方法,在方法的實現裡再呼叫的 Animatable 建構函式建立真正的 Animatable例項,原始碼分別如下:

Animatable(Float)

```kotlin package androidx.compose.animation.core

fun Animatable( initialValue: Float, visibilityThreshold: Float = Spring.DefaultDisplacementThreshold ) = Animatable( initialValue, Float.VectorConverter, visibilityThreshold ) ```

Animatable(Color) :

```kotlin package androidx.compose.animation

fun Animatable(initialValue: Color): Animatable = Animatable(initialValue, (Color.VectorConverter)(initialValue.colorSpace)) ```

Animatable建立好後下面看看怎麼觸發動畫執行。

animateTo

Animatable提供了一個 animateTo方法用於觸發動畫執行,看看這個方法的定義:

kotlin suspend fun animateTo( targetValue: T, animationSpec: AnimationSpec<T> = defaultSpringSpec, initialVelocity: T = velocity, block: (Animatable<T, V>.() -> Unit)? = null ): AnimationResult<T, V>

首先animateTo 方法是用 suspend修飾的,即只能在協程中呼叫;其次方法有四個引數,對應解析如下:

  • targetValue:動畫目標值
  • animationSpec:動畫規格配置,這個前面幾篇檔案進行了詳細介紹,共有 6 種動畫規格可進行設定
  • initialVelocity:初始速度
  • block:函式型別引數,動畫執行的每一幀都會回撥這個 block 方法,可用於動畫監聽

最後返回值為 AnimationResult型別,包含動畫結束時的狀態和原因。

執行動畫

我們還是以前面文章熟悉的方塊移動動畫來看一下 animateTo的使用效果,程式碼如下:

```kotlin // 建立狀態 通過狀態驅動動畫 var moveToRight by remember { mutableStateOf(false) } // 動畫例項 val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }

// animateTo 是 suspend 方法,所以需要在協程中進行呼叫 LaunchedEffect(moveToRight) { // 根據狀態確定動畫移動的目標值 animatable.animateTo(if (moveToRight) 200.dp else 10.dp) } Box( Modifier // 使用動畫值 .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { // 修改狀態 moveToRight = !moveToRight } ) ```

animateTo 需要在協程中進行呼叫,這裡使用的是 LaunchedEffect來開啟協程,他是 Compose 提供的專用協程開啟方法,其特點是不會在每次 UI 重組時都重新啟動協程,只會在 LaunchedEffect 引數發生變化時才會重新啟動協程執行協程中的程式碼。

因為本篇主要介紹 Compose 動畫的使用,關於 Compose 協程相關內容這裡就不做過多贅述,有興趣的同學可自行查閱相關資料。

看一下執行效果:

除了通過狀態觸發 animateTo 外,也可以直接在按鈕事件中觸發,程式碼如下:

```kotlin val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } // 獲取協程作用域 val scope = rememberCoroutineScope()

Box( Modifier .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { // 開啟協程 scope.launch { // 執行動畫 animatable.animateTo(200.dp) } } ) ```

因為 LaunchedEffect 只能在 Compose 函式中使用,而點選事件並不是 Compose 函式,所以這裡需要使用 rememberCoroutineScope()獲取協程作用域後再用其啟動協程。

效果如下:

動畫監聽

animateTo的最後一個引數是一個函式型別 (Animatable<T, V>.() -> Unit)?,可以用來對動畫進行監聽,在回撥方法裡可以通過 this 獲取到當前動畫 Animatable的例項,通過其可以獲取到動畫當前時刻的值、目標值、速度等。使用如下:

kotlin animatable.animateTo(200.dp){ // 動畫當前值 val value = this.value // 當前速度 val velocity = this.velocity // 動畫目標值 val targetValue = this.targetValue }

可以通過監聽動畫實現介面的聯動操作,比如讓另一個元件跟隨動畫元件一起運動等。

返回結果

animateTo方法是有返回結果的,型別為AnimationResult,通過返回結果可以獲取到動畫結束時的狀態和原因,AnimationResult 原始碼如下:

```kotlin class AnimationResult(

// 結束狀態
val endState: AnimationState<T, V>,

// 結束原因
val endReason: AnimationEndReason

) ```

只有兩個屬性 endStateendReason分別代表動畫結束時的狀態和原因

endStateAnimationState型別,通過其可以獲取到動畫結束時的值、速度、時間等資料,原始碼定義如下:

```kotlin class AnimationState( val typeConverter: TwoWayConverter, initialValue: T, initialVelocityVector: V? = null, lastFrameTimeNanos: Long = AnimationConstants.UnspecifiedTime, finishedTimeNanos: Long = AnimationConstants.UnspecifiedTime, isRunning: Boolean = false ) : State {

// 動畫值
override var value: T by mutableStateOf(initialValue)
    internal set

// 動畫速度向量
var velocityVector: V =
    initialVelocityVector?.copy() ?: typeConverter.createZeroVectorFrom(initialValue)
    internal set

// 最後一幀時間(納秒)
@get:Suppress("MethodNameUnits")
var lastFrameTimeNanos: Long = lastFrameTimeNanos
    internal set

// 結束時的時間(納秒)
@get:Suppress("MethodNameUnits")
var finishedTimeNanos: Long = finishedTimeNanos
    internal set

// 是否正在執行
var isRunning: Boolean = isRunning
    internal set

// 動畫速度
val velocity: T
    get() = typeConverter.convertFromVector(velocityVector)

} ```

注意這裡的 lastFrameTimeNanosfinishedTimeNanos是基於 System.nanoTime獲取到的納秒值,不是系統時間。

endReason是一個 AnimationEndReason型別的列舉,只有兩個列舉值:

```kotlin enum class AnimationEndReason {

// 動畫執行到邊界時停止結束
BoundReached,

// 動畫正常結束
Finished

} ```

Finished很好理解,就是動畫正常執行完成;那 BoundReached到達邊界停止是什麼意思呢?Animatable是可以為動畫設定邊界的,當動畫執行到邊界時就會立即停止,此時返回結果的停止原因就是 BoundReached,關於動畫的邊界設定以及動畫停止的更多內容會在後續文章中進行詳細介紹。

那麼返回值在哪些情況下會用到呢?比如一個動畫被打斷時另一個動畫需要依賴上一個動畫的值、速度等繼續執行,或者動畫遇到邊界停止時需要重新進行動畫此時就可以通過上一個動畫的返回值獲取到需要的資料後進行相關處理。

snapTo

除了 animateToAnimatable還提供了 snapTo執行動畫,看到 snapTo我們自然想到了前面介紹動畫配置時的快閃動畫 SnapSpec,即動畫時長為 0 瞬間執行完成,snapTo也是同樣的作用,可以讓動畫瞬間達到目標值,方法定義如下:

kotlin suspend fun snapTo(targetValue: T)

同樣是一個被 suspend修飾的掛起函式,即必須在協程裡執行;引數只有一個 targetValue即目標值,使用如下:

```kotlin val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } val scope = rememberCoroutineScope()

Box( Modifier .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { scope.launch { // 通過 snapTo 瞬間到達目標值位置 animatable.snapTo(200.dp) } } ) ```

效果如下:

通過 snapTo 我們可以實現先讓動畫瞬間達到某個值,再繼續執行後面的動畫,比如上面的動畫我們可以通過 snapTo讓方塊瞬間到 100.dp 位置然後使用 animateTo 動畫到 200.dp,程式碼如下:

kotlin scope.launch { // 先瞬間到達 100.dp animatable.snapTo(100.dp) // 再從 100.dp 動畫到 200.dp animatable.animateTo(200.dp, animationSpec = tween(1000)) }

動畫效果:

實戰

《Android Compose 動畫使用詳解(三)自定義animateXxxAsState動畫》一文中我們通過 animateValueAsState自定義 animateUploadAsState實現了上傳按鈕的動畫,現在我們看看如何通過 Animatable自定義實現同樣的動畫效果。

關於上傳按鈕動畫的實現原理可檢視 《Android Compose 動畫使用詳解(二)狀態改變動畫animateXxxAsState》一文的詳細介紹。

首先自定義 UploadData實體類:

kotlin data class UploadData( val backgroundColor: Color, val textAlpha: Float, val boxWidth: Dp, val progress: Int, val progressAlpha: Float )

然後自定義 animateUploadAsStateapi:

```kotlin @Composable fun animateUploadAsState( // 上傳按鈕動畫資料 value: UploadData, // 狀態 state: Any, ): UploadData {

// 建立對應值的 Animatable 例項
val bgColorAnimatable = remember {
    Animatable(
        value.backgroundColor,
        Color.VectorConverter(value.backgroundColor.colorSpace)
    )
}
val textAlphaAnimatable = remember { Animatable(value.textAlpha) }
val boxWidthAnimatable = remember { Animatable(value.boxWidth, Dp.VectorConverter) }
val progressAnimatable = remember { Animatable(value.progress, Int.VectorConverter) }
val progressAlphaAnimatable = remember { Animatable(value.progressAlpha) }

// 當狀態改變時在協程裡分別執行 animateTo
LaunchedEffect(state) {
    bgColorAnimatable.animateTo(value.backgroundColor)
}
LaunchedEffect(state) {
    textAlphaAnimatable.animateTo(value.textAlpha)
}
LaunchedEffect(state) {
    boxWidthAnimatable.animateTo(value.boxWidth)
}
LaunchedEffect(state) {
    progressAnimatable.animateTo(value.progress)
}
LaunchedEffect(state) {
    progressAlphaAnimatable.animateTo(value.progressAlpha)
}

// 返回最新資料
return UploadData(
    bgColorAnimatable.value,
    textAlphaAnimatable.value,
    boxWidthAnimatable.value,
    progressAnimatable.value,
    progressAlphaAnimatable.value
)

} ```

使用:

```kotlin val originWidth = 180.dp val circleSize = 48.dp // 上傳狀態 var uploadState by remember { mutableStateOf(UploadState.Normal) } // 按鈕文字 var text by remember { mutableStateOf("Upload") }

// 根據狀態建立目標動畫資料 val uploadValue = when (uploadState) { UploadState.Normal -> UploadData(Color.Blue, 1f, originWidth, 0, 0f) UploadState.Start -> UploadData(Color.Gray, 0f, circleSize, 0, 1f) UploadState.Uploading -> UploadData(Color.Gray, 0f, circleSize, 100, 1f) UploadState.Success -> UploadData(Color.Red, 1f, originWidth, 100, 0f) }

// 通過自定義api建立動畫 val upload = animateUploadAsState(uploadValue, uploadState)

Column { // 按鈕佈局 Box( modifier = Modifier .padding(start = 10.dp, top = 20.dp) .width(originWidth), contentAlignment = Alignment.Center ) { Box( modifier = Modifier .clip(RoundedCornerShape(circleSize / 2)) .background(upload.backgroundColor) .size(upload.boxWidth, circleSize), contentAlignment = Alignment.Center, ) { Box( modifier = Modifier.size(circleSize).clip(ArcShape(upload.progress)) .alpha(upload.progressAlpha).background(Color.Blue) ) Box( modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp)) .alpha(upload.progressAlpha).background(Color.White) ) Text(text, color = Color.White, modifier = Modifier.alpha(upload.textAlpha)) } }

// 輔助按鈕,用於模擬上傳狀態的改變
Button(onClick = {
    when (uploadState) {
        UploadState.Normal -> {
            uploadState = UploadState.Start
        }
        UploadState.Start -> {
            uploadState = UploadState.Uploading
        }
        UploadState.Uploading -> {
            uploadState = UploadState.Success
            text = "Success"
        }
        UploadState.Success -> {
            uploadState = UploadState.Normal
        }
    }
}, modifier = Modifier.padding(start = 10.dp, top = 20.dp)) {
    Text("改變上傳狀態")
}

} ```

執行效果如下:

最後

本篇介紹了更底層動畫 api Animatable的建立以及 animateTosnapTo 的使用,並通過一個簡單的實戰例項完成了如何通過 Animatable 實現自定義動畫完成與 animateXxxAsState 同樣的效果。除此之外 Animatable 還有 animateDecayapi 、邊界值的設定以及停止動畫等,由於篇幅問題我們將在後續文章中進行詳細介紹,請持續關注本專欄瞭解更多 Compose 動畫內容。