Compose?動(dòng)畫藝術(shù)之屬性動(dòng)畫探索
前言
本篇文章是此專欄的第三篇文章,如果想閱讀前兩篇文章的話請(qǐng)點(diǎn)擊下方鏈接:
Compose的屬性動(dòng)畫
屬性動(dòng)畫是通過不斷地修改值來實(shí)現(xiàn)的,而初始值和結(jié)束值之間的過渡動(dòng)畫就需要來計(jì)算了。在 Compose
中為我們提供了一整套 api 來實(shí)現(xiàn)屬性動(dòng)畫,具體有哪些呢?讓我們一起來看下吧!
官方為我們提供了上圖這十種方法,我們可以根據(jù)實(shí)際項(xiàng)目中的需求進(jìn)行挑選使用。
在第一篇文章中也提到了 Compose
的屬性動(dòng)畫,但只是簡(jiǎn)單使用了下,告訴大家 Compose
有這個(gè)東西,今天咱們來具體看下!
先來看下 animateColorAsState
的代碼吧:
@Composable fun animateColorAsState( ? ?targetValue: Color, ? ?animationSpec: AnimationSpec<Color> = colorDefaultSpring, ? ?label: String = "ColorAnimation", ? ?finishedListener: ((Color) -> Unit)? = null ): State<Color> { ? ?val converter = remember(targetValue.colorSpace) { ? ? ? (Color.VectorConverter)(targetValue.colorSpace) ? } ? ?return animateValueAsState( ? ? ? ?targetValue, converter, animationSpec, label = label, finishedListener = finishedListener ? ) }
可以看到一共接收四個(gè)參數(shù),來分別看下代表什么吧:
- targetValue:顧名思義,目標(biāo)值,這里對(duì)應(yīng)的就是想要轉(zhuǎn)換成的顏色
- animationSpec:動(dòng)畫規(guī)格,動(dòng)畫隨著時(shí)間改變值的一種規(guī)格吧,上一篇文章中也提到了,但由于上一篇文章主要內(nèi)容并不是這個(gè),也就沒有講,本篇文章會(huì)詳細(xì)說明
- label:標(biāo)簽,以區(qū)別于其他動(dòng)畫
- finishedListener:在動(dòng)畫完成時(shí)會(huì)進(jìn)行回調(diào)
參數(shù)并不算多,而且有三個(gè)是可選參數(shù),也就只有 targetValue
必須要進(jìn)行設(shè)置。方法體內(nèi)只通過 Color.colorSpace
強(qiáng)轉(zhuǎn)構(gòu)建了一個(gè) TwoWayConverter
。
前面說過,大多數(shù) Compose
動(dòng)畫 API 支持將 Float
、Color
、Dp
以及其他基本數(shù)據(jù)類型作為 開箱即用的動(dòng)畫值,但有時(shí)我們需要為其他數(shù)據(jù)類型(比如自定義類型)添加動(dòng)畫效果。在動(dòng)畫播放期間,任何動(dòng)畫值都表示為 AnimationVector
。使用相應(yīng)的 TwoWayConverter
即可將值轉(zhuǎn)換為 AnimationVector
,反之亦然,這樣一來,核心動(dòng)畫系統(tǒng)就可以統(tǒng)一對(duì)其進(jìn)行處理了。由于顏色有 argb,所以構(gòu)建的時(shí)候使用的是 AnimationVector4D
,來看下吧:
val Color.Companion.VectorConverter: ? (colorSpace: ColorSpace) -> TwoWayConverter<Color, AnimationVector4D> ? ? ? ?get() = ColorToVector
如果按照我之前的習(xí)慣肯定要接著看 animateValueAsState
方法內(nèi)部的代碼了,但今天等會(huì)再看!再來看看 animateDpAsState
的代碼吧!
@Composable fun animateDpAsState( ? ?targetValue: Dp, ? ?animationSpec: AnimationSpec<Dp> = dpDefaultSpring, ? ?label: String = "DpAnimation", ? ?finishedListener: ((Dp) -> Unit)? = null ): State<Dp> { ? ?return animateValueAsState( ? ? ? ?targetValue, ? ? ? ?Dp.VectorConverter, ? ? ? ?animationSpec, ? ? ? ?label = label, ? ? ? ?finishedListener = finishedListener ? ) }
發(fā)現(xiàn)了點(diǎn)什么沒有,參數(shù)基本一摸一樣,別著急,咱們?cè)倏纯磩e的!
@Composable fun animateIntAsState( ? ?targetValue: Int, ? ?animationSpec: AnimationSpec<Int> = intDefaultSpring, ? ?label: String = "IntAnimation", ? ?finishedListener: ((Int) -> Unit)? = null ) ? @Composable fun animateSizeAsState( ? ?targetValue: Size, ? ?animationSpec: AnimationSpec<Size> = sizeDefaultSpring, ? ?label: String = "SizeAnimation", ? ?finishedListener: ((Size) -> Unit)? = null ) ? @Composable fun animateRectAsState( ? ?targetValue: Rect, ? ?animationSpec: AnimationSpec<Rect> = rectDefaultSpring, ? ?label: String = "RectAnimation", ? ?finishedListener: ((Rect) -> Unit)? = null )
不能說是大同小異,只能說是一摸一樣!既然一摸一樣的話咱們就以文章開頭的 animateColorAsState
來看吧!
上面的說法其實(shí)是不對(duì)的,并不是有十種,而是九種,因?yàn)榫欧N都調(diào)用了 animateValueAsState
,其實(shí)也可以說有無數(shù)種,因?yàn)榭梢宰远x。。。。
參數(shù)
下面先來看下 animateValueAsState
的方法體吧:
@Composable fun <T, V : AnimationVector> animateValueAsState( ? ?targetValue: T, ? ?typeConverter: TwoWayConverter<T, V>, ? ?animationSpec: AnimationSpec<T> = remember { spring() }, ? ?visibilityThreshold: T? = null, ? ?label: String = "ValueAnimation", ? ?finishedListener: ((T) -> Unit)? = null ): State<T>
來看看接收的參數(shù)吧,可以發(fā)現(xiàn)有兩個(gè)參數(shù)沒有見過:
- typeConverter:類型轉(zhuǎn)換器,將需要的類型轉(zhuǎn)換為
AnimationVector
- visibilityThreshold:一個(gè)可選的閾值,用于定義何時(shí)動(dòng)畫值可以被認(rèn)為足夠接近targetValue以結(jié)束動(dòng)畫
OK,剩下的參數(shù)在上面都介紹過,就不重復(fù)進(jìn)行介紹了。
方法體
由于 animateValueAsState
方法有點(diǎn)長(zhǎng),所以分開來看吧,接下來看下 animateValueAsState
方法中的前半部分:
val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) } val listener by rememberUpdatedState(finishedListener) val animSpec: AnimationSpec<T> by rememberUpdatedState( ? ?animationSpec.run { ? ? ? ?if (visibilityThreshold != null && this is SpringSpec && ? ? ? ? ? ?this.visibilityThreshold != visibilityThreshold ? ? ? ) { ? ? ? ? ? ?spring(dampingRatio, stiffness, visibilityThreshold) ? ? ? } else { ? ? ? ? ? ?this ? ? ? } ? } ) 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) ? ? ? ? ? } ? ? ? } ? } }
可以看到首先構(gòu)建了一個(gè) Animatable
,然后記錄了完成回調(diào),又記錄了 AnimationSpec
,之后有個(gè)判斷,如果 visibilityThreshold
不為空并且 AnimationSpec
為 SpringSpec
的時(shí)候?yàn)樾聵?gòu)建的一個(gè) AnimationSpec
,反之則還是傳進(jìn)來的 AnimationSpec
。
那 Animatable
是個(gè)啥呢?它是一個(gè)值容器,它可以在通過 animateTo
更改值時(shí)為值添加動(dòng)畫效果,它可確保一致的連續(xù)性和互斥性,這意味著值變化始終是連續(xù)的,并且會(huì)取消任何正在播放的動(dòng)畫。Animatable
的許多功能(包括 animateTo
)以掛起函數(shù)的形式提供,所以需要封裝在適當(dāng)?shù)膮f(xié)程作用域內(nèi),所以下面使用了 LaunchedEffect
來包裹執(zhí)行 animateTo
方法,最后調(diào)用了動(dòng)畫完成的回調(diào)。
由于 Animatable
類中代碼比較多,先來看下類的初始化及構(gòu)造方法吧!
class Animatable<T, V : AnimationVector>( ? ?initialValue: T, ? ?val typeConverter: TwoWayConverter<T, V>, ? ?private val visibilityThreshold: T? = null, ? ?val label: String = "Animatable" )
可以看到這里使用到的參數(shù)在 animateValueAsState
中都有,就不一一介紹了,挑著重點(diǎn)來,來看看上面使用到的 animateTo
吧:
suspend fun animateTo( ? ?targetValue: T, ? ?animationSpec: AnimationSpec<T> = defaultSpringSpec, ? ?initialVelocity: T = velocity, ? ?block: (Animatable<T, V>.() -> Unit)? = null ): AnimationResult<T, V> { ? ?val anim = TargetBasedAnimation( ? ? ? ?animationSpec = animationSpec, ? ? ? ?initialValue = value, ? ? ? ?targetValue = targetValue, ? ? ? ?typeConverter = typeConverter, ? ? ? ?initialVelocity = initialVelocity ? ) ? ?return runAnimation(anim, initialVelocity, block) }
可以看到 animateTo
使用傳進(jìn)來的參數(shù)構(gòu)建了一個(gè) TargetBasedAnimation
,這是一個(gè)方便的動(dòng)畫包裝類,適用于所有基于目標(biāo)的動(dòng)畫,即具有預(yù)定義結(jié)束值的動(dòng)畫。然后返回調(diào)用了 runAnimation
,返回值為 AnimationResult
,來看下吧:
class AnimationResult<T, V : AnimationVector>( ? ? ? ?val endState: AnimationState<T, V>, ? ? ? ?val endReason: AnimationEndReason ) { ? ?override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)" }
AnimationResult
在動(dòng)畫結(jié)尾包含關(guān)于動(dòng)畫的信息,endState
捕獲動(dòng)畫在最后一幀的值 evelocityframe time
等。它可以用于啟動(dòng)另一個(gè)動(dòng)畫以從先前中斷的動(dòng)畫繼續(xù)速度。endReason
描述動(dòng)畫結(jié)束的原因。
下面看下 runAnimation
吧:
private suspend fun runAnimation( ? ?animation: Animation<T, V>, ? ?initialVelocity: T, ? ?block: (Animatable<T, V>.() -> Unit)? ): AnimationResult<T, V> { ? ? ?val startTime = internalState.lastFrameTimeNanos ? ?return mutatorMutex.mutate { ? ? ? ?try { ? ? ? ? ? ...... ? ? ? ? ? ?endState.animate( ? ? ? ? ? ? ? ?animation, ? ? ? ? ? ? ? ?startTime ? ? ? ? ? ) { ? ? ? ? ? ? ? ?updateState(internalState) ? ? ? ? ? ? ? ...... ? ? ? ? ? } ? ? ? ? ? ?val endReason = if (clampingNeeded) BoundReached else Finished ? ? ? ? ? ?endAnimation() ? ? ? ? ? ?AnimationResult(endState, endReason) ? ? ? } catch (e: CancellationException) { ? ? ? ? ? ?// Clean up internal states first, then throw. ? ? ? ? ? ?endAnimation() ? ? ? ? ? ?throw e ? ? ? } ? } }
這里需要注意:所有不同類型的動(dòng)畫代碼路徑最終都會(huì)匯聚到這個(gè)方法中。
好了,基本快見到陽光了!
天亮了
上面方法中有一行:endState.animate
,這個(gè)是關(guān)鍵,來看下!
internal suspend fun <T, V : AnimationVector> AnimationState<T, V>.animate( ? ?animation: Animation<T, V>, ? ?startTimeNanos: Long = AnimationConstants.UnspecifiedTime, ? ?block: AnimationScope<T, V>.() -> Unit = {} ) { ? ?val initialValue = animation.getValueFromNanos(0) ? ?val initialVelocityVector = animation.getVelocityVectorFromNanos(0) ? ?var lateInitScope: AnimationScope<T, V>? = null ? ?try { ? ? ? ?if (startTimeNanos == AnimationConstants.UnspecifiedTime) { ? ? ? ? ? ?val durationScale = coroutineContext.durationScale ? ? ? ? ? ?animation.callWithFrameNanos { ? ? ? ? ? ? ? ?lateInitScope = AnimationScope(...).apply { ? ? ? ? ? ? ? ? ? ?// 第一幀 ? ? ? ? ? ? ? ? ? ?doAnimationFrameWithScale(it, durationScale, animation, this@animate, block) ? ? ? ? ? ? ? } ? ? ? ? ? } ? ? ? } else { ? ? ? ? ? ?lateInitScope = AnimationScope(...).apply { ? ? ? ? ? ? ? ?// 第一幀 ? ? ? ? ? ? ? ?doAnimationFrameWithScale() ? ? ? ? ? } ? ? ? } ? ? ? ?// 后續(xù)幀 ? ? ? ?while (lateInitScope!!.isRunning) { ? ? ? ? ? ?val durationScale = coroutineContext.durationScale ? ? ? ? ? ?animation.callWithFrameNanos { ? ? ? ? ? ? ? ?lateInitScope!!.doAnimationFrameWithScale(it, durationScale, animation, this, block) ? ? ? ? ? } ? ? ? } ? ? ? ?// 動(dòng)畫結(jié)束 ? } catch (e: CancellationException) { ? ? ? ?lateInitScope?.isRunning = false ? ? ? ?if (lateInitScope?.lastFrameTimeNanos == lastFrameTimeNanos) { ? ? ? ? ? ?isRunning = false ? ? ? } ? ? ? ?throw e ? } }
嗯,柳暗花明!這個(gè)動(dòng)畫函數(shù)從頭到尾運(yùn)行給定 animation
中定義的動(dòng)畫。在動(dòng)畫過程中,AnimationState
將被更新為最新的值,速度,幀時(shí)間等。
到這里 animateColorAsState
大概過了一遍,但也只是簡(jiǎn)單走了一遍流程,并沒有深究里面的細(xì)節(jié),比如 Animatable
類中都沒看,runAnimation
方法也只是看了主要的代碼等等。
結(jié)尾
到此這篇關(guān)于Compose 動(dòng)畫藝術(shù)之屬性動(dòng)畫探索的文章就介紹到這了,更多相關(guān)Compose屬性動(dòng)畫內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Android之Compose頁面切換動(dòng)畫介紹
- 利用Jetpack Compose繪制可愛的天氣動(dòng)畫
- 通過Jetpack Compose實(shí)現(xiàn)雙擊點(diǎn)贊動(dòng)畫效果
- Jetpack Compose實(shí)現(xiàn)動(dòng)畫效果的方法詳解
- Jetpack Compose實(shí)現(xiàn)列表和動(dòng)畫效果詳解
- Android中分析Jetpack?Compose動(dòng)畫內(nèi)部的實(shí)現(xiàn)原理
- Compose開發(fā)之動(dòng)畫藝術(shù)探索及實(shí)現(xiàn)示例
- Android Compose 屬性動(dòng)畫使用探索詳解
相關(guān)文章
Android獲取手機(jī)系統(tǒng)版本等信息的方法
這篇文章主要介紹了Android獲取手機(jī)系統(tǒng)版本等信息的方法,涉及Android獲取手機(jī)版本中各種常見信息的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件
這篇文章主要介紹了Android 基于RecyclerView實(shí)現(xiàn)的歌詞滾動(dòng)自定義控件,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-03-03android開機(jī)自啟動(dòng)原理與實(shí)現(xiàn)案例(附源碼)
完成一下步驟后,啟動(dòng)一次程序,完成注冊(cè)。等下次手機(jī)開機(jī)時(shí),該軟件即會(huì)自動(dòng)啟動(dòng),具體實(shí)現(xiàn)如下,感興趣的朋友可以參考下哈2013-06-06Android ActionBarActivity設(shè)置全屏無標(biāo)題的方法總結(jié)
這篇文章主要介紹了Android ActionBarActivity設(shè)置全屏無標(biāo)題的相關(guān)資料,需要的朋友可以參考下2017-07-07Android 通過httppost上傳文本文件到服務(wù)器的實(shí)例代碼
這篇文章主要介紹了Android 通過httppost上傳文本文件到服務(wù)器的實(shí)例代碼,非常簡(jiǎn)單易懂,非常實(shí)用,需要的朋友可以參考下2016-08-08Android中GridView和ArrayAdapter用法實(shí)例分析
這篇文章主要介紹了Android中GridView和ArrayAdapter用法,結(jié)合實(shí)例形式分析了Android中GridView結(jié)合ArrayAdapter實(shí)現(xiàn)表格化排版的相關(guān)技巧,需要的朋友可以參考下2016-02-02AndroidStudio 實(shí)現(xiàn)加載字體資源的方法
這篇文章主要介紹了AndroidStudio 實(shí)現(xiàn)加載字體資源的方法的相關(guān)資料,這里提供了詳細(xì)的實(shí)現(xiàn)方法,需要的朋友可以參考下2016-11-11Android開發(fā)之Service用法實(shí)例
這篇文章主要介紹了Android開發(fā)之Service用法,實(shí)例分析了Android中Service的功能及使用技巧,需要的朋友可以參考下2015-05-05