Android?Compose之Animatable動畫停止使用詳解
前言
前面講了Animatable
的基礎(chǔ)以及衰減動畫的用法,本篇則主要講解 Animatable
動畫的停止,動畫的停止情況主要分為四種,如下:
- 動畫正常運行完成后停止
- 動畫被打斷停止
- 主動停止動畫
- 動畫觸達邊界停止
第一種很好理解,就是動畫按照我們設(shè)計的參數(shù)正常運行完成的情況,這種情況屬于正常停止,此時動畫返回結(jié)果的 endReason
為 AnimationEndReason.Finished
(在《Android Compose 動畫Animatable的使用》一文中有詳細介紹),本文主要介紹剩下三種停止情況。
打斷停止
顧名思義即動畫在運行過程中被打斷,那么是被什么打斷呢?被同一個 Animatable
的另一個動畫打斷,簡單的說就是當 Animatable
在執(zhí)行某一個動畫的過程中,此時再使用同一個 Animatable
去開啟另一個動畫,此時就會打斷正在運行中的動畫,即停止正在運行中的話執(zhí)行新的動畫。
那么為什么要打斷正在運行中的動畫呢?不能兩個動畫一起執(zhí)行嗎?假設(shè)一個動畫是將方塊移動到 200dp 的位置,另一個動畫是將方塊移動到 0dp 的位置,如果不會被打斷兩個動畫可以同時執(zhí)行,那就會出現(xiàn)方塊一會兒往 200dp 位置一會兒往 0dp 移動的閃爍情況,顯然動畫效果不符合預期,所以被設(shè)計成了后一個動畫會打斷前一個動畫。
下面就用一個實例演示動畫的打斷,代碼如下:
// 方塊顏色,默認為藍色 var backgroundColor by remember { mutableStateOf(Color.Blue) } // 動畫實例 val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } // 獲取協(xié)程作用域 val scope = rememberCoroutineScope() Box { // 動畫方塊 Box( Modifier // 使用動畫值 .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(backgroundColor) .clickable { // 點擊事件 // 啟動動畫 scope.launch { animatable.animateTo(200.dp, // 為了方便看到效果,動畫時間設(shè)置為 1000ms animationSpec = tween(durationMillis = 1000) ) } } ) // 按鈕,用于開啟新動畫 Button(onClick = { // 修改方塊顏色,方便觀察區(qū)分兩個動畫 backgroundColor = Color.Cyan // 啟動新動畫 scope.launch { animatable.animateTo(50.dp, animationSpec = tween(durationMillis = 1000)) } }, Modifier.padding(top = 170.dp, start = 70.dp)) { Text(text = "Next", style = TextStyle(fontSize = 10.sp)) } }
界面上添加了一個方塊和一個按鈕,點擊方塊執(zhí)行動畫移動到 200dp 位置,點擊按鈕執(zhí)行動畫移動到 50dp 位置,為了區(qū)分兩個動畫動畫執(zhí)行時為方塊設(shè)置了不同的顏色,運行效果如下:
從上面的效果可以看出,對同一個 Animatable
開啟新的動畫確實會打斷正在運行的動畫。
除了上面演示的 animateTo
可以打斷動畫以外,Animatable
的snapTo
、animateDecay
同樣可以打斷動畫。
主動停止
前面介紹的是新動畫打斷正在運行的動畫,那么如果我們想主動停止一個Animatable
動畫該怎么辦呢?很簡單,Animatable
提供了stop
方法用于停止動畫。
示例代碼如下:
var backgroundColor by remember { mutableStateOf(Color.Blue) } // 動畫實例 val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } val scope = rememberCoroutineScope() Box { // 動畫方塊 Box( Modifier // 使用動畫值 .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(backgroundColor) .clickable { // 點擊事件,開啟動畫 scope.launch { animatable.animateTo(200.dp, // 為了方便觀察動畫效果,動畫時長設(shè)置為 1000ms animationSpec = tween(durationMillis = 1000) ) } } ) // 停止按鈕 Button(onClick = { // 修改方塊顏色 backgroundColor = Color.Cyan // 停止動畫 scope.launch { animatable.stop() } }, Modifier.padding(top = 170.dp, start = 70.dp)) { Text(text = "Stop", style = TextStyle(fontSize = 10.sp)) } }
需要注意的是 stop
方法也是一個掛起函數(shù),需要在協(xié)程中執(zhí)行,效果如下:
觸達邊界停止
Animatable
可以通過 updateBounds
函數(shù)為動畫設(shè)置邊界值,當動畫運動到邊界時會立即停止動畫,updateBounds
定義如下:
fun updateBounds(lowerBound: T? = this.lowerBound, upperBound: T? = this.upperBound)
updateBounds
方法有兩個參數(shù) lowerBound
、upperBound
分別為動畫的邊界下限值和上限值,默認為 null
即不做限制,可以單獨設(shè)置上限和下限的值,當設(shè)置對應值后,動畫運行過程中動畫值達到邊界值時就會立即停止動畫。
示例代碼如下:
// 創(chuàng)建狀態(tài) 通過狀態(tài)驅(qū)動動畫 var moveToRight by remember { mutableStateOf(false) } // 動畫實例 val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } // 設(shè)置動畫邊界 animatable.updateBounds(lowerBound = 10.dp, upperBound = 200.dp) val scope = rememberCoroutineScope() Box( Modifier // 使用動畫值 .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { // 點擊事件 // 修改狀態(tài) moveToRight = !moveToRight scope.launch { // 執(zhí)行動畫 animatable.animateTo( // 根據(jù)狀態(tài)設(shè)置動畫的目標值,分別是向右到 400dp 和 向左到 -100dp 位置 if (moveToRight) 400.dp else -100.dp, animationSpec = tween(durationMillis = 1000) ) } } )
上面代碼分別設(shè)置了下限值為 10dp、上限值為 200dp,同時動畫目標值分別設(shè)置為 -100dp 和 400dp,看一下運行效果:
可以看出來,雖然動畫目標設(shè)置分別設(shè)置了 -100dp 和 400dp,但是因為我們設(shè)置了邊界值為 10dp 和 200dp,所以動畫向右運動時到達邊界值即 200dp 位置時就停止了,向左同樣的到達邊界值 10dp 也停止了動畫,這就是動畫邊界的作用。
多維邊界
之前介紹了 Compose 動畫是可以作用于多維數(shù)值的,比如作用于 Size、Offset、React 等數(shù)據(jù)時就是多維的動畫,此時對動畫設(shè)置邊界后,動畫目標值只要有其中一維的數(shù)值達到邊界就會立即停止,并不會等到所有維的數(shù)值都達到邊界才會停止。
下面用一個示例來舉例說明,還是上面的方塊動畫,上面只進行了橫向的動畫,如果我們要同時進行橫向和豎向的動畫,可以使用 Offset 來進行動畫,然后對其進行邊界設(shè)置來觀察效果,代碼如下:
// 創(chuàng)建狀態(tài) 通過狀態(tài)驅(qū)動動畫 var moveToRight by remember { mutableStateOf(false) } // 動畫實例 val animatable = remember { Animatable(Offset(10f, 30f), Offset.VectorConverter) } // 設(shè)置邊界值,下限:Offset(10f, 30f) 上限:Offset(400f, 200f) animatable.updateBounds(lowerBound = Offset(10f, 30f), upperBound = Offset(400f, 200f)) val scope = rememberCoroutineScope() Box { Box( Modifier // 使用動畫值 .padding(start = animatable.value.x.dp, top = animatable.value.y.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { // 修改狀態(tài) moveToRight = !moveToRight scope.launch { animatable.animateTo( // 根據(jù)狀態(tài)設(shè)置動畫的目標值 // 分別為向右和向下的 Offset(400f,400f) // 向上和向左的 Offset(-100f,0f) if (moveToRight) Offset(400f,400f) else Offset(-100f,0f), animationSpec = tween(durationMillis = 1000) ) } } ) }
運行效果:
可以發(fā)現(xiàn),上限設(shè)置為 Offset(400f, 200f)
即 x 軸最大為 400dp、y 軸最大為 200dp,動畫目標值為Offset(400f,400f)
,當方塊移動到 y 坐標為 200dp 時 y 坐標的值達到邊界值,動畫就停止了,此時 x 坐標值并未達到邊界值。同樣的往回執(zhí)行時 x 坐標先觸發(fā)達到邊界值 10dp 時停止了動畫。
如果就是想讓動畫都達到邊界才停止,此時不應該采用多維動畫的方式,而是應該使用多個單維動畫,對其分別設(shè)置邊界即可。
動畫停止監(jiān)聽
動畫停止可分為異常停止和正常停止,其中打斷和主動停止動畫屬于異常停止,動畫運行完成或達到邊界后停止屬于正常停止。
異常停止
一個動畫打斷另一個動畫,或調(diào)用 stop
主動停止動畫都屬于異常停止,此時動畫會拋出 CancellationException
的異常,在代碼中可通過捕獲該異常來監(jiān)聽動畫的異常停止,代碼如下:
scope.launch { try { // 異常停止時動畫會拋出異常,不會正常返回動畫結(jié)果 val animationResult = animatable.animateTo(200.dp, animationSpec = tween(durationMillis = 1000) ) // 動畫異常停止時會拋出異常,下面的代碼不會被執(zhí)行 // do something } catch (e: CancellationException) { Log.e("ANIMATOIN", "動畫異常停止") } }
正常停止
動畫觸達邊界停止屬于正常停止,此時動畫會正常返回結(jié)果,從結(jié)果中的 endReason
可判斷動畫是否為觸達邊界停止,代碼如下:
scope.launch { val animationResult = animatable.animateTo(200.dp, animationSpec = tween(durationMillis = 1000) ) // 判斷動畫是否觸達邊界停止 if(animationResult.endReason == AnimationEndReason.BoundReached){ // do something } }
同時動畫結(jié)果還能拿到動畫停止時的速度等數(shù)據(jù),這樣就能通過監(jiān)聽動畫邊界停止進行自定義的處理,比如結(jié)合上一篇介紹的衰減動畫讓動畫到達邊界后反向運動,代碼如下:
@Preview @Composable fun AnimationBound3() { // 動畫實例 val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } // 設(shè)置邊界值 animatable.updateBounds(upperBound = 200.dp, lowerBound = 10.dp) val scope = rememberCoroutineScope() val splineBasedDecay = rememberSplineBasedDecay<Dp>() Box( Modifier // 使用動畫值 .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { scope.launch { // 啟動動畫,設(shè)置初始速度為 3000dp val animationResult = animatable.animateDecay(3000.dp, splineBasedDecay) // 判斷是否為到達邊界停止 if(animationResult.endReason == AnimationEndReason.BoundReached){ // 執(zhí)行反向動畫,初始速度取動畫結(jié)束時速度的負數(shù) reverseAnimation(animatable, -animationResult.endState.velocity, splineBasedDecay) } } } ) } /// 反向執(zhí)行動畫 private suspend fun reverseAnimation( animatable: Animatable<Dp, AnimationVector1D>, initialVelocity: Dp, splineBasedDecay: DecayAnimationSpec<Dp> ) { val result = animatable.animateDecay(initialVelocity, splineBasedDecay) // 判斷是邊界停止時遞歸執(zhí)行反向動畫直到動畫非邊界停止 if(result.endReason == AnimationEndReason.BoundReached){ reverseAnimation(animatable, -result.endState.velocity, splineBasedDecay) } }
運行效果如下:
這樣就通過監(jiān)聽動畫的停止實現(xiàn)了動畫到達邊界后反向運動的效果。
最后
本篇介紹了 Animatable
動畫的停止,包括打斷停止、主動停止和到達邊界停止,介紹了不同停止的實現(xiàn)方式以及對動畫停止的監(jiān)聽處理。下一篇我們繼續(xù)探索 Compose 動畫的其他使用,請持續(xù)關(guān)注本專欄了解更多 Compose 動畫內(nèi)容。
相關(guān)文章
Android Studio 視頻播放失敗 start called in state1 異常怎么解決
很多朋友問小編在使用MediaPlayer播放音頻時報出 E/MediaPlayerNative: start called in state 1, mPlayer(0x0)問題,該如何處理呢,今天小編給大家?guī)砹薃ndroid Studio 視頻播放失敗 start called in state1 異常問題,需要的朋友可以參考下2020-03-03Android開發(fā)之DatePickerDialog、TimePickerDialog時間日期對話框用法示例
這篇文章主要介紹了Android開發(fā)之DatePickerDialog、TimePickerDialog時間日期對話框用法,結(jié)合實例形式分析了Android使用DatePickerDialog、TimePickerDialog顯示日期時間相關(guān)操作技巧,需要的朋友可以參考下2019-03-03android原生實現(xiàn)多線程斷點續(xù)傳功能
這篇文章主要為大家詳細介紹了android原生實現(xiàn)多線程斷點續(xù)傳功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-07-07