Android Compose 動畫使用詳解(八)Animatable的使用
theme: smartblue
本文為稀土掘金技術社區首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!
前言
前面介紹了 Compose 的 animateXxxAsState
動畫 Api 的使用,以及如何通過 animateValueAsState
實現自定義 animateXxxAsState
動畫 Api ,如何對動畫進行詳細配置從而達到靈活的實現各種動畫效果。
本篇將為大家介紹更底層的動畫 Api :Animatable
Animatable
在前面介紹 animateXxxAsState
的時候我們跟蹤源碼發現其內部調用的是 animateValueAsState
,那麼 animateValueAsState
內部又是怎麼實現的呢?來看看 animateValueAsState
的源碼:
```kotlin
fun
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
創建好後下面看看怎麼觸發動畫執行。
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
) ```
只有兩個屬性 endState
和 endReason
分別代表動畫結束時的狀態和原因
endState
是 AnimationState
類型,通過其可以獲取到動畫結束時的值、速度、時間等數據,源碼定義如下:
```kotlin
class AnimationState
// 動畫值
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)
} ```
注意這裏的 lastFrameTimeNanos
和 finishedTimeNanos
是基於 System.nanoTime
獲取到的納秒值,不是系統時間。
endReason
是一個 AnimationEndReason
類型的枚舉,只有兩個枚舉值:
```kotlin enum class AnimationEndReason {
// 動畫運行到邊界時停止結束
BoundReached,
// 動畫正常結束
Finished
} ```
Finished
很好理解,就是動畫正常執行完成;那 BoundReached
到達邊界停止是什麼意思呢?Animatable
是可以為動畫設置邊界的,當動畫運行到邊界時就會立即停止,此時返回結果的停止原因就是 BoundReached
,關於動畫的邊界設置以及動畫停止的更多內容會在後續文章中進行詳細介紹。
那麼返回值在哪些情況下會用到呢?比如一個動畫被打斷時另一個動畫需要依賴上一個動畫的值、速度等繼續執行,或者動畫遇到邊界停止時需要重新進行動畫此時就可以通過上一個動畫的返回值獲取到需要的數據後進行相關處理。
snapTo
除了 animateTo
,Animatable
還提供了 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
)
然後自定義 animateUploadAsState
api:
```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
的創建以及 animateTo
和 snapTo
的使用,並通過一個簡單的實戰實例完成了如何通過 Animatable
實現自定義動畫完成與 animateXxxAsState
同樣的效果。除此之外 Animatable
還有 animateDecay
api 、邊界值的設置以及停止動畫等,由於篇幅問題我們將在後續文章中進行詳細介紹,請持續關注本專欄瞭解更多 Compose 動畫內容。
- Android Compose 動畫使用詳解(九)Animatable之衰減動畫
- Android Compose 動畫使用詳解(八)Animatable的使用
- Android Compose 動畫使用詳解(六)動畫配置之SpringSpec
- Android Compose 動畫使用詳解(二)狀態改變動畫animateXxxAsState
- Android基於DataBinding Koin實現MVVM模式頁面快速開發框架
- Android基於DataBinding封裝RecyclerView實現快速列表開發
- Flutter實現文件上傳華為對象存儲(OBS)
- Flutter遊戲引擎Flame初探,帶你實現一個簡單小遊戲
- Flutter使用Canvas實現小白兔的繪製
- Flutter使用Canvas實現微信紅包領取效果
- Flutter使用Canvas實現精美錶盤效果
- Flutter之事件節流、防抖封裝
- Flutter之GetX狀態管理——Obx的使用及原理詳解
- Flutter快速開發——列表分頁加載封裝
- Flutter應用框架搭建(四) 網絡請求封裝
- Flutter應用框架搭建(一)GetX集成及使用詳解