Android Compose衰減動畫Animatable使用詳解
前言
之前介紹了 Animatable
動畫以及其 animateTo
和 snapTo
兩個開啟動畫 api 的使用,實際上 Animatable
除了這兩個 api 以外還有一個 animateDecay
即本篇要介紹的衰減動畫。
什么是衰減動畫呢?就是動畫速度由快到慢最后停止,最常見的應(yīng)用場景就是慣性動畫,比如滑動列表時手指松開后列表不會立即停止而是會繼續(xù)滑動一段距離后才停止;下面就來看看 animateDecay
具體如何使用。
animateDecay
首先還是來看一下 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ù)有三個,分別解析如下:
- initialVelocity:初始速度
- animationSpec:動畫配置,
DecayAnimationSpec
類型 - block:函數(shù)類型參數(shù),動畫運行的每一幀都會回調(diào)這個 block 方法,可用于動畫監(jiān)聽
返回值跟 animateTo
一樣都是 AnimationResult
類型。
initialVelocity
是動畫的初始速度,動畫會從這個初始速度按照一定的衰減曲線進行衰減,直到速度為 0 或達(dá)到閾值時動畫停止。那這個初始速度的單位是多少呢?是單位/秒 這里的單位就是動畫作用的數(shù)值類型,比如數(shù)值類型是 Dp,那就代表多少 Dp 每秒。
而衰減曲線的配置就是第二個參數(shù) animationSpec
,需要注意的是這里的 animationSpec
是 DecayAnimationSpec
類型,它并不是前面介紹的 AnimationSpec
的子類,是衰減動畫特有的動畫配置,看一下 DecayAnimationSpec
的定義:
interface DecayAnimationSpec<T> { fun <V : AnimationVector> vectorize( typeConverter: TwoWayConverter<T, V> ): VectorizedDecayAnimationSpec<V> }
從源碼可以知曉,DecayAnimationSpec
是一個獨立的接口,跟蹤其實現(xiàn)類只有一個 DecayAnimationSpecImpl
:
private class DecayAnimationSpecImpl<T>( private val floatDecaySpec: FloatDecayAnimationSpec ) : DecayAnimationSpec<T> { override fun <V : AnimationVector> vectorize( typeConverter: TwoWayConverter<T, V> ): VectorizedDecayAnimationSpec<V> = VectorizedFloatDecaySpec(floatDecaySpec) }
這個實現(xiàn)類是 private
的,也就是不能直接創(chuàng)建其實例,那怎么創(chuàng)建呢?Compose 提供三個方法用于創(chuàng)建,分別是 splineBasedDecay
、rememberSplineBasedDecay
和 exponentialDecay
,那么這三種方法又有什么區(qū)別呢?下面分別對其進行詳細(xì)介紹。
splineBasedDecay
splineBasedDecay
根據(jù)方法命名我們可以翻譯為基于樣條曲線的衰減,什么是樣條曲線呢?Google得到的答案:樣條曲線是經(jīng)過或接近影響曲線形狀的一系列點的平滑曲線。更抽象了,實際上我們并不需要了解他是怎么實現(xiàn)的,當(dāng)然感興趣的可以自行查詢相關(guān)資料,我們只要知道在 Android 中默認(rèn)的列表慣性滑動就是基于此曲線算法實現(xiàn)的。
概念了解清楚后,再來看一下 splineBasedDecay
方法的定義:
fun <T> splineBasedDecay(density: Density): DecayAnimationSpec<T>
只有一個參數(shù) density
即屏幕像素密度。為什么要傳 density
呢?這是因為 splineBasedDecay
是基于屏幕像素進行的動畫速度衰減,當(dāng)像素密度越大動畫減速越快,動畫的時長越短,動畫慣性滑動的距離越短;可以理解屏幕像素密度越大摩擦力越大,所以慣性滑動的距離就越短。
使用 splineBasedDecay
實現(xiàn)動畫效果,代碼如下:
// 創(chuàng)建 Animatable 實例 val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } val scope = rememberCoroutineScope() // 創(chuàng)建 splineBasedDecay // 通過 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 { // 啟動衰減動畫,初始速度設(shè)置為 1000.dp 每秒 animatable.animateDecay(1000.dp, splineBasedDecay) } } )
將上述代碼分別在屏幕尺寸均為 6.0 英寸、屏幕密度分別為 440 dpi 和 320 dpi 的設(shè)備上運行,效果如下:
可以發(fā)現(xiàn),屏幕密度小的動畫運行的距離更長。
rememberSplineBasedDecay
rememberSplineBasedDecay
跟 splineBasedDecay
的作用是一樣的,區(qū)別在 splineBasedDecay
上用 remember
包裹了一層,上一節(jié)中使用 splineBasedDecay
并未用 remember
包裹,就意味著每次界面刷新時都會重新調(diào)用 splineBasedDecay
創(chuàng)建衰減配置的實例。而使用 rememberSplineBasedDecay
就可以優(yōu)化該問題,且無需手動傳入 density
參數(shù)。
看一下 rememberSplineBasedDecay
源碼:
@Composable actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> { val density = LocalDensity.current return remember(density.density) { SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec() } }
首先也是通過 LocalDensity.current
獲取屏幕像素密度,然后使用 remember
創(chuàng)建衰減配置實例,remember
參數(shù)傳入了 density
,也就是當(dāng)特殊情況屏幕密度發(fā)生變化時會重新創(chuàng)建衰減配置實例。
在開發(fā)中遇到要使用 splineBasedDecay
的時候一般直接使用 rememberSplineBasedDecay
即可。
思考:前面介紹 splineBasedDecay
是跟屏幕像素密度有關(guān)的,如果需求就是不想因為屏幕像素密度而導(dǎo)致不同設(shè)備表現(xiàn)不一樣怎么辦呢?或者動畫作用的數(shù)值就是跟屏幕像素密度沒關(guān),比如作用于旋轉(zhuǎn)角度的動畫,此時怎么辦呢?這個時候就不能使用 splineBasedDecay
,而是應(yīng)該使用 exponentialDecay
。
exponentialDecay
exponentialDecay
是指數(shù)衰減,即動畫速度按指數(shù)遞減,他不依賴屏幕像素密度,可用于通用數(shù)據(jù)的衰減動畫。其定義如下:
fun <T> exponentialDecay( frictionMultiplier: Float = 1f, absVelocityThreshold: Float = 0.1f ): DecayAnimationSpec<T>
有兩個參數(shù),且都有默認(rèn)值,參數(shù)解析如下:
- frictionMultiplier:摩擦系數(shù),摩擦系數(shù)越大,速度減速越快,反之則減速越慢
- absVelocityThreshold:絕對速度閾值,當(dāng)速度絕對值低于此值時動畫停止,這里的數(shù)值是指多少單位的速度,比如動畫數(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 衰減動畫 animatable.animateDecay(1000.dp, exponentialDecay()) } } )
運行效果:
將摩擦系數(shù)設(shè)置為 5f 體驗一下增加摩擦系數(shù)后的效果:
exponentialDecay(5f)
摩擦系數(shù)增大后,動畫運行的距離和時間都明顯縮短了。
將絕對速度閾值設(shè)置為 500f 再看一下效果:
exponentialDecay(absVelocityThreshold = 500f)
當(dāng)動畫速度達(dá)到閾值速度后動畫就停止了,所以閾值越大動畫越早停止。
實戰(zhàn)
下面我們用衰減動畫實現(xiàn)一個轉(zhuǎn)盤抽獎的動畫效果,即當(dāng)點擊抽獎后轉(zhuǎn)盤開始轉(zhuǎn)動然后緩緩?fù)O?,最后指針指向的位置就是中獎的獎品?/p>
因為是旋轉(zhuǎn)動畫,所以這里我們使用 exponentialDecay
指數(shù)衰減動畫,同時準(zhǔn)備兩張圖片素材,如下:
將兩張圖片居中疊加,然后通過動畫旋轉(zhuǎn)下面的圓盤就完成了整個動畫效果,代碼如下:
// 創(chuàng)建動畫實例 val animatable = remember { Animatable(0, Int.VectorConverter) } // 獲取協(xié)程作用域用戶在按鈕點擊事件中開啟協(xié)程 val scope = rememberCoroutineScope() // 中獎結(jié)果 var luckyResult by remember { mutableStateOf("") } // 中獎項 val luckyItem = remember { arrayOf("50元紅包", "20元紅包","10元紅包","100-50券","小米藍(lán)牙耳機","謝謝參與") } Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { Box{ // 底部圓盤圖片 Image( painter = painterResource(R.drawable.bg), contentDescription = "bg", // 旋轉(zhuǎn)角度設(shè)置為動畫的值 modifier = Modifier.rotate(animatable.value.toFloat()) ) // 中間指針圖片 Image( painter = painterResource(R.drawable.center), contentDescription = "center", // 設(shè)置點擊事件 modifier = Modifier.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = { // 開啟協(xié)程 scope.launch { // 更新抽獎狀態(tài) luckyResult = "抽獎中" // 開啟動畫 // 初始速度設(shè)置為 10000 再加上 1000~10000 的隨機數(shù) // 衰減曲線設(shè)置為 exponentialDecay 摩擦系數(shù)設(shè)置為 0.5f val result = animatable.animateDecay(10000 + Random.nextInt(1000,10000), exponentialDecay(frictionMultiplier = 0.5f)) // 動畫執(zhí)行完后從動畫結(jié)果中獲取最后的值,即旋轉(zhuǎn)角度 val angle = result.endState.value // 通過計算獲取當(dāng)前指針在哪個范圍 val index = angle % 360 / 60 // 獲取中獎結(jié)果,并顯示在屏幕上 luckyResult = luckyItem[index] } }) ) } // 顯示中獎結(jié)果 Text(luckyResult, modifier = Modifier.padding(10.dp)) // 添加重置按鈕 Button(onClick = { scope.launch { // 通過 snapTo 瞬間回到初始狀態(tài) animatable.snapTo(0) } }){ Text("重置") } }
最終效果:
最后
本篇繼 Animatable
的 animateTo
和 snapTo
后繼續(xù)介紹了 animateDecay
衰減動畫的使用,包括如何設(shè)置衰減曲線,不同衰減曲線的參數(shù)配置以及使用場景,并通過衰減動畫實現(xiàn)了抽獎轉(zhuǎn)盤效果。下一篇我們繼續(xù)探索 Animatable
的邊界設(shè)置及其相關(guān)的應(yīng)用,請持續(xù)關(guān)注本專欄了解更多 Compose 動畫內(nèi)容。
以上就是Android Compose衰減動畫Animatable使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Android Compose衰減動畫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android?webView加載數(shù)據(jù)時內(nèi)存溢出問題及解決
這篇文章主要介紹了Android?webView加載數(shù)據(jù)時內(nèi)存溢出問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12Android?ViewPager?+?Fragment實現(xiàn)滑動頁面效果
本文通過實例代碼較詳細(xì)的給大家介紹了Android?ViewPager?+?Fragment實現(xiàn)滑動頁面效果,需要的朋友可以參考下2018-06-06Android App開發(fā)中創(chuàng)建Fragment組件的教程
這篇文章主要介紹了Android App開發(fā)中創(chuàng)建Fragment的教程,Fragment是用以更靈活地構(gòu)建多屏幕界面的可UI組件,需要的朋友可以參考下2016-05-05Flutter StaggeredGridView實現(xiàn)瀑布流效果
這篇文章主要為大家詳細(xì)介紹了Flutter StaggeredGridView實現(xiàn)瀑布流效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03Android編程之DatePicker和TimePicke簡單時間監(jiān)聽用法分析
這篇文章主要介紹了Android編程之DatePicker和TimePicke簡單時間監(jiān)聽用法,結(jié)合具體實例形式分析了時間控件DatePicker和TimePicke布局與具體功能實現(xiàn)技巧,需要的朋友可以參考下2017-02-02