Android Compose衰減動(dòng)畫(huà)Animatable使用詳解
前言
之前介紹了 Animatable 動(dòng)畫(huà)以及其 animateTo和 snapTo兩個(gè)開(kāi)啟動(dòng)畫(huà) api 的使用,實(shí)際上 Animatable 除了這兩個(gè) api 以外還有一個(gè) animateDecay即本篇要介紹的衰減動(dòng)畫(huà)。
什么是衰減動(dòng)畫(huà)呢?就是動(dòng)畫(huà)速度由快到慢最后停止,最常見(jiàn)的應(yīng)用場(chǎng)景就是慣性動(dòng)畫(huà),比如滑動(dòng)列表時(shí)手指松開(kāi)后列表不會(huì)立即停止而是會(huì)繼續(xù)滑動(dòng)一段距離后才停止;下面就來(lái)看看 animateDecay具體如何使用。
animateDecay
首先還是來(lái)看一下 animateDecay的定義:
suspend fun animateDecay(
initialVelocity: T,
animationSpec: DecayAnimationSpec<T>,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>
跟前面介紹的 animateTo和 snapTo一樣都是 suspend修飾的方法,即必須在協(xié)程中調(diào)用,參數(shù)有三個(gè),分別解析如下:
- initialVelocity:初始速度
- animationSpec:動(dòng)畫(huà)配置,
DecayAnimationSpec類型 - block:函數(shù)類型參數(shù),動(dòng)畫(huà)運(yùn)行的每一幀都會(huì)回調(diào)這個(gè) block 方法,可用于動(dòng)畫(huà)監(jiān)聽(tīng)
返回值跟 animateTo一樣都是 AnimationResult類型。
initialVelocity是動(dòng)畫(huà)的初始速度,動(dòng)畫(huà)會(huì)從這個(gè)初始速度按照一定的衰減曲線進(jìn)行衰減,直到速度為 0 或達(dá)到閾值時(shí)動(dòng)畫(huà)停止。那這個(gè)初始速度的單位是多少呢?是單位/秒 這里的單位就是動(dòng)畫(huà)作用的數(shù)值類型,比如數(shù)值類型是 Dp,那就代表多少 Dp 每秒。
而衰減曲線的配置就是第二個(gè)參數(shù) animationSpec,需要注意的是這里的 animationSpec 是 DecayAnimationSpec類型,它并不是前面介紹的 AnimationSpec的子類,是衰減動(dòng)畫(huà)特有的動(dòng)畫(huà)配置,看一下 DecayAnimationSpec 的定義:
interface DecayAnimationSpec<T> {
fun <V : AnimationVector> vectorize(
typeConverter: TwoWayConverter<T, V>
): VectorizedDecayAnimationSpec<V>
}
從源碼可以知曉,DecayAnimationSpec是一個(gè)獨(dú)立的接口,跟蹤其實(shí)現(xiàn)類只有一個(gè) DecayAnimationSpecImpl:
private class DecayAnimationSpecImpl<T>(
private val floatDecaySpec: FloatDecayAnimationSpec
) : DecayAnimationSpec<T> {
override fun <V : AnimationVector> vectorize(
typeConverter: TwoWayConverter<T, V>
): VectorizedDecayAnimationSpec<V> = VectorizedFloatDecaySpec(floatDecaySpec)
}
這個(gè)實(shí)現(xiàn)類是 private的,也就是不能直接創(chuàng)建其實(shí)例,那怎么創(chuàng)建呢?Compose 提供三個(gè)方法用于創(chuàng)建,分別是 splineBasedDecay、rememberSplineBasedDecay和 exponentialDecay,那么這三種方法又有什么區(qū)別呢?下面分別對(duì)其進(jìn)行詳細(xì)介紹。
splineBasedDecay
splineBasedDecay根據(jù)方法命名我們可以翻譯為基于樣條曲線的衰減,什么是樣條曲線呢?Google得到的答案:樣條曲線是經(jīng)過(guò)或接近影響曲線形狀的一系列點(diǎn)的平滑曲線。更抽象了,實(shí)際上我們并不需要了解他是怎么實(shí)現(xiàn)的,當(dāng)然感興趣的可以自行查詢相關(guān)資料,我們只要知道在 Android 中默認(rèn)的列表慣性滑動(dòng)就是基于此曲線算法實(shí)現(xiàn)的。
概念了解清楚后,再來(lái)看一下 splineBasedDecay 方法的定義:
fun <T> splineBasedDecay(density: Density): DecayAnimationSpec<T>
只有一個(gè)參數(shù) density即屏幕像素密度。為什么要傳 density 呢?這是因?yàn)?splineBasedDecay 是基于屏幕像素進(jìn)行的動(dòng)畫(huà)速度衰減,當(dāng)像素密度越大動(dòng)畫(huà)減速越快,動(dòng)畫(huà)的時(shí)長(zhǎng)越短,動(dòng)畫(huà)慣性滑動(dòng)的距離越短;可以理解屏幕像素密度越大摩擦力越大,所以慣性滑動(dòng)的距離就越短。
使用 splineBasedDecay 實(shí)現(xiàn)動(dòng)畫(huà)效果,代碼如下:
// 創(chuàng)建 Animatable 實(shí)例
val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }
val scope = rememberCoroutineScope()
// 創(chuàng)建 splineBasedDecay
// 通過(guò) LocalDensity.current 獲取當(dāng)前設(shè)備屏幕密度
val splineBasedDecay = splineBasedDecay<Dp>(LocalDensity.current)
Box(
Modifier
.padding(start = 10.dp, top = animatable.value)
.size(100.dp, 100.dp)
.background(Color.Blue)
.clickable {
scope.launch {
// 啟動(dòng)衰減動(dòng)畫(huà),初始速度設(shè)置為 1000.dp 每秒
animatable.animateDecay(1000.dp, splineBasedDecay)
}
}
)
將上述代碼分別在屏幕尺寸均為 6.0 英寸、屏幕密度分別為 440 dpi 和 320 dpi 的設(shè)備上運(yùn)行,效果如下:

可以發(fā)現(xiàn),屏幕密度小的動(dòng)畫(huà)運(yùn)行的距離更長(zhǎng)。
rememberSplineBasedDecay
rememberSplineBasedDecay 跟 splineBasedDecay 的作用是一樣的,區(qū)別在 splineBasedDecay 上用 remember包裹了一層,上一節(jié)中使用 splineBasedDecay 并未用 remember包裹,就意味著每次界面刷新時(shí)都會(huì)重新調(diào)用 splineBasedDecay 創(chuàng)建衰減配置的實(shí)例。而使用 rememberSplineBasedDecay就可以優(yōu)化該問(wèn)題,且無(wú)需手動(dòng)傳入 density參數(shù)。
看一下 rememberSplineBasedDecay源碼:
@Composable
actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> {
val density = LocalDensity.current
return remember(density.density) {
SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()
}
}
首先也是通過(guò) LocalDensity.current獲取屏幕像素密度,然后使用 remember創(chuàng)建衰減配置實(shí)例,remember參數(shù)傳入了 density,也就是當(dāng)特殊情況屏幕密度發(fā)生變化時(shí)會(huì)重新創(chuàng)建衰減配置實(shí)例。
在開(kāi)發(fā)中遇到要使用 splineBasedDecay的時(shí)候一般直接使用 rememberSplineBasedDecay 即可。
思考:前面介紹 splineBasedDecay 是跟屏幕像素密度有關(guān)的,如果需求就是不想因?yàn)槠聊幌袼孛芏榷鴮?dǎo)致不同設(shè)備表現(xiàn)不一樣怎么辦呢?或者動(dòng)畫(huà)作用的數(shù)值就是跟屏幕像素密度沒(méi)關(guān),比如作用于旋轉(zhuǎn)角度的動(dòng)畫(huà),此時(shí)怎么辦呢?這個(gè)時(shí)候就不能使用 splineBasedDecay,而是應(yīng)該使用 exponentialDecay。
exponentialDecay
exponentialDecay是指數(shù)衰減,即動(dòng)畫(huà)速度按指數(shù)遞減,他不依賴屏幕像素密度,可用于通用數(shù)據(jù)的衰減動(dòng)畫(huà)。其定義如下:
fun <T> exponentialDecay(
frictionMultiplier: Float = 1f,
absVelocityThreshold: Float = 0.1f
): DecayAnimationSpec<T>
有兩個(gè)參數(shù),且都有默認(rèn)值,參數(shù)解析如下:
- frictionMultiplier:摩擦系數(shù),摩擦系數(shù)越大,速度減速越快,反之則減速越慢
- absVelocityThreshold:絕對(duì)速度閾值,當(dāng)速度絕對(duì)值低于此值時(shí)動(dòng)畫(huà)停止,這里的數(shù)值是指多少單位的速度,比如動(dòng)畫(huà)數(shù)值類型為 Dp,這里傳 100f 即 100f * 1.dp
使用如下:
var move by remember { mutableStateOf(false) }
val animatable = remember { Animatable(30.dp, Dp.VectorConverter) }
val scope = rememberCoroutineScope()
Box(
Modifier
.padding(start = 30.dp, top = animatable.value)
.size(100.dp, 100.dp)
.background(Color.Blue)
.clickable {
scope.launch {
// 使用 exponentialDecay 衰減動(dòng)畫(huà)
animatable.animateDecay(1000.dp, exponentialDecay())
}
}
)
運(yùn)行效果:

將摩擦系數(shù)設(shè)置為 5f 體驗(yàn)一下增加摩擦系數(shù)后的效果:
exponentialDecay(5f)

摩擦系數(shù)增大后,動(dòng)畫(huà)運(yùn)行的距離和時(shí)間都明顯縮短了。
將絕對(duì)速度閾值設(shè)置為 500f 再看一下效果:
exponentialDecay(absVelocityThreshold = 500f)

當(dāng)動(dòng)畫(huà)速度達(dá)到閾值速度后動(dòng)畫(huà)就停止了,所以閾值越大動(dòng)畫(huà)越早停止。
實(shí)戰(zhàn)
下面我們用衰減動(dòng)畫(huà)實(shí)現(xiàn)一個(gè)轉(zhuǎn)盤(pán)抽獎(jiǎng)的動(dòng)畫(huà)效果,即當(dāng)點(diǎn)擊抽獎(jiǎng)后轉(zhuǎn)盤(pán)開(kāi)始轉(zhuǎn)動(dòng)然后緩緩?fù)O?,最后指針指向的位置就是中?jiǎng)的獎(jiǎng)品。
因?yàn)槭切D(zhuǎn)動(dòng)畫(huà),所以這里我們使用 exponentialDecay指數(shù)衰減動(dòng)畫(huà),同時(shí)準(zhǔn)備兩張圖片素材,如下:


將兩張圖片居中疊加,然后通過(guò)動(dòng)畫(huà)旋轉(zhuǎn)下面的圓盤(pán)就完成了整個(gè)動(dòng)畫(huà)效果,代碼如下:
// 創(chuàng)建動(dòng)畫(huà)實(shí)例
val animatable = remember { Animatable(0, Int.VectorConverter) }
// 獲取協(xié)程作用域用戶在按鈕點(diǎn)擊事件中開(kāi)啟協(xié)程
val scope = rememberCoroutineScope()
// 中獎(jiǎng)結(jié)果
var luckyResult by remember { mutableStateOf("") }
// 中獎(jiǎng)項(xiàng)
val luckyItem = remember { arrayOf("50元紅包", "20元紅包","10元紅包","100-50券","小米藍(lán)牙耳機(jī)","謝謝參與") }
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Box{
// 底部圓盤(pán)圖片
Image(
painter = painterResource(R.drawable.bg),
contentDescription = "bg",
// 旋轉(zhuǎn)角度設(shè)置為動(dòng)畫(huà)的值
modifier = Modifier.rotate(animatable.value.toFloat())
)
// 中間指針圖片
Image(
painter = painterResource(R.drawable.center),
contentDescription = "center",
// 設(shè)置點(diǎn)擊事件
modifier = Modifier.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
// 開(kāi)啟協(xié)程
scope.launch {
// 更新抽獎(jiǎng)狀態(tài)
luckyResult = "抽獎(jiǎng)中"
// 開(kāi)啟動(dòng)畫(huà)
// 初始速度設(shè)置為 10000 再加上 1000~10000 的隨機(jī)數(shù)
// 衰減曲線設(shè)置為 exponentialDecay 摩擦系數(shù)設(shè)置為 0.5f
val result = animatable.animateDecay(10000 + Random.nextInt(1000,10000), exponentialDecay(frictionMultiplier = 0.5f))
// 動(dòng)畫(huà)執(zhí)行完后從動(dòng)畫(huà)結(jié)果中獲取最后的值,即旋轉(zhuǎn)角度
val angle = result.endState.value
// 通過(guò)計(jì)算獲取當(dāng)前指針在哪個(gè)范圍
val index = angle % 360 / 60
// 獲取中獎(jiǎng)結(jié)果,并顯示在屏幕上
luckyResult = luckyItem[index]
}
})
)
}
// 顯示中獎(jiǎng)結(jié)果
Text(luckyResult, modifier = Modifier.padding(10.dp))
// 添加重置按鈕
Button(onClick = {
scope.launch {
// 通過(guò) snapTo 瞬間回到初始狀態(tài)
animatable.snapTo(0)
}
}){
Text("重置")
}
}
最終效果:

最后
本篇繼 Animatable 的 animateTo和 snapTo后繼續(xù)介紹了 animateDecay 衰減動(dòng)畫(huà)的使用,包括如何設(shè)置衰減曲線,不同衰減曲線的參數(shù)配置以及使用場(chǎng)景,并通過(guò)衰減動(dòng)畫(huà)實(shí)現(xiàn)了抽獎(jiǎng)轉(zhuǎn)盤(pán)效果。下一篇我們繼續(xù)探索 Animatable 的邊界設(shè)置及其相關(guān)的應(yīng)用,請(qǐng)持續(xù)關(guān)注本專欄了解更多 Compose 動(dòng)畫(huà)內(nèi)容。
以上就是Android Compose衰減動(dòng)畫(huà)Animatable使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Android Compose衰減動(dòng)畫(huà)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)動(dòng)態(tài)曲線繪制
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)動(dòng)態(tài)曲線繪制,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
ExpandListView實(shí)現(xiàn)下拉列表案例
這篇文章主要為大家詳細(xì)介紹了ExpandListView實(shí)現(xiàn)下拉列表案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08
Android?webView加載數(shù)據(jù)時(shí)內(nèi)存溢出問(wèn)題及解決
這篇文章主要介紹了Android?webView加載數(shù)據(jù)時(shí)內(nèi)存溢出問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
Android?ViewPager?+?Fragment實(shí)現(xiàn)滑動(dòng)頁(yè)面效果
本文通過(guò)實(shí)例代碼較詳細(xì)的給大家介紹了Android?ViewPager?+?Fragment實(shí)現(xiàn)滑動(dòng)頁(yè)面效果,需要的朋友可以參考下2018-06-06
Android App開(kāi)發(fā)中創(chuàng)建Fragment組件的教程
這篇文章主要介紹了Android App開(kāi)發(fā)中創(chuàng)建Fragment的教程,Fragment是用以更靈活地構(gòu)建多屏幕界面的可UI組件,需要的朋友可以參考下2016-05-05
Flutter StaggeredGridView實(shí)現(xiàn)瀑布流效果
這篇文章主要為大家詳細(xì)介紹了Flutter StaggeredGridView實(shí)現(xiàn)瀑布流效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Android 簡(jiǎn)單的照相機(jī)程序的實(shí)例代碼
終于經(jīng)過(guò)多次找錯(cuò),修改把一個(gè)簡(jiǎn)單的照相機(jī)程序完成了,照相類代碼如下:2013-05-05
Android編程之DatePicker和TimePicke簡(jiǎn)單時(shí)間監(jiān)聽(tīng)用法分析
這篇文章主要介紹了Android編程之DatePicker和TimePicke簡(jiǎn)單時(shí)間監(jiān)聽(tīng)用法,結(jié)合具體實(shí)例形式分析了時(shí)間控件DatePicker和TimePicke布局與具體功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-02-02
Android引導(dǎo)頁(yè)面的簡(jiǎn)單實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了Android引導(dǎo)頁(yè)面的簡(jiǎn)單實(shí)現(xiàn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02

