入門
https://developer.android.com/jetpack/compose/animation/quick-guide
思想
Compose的動(dòng)畫系統(tǒng)是基于值系統(tǒng)的動(dòng)畫,和傳統(tǒng)的基于回調(diào)的動(dòng)畫不同窝剖,Compose的動(dòng)畫api通常對(duì)外暴露一個(gè)可觀察的隨時(shí)間改變的狀態(tài)麻掸,進(jìn)而驅(qū)動(dòng)重組或者重繪,從而達(dá)成動(dòng)畫的效果
基本使用
可見性動(dòng)畫使用AnimatedVisibility,內(nèi)容尺寸動(dòng)畫用animateContentSize,根據(jù)不同的狀態(tài)展示不同的Composable之間的動(dòng)畫切換使用AnimatedContent
,也可以使用Crossfade
簡(jiǎn)單的值動(dòng)畫使用使用animateXXAsState,多個(gè)動(dòng)畫同時(shí)啟用赐纱,可以用Transition進(jìn)行管理脊奋,Transition可以基于狀態(tài)啟動(dòng)多個(gè)動(dòng)畫
如果需要在啟動(dòng)的時(shí)候就進(jìn)行一個(gè)動(dòng)畫,推薦使用Transition或者Animtable動(dòng)畫接口疙描,通過在可組合函數(shù)中聲明一個(gè)基礎(chǔ)狀態(tài)诚隙,在compose中啟動(dòng)該動(dòng)畫是一個(gè)副作用效應(yīng),應(yīng)該在LaunchEffect中進(jìn)行起胰,Animatable提供的animateTo是一個(gè)中斷函數(shù)久又,直到動(dòng)畫狀態(tài)進(jìn)行到目標(biāo)狀態(tài)時(shí)才會(huì)恢復(fù),通過該特性我們可以執(zhí)行序列化的動(dòng)畫效五,同樣地地消,如果我們想要?jiǎng)赢嬐瑫r(shí)執(zhí)行的話,可以通過啟動(dòng)多個(gè)協(xié)程來(lái)達(dá)成這一點(diǎn)
Compose動(dòng)畫api的設(shè)計(jì)
底層動(dòng)畫為實(shí)現(xiàn)Animation
接口的兩個(gè)對(duì)象畏妖,一個(gè)是TargetBasedAnimation
脉执,提供了從起始值到目標(biāo)值變更的動(dòng)畫邏輯,DecayAnimation
一個(gè)無(wú)狀態(tài)的動(dòng)畫戒劫,提供了一個(gè)從初始速度漸漸慢下來(lái)的動(dòng)畫邏輯(滑動(dòng)動(dòng)畫)
Animation的目標(biāo)是提供無(wú)狀態(tài)的動(dòng)畫邏輯半夷,因此他是一個(gè)底層組件,頂層組件在Animation的基礎(chǔ)上構(gòu)建有狀態(tài)的動(dòng)畫邏輯,設(shè)計(jì)為無(wú)狀態(tài)的接口迅细,意味著不同于安卓View動(dòng)畫巫橄,是沒有pause,cancel等邏輯的,換句話說(shuō)疯攒,如果使用底層動(dòng)畫api創(chuàng)建的動(dòng)畫被取消了嗦随,并且需要重新開始,這種情況下應(yīng)該重新創(chuàng)建一個(gè)底層動(dòng)畫的實(shí)例敬尺,并且初始值和初始速度為之前取消時(shí)的當(dāng)前值和當(dāng)前速度
Animatable是在Animation的基礎(chǔ)上封裝的有狀態(tài)的動(dòng)畫api,提供停止和開始動(dòng)畫的能力
Animatable使用
- 創(chuàng)建動(dòng)畫實(shí)例枚尼,指定初始值,如果是在compose中使用砂吞,要用remember保存起來(lái)
val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
- 啟動(dòng)動(dòng)畫
根據(jù)動(dòng)畫是以值動(dòng)畫為基礎(chǔ)署恍,還是以速度為基礎(chǔ),分別調(diào)用不同的api,前者是animateTo,后者是animateDecay,這兩個(gè)api實(shí)際上也對(duì)應(yīng)了底層兩個(gè)不同的Animation類型蜻直,
可以看到在啟動(dòng)動(dòng)畫這里
animatable.animateTo(newTarget, animSpec)
- 停止動(dòng)畫
分為兩種兩種盯质,一種是我們需要手動(dòng)停止動(dòng)畫袁串,另外一種是隨著需要?jiǎng)赢嫷腸omposable退出組合自動(dòng)停止動(dòng)畫,其中手動(dòng)停止動(dòng)畫呼巷,直接調(diào)用Animatable的stop方法囱修,另外一種是自動(dòng)停止動(dòng)畫,只需要在使用該動(dòng)畫的composable處使用LaunchEffect啟動(dòng)動(dòng)畫就好
手動(dòng)停止動(dòng)畫會(huì)設(shè)置一個(gè)flag,在動(dòng)畫執(zhí)行的過程中會(huì)檢查該flag,從而達(dá)到停止動(dòng)畫的目的王悍,而自動(dòng)停止動(dòng)畫破镰,則是通過協(xié)程的取消機(jī)制來(lái)保證的,
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)
}
private suspend fun runAnimation(
animation: Animation<T, V>,
initialVelocity: T,
block: (Animatable<T, V>.() -> Unit)?
): AnimationResult<T, V> {
// Store the start time before it's reset during job cancellation.
val startTime = internalState.lastFrameTimeNanos
return mutatorMutex.mutate {
try {
internalState.velocityVector = typeConverter.convertToVector(initialVelocity)
targetValue = animation.targetValue
isRunning = true
val endState = internalState.copy(
finishedTimeNanos = AnimationConstants.UnspecifiedTime
)
var clampingNeeded = false
endState.animate(
animation,
startTime
) {
updateState(internalState)
val clamped = clampToBounds(value)
if (clamped != value) {
internalState.value = clamped
endState.value = clamped
block?.invoke(this@Animatable)
cancelAnimation()
clampingNeeded = true
} else {
block?.invoke(this@Animatable)
}
}
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)畫被放在mutatorMutex.mutate邏輯中压储,這個(gè)函數(shù)被設(shè)計(jì)成啟動(dòng)成獲取新鎖的時(shí)候會(huì)取消前一個(gè)協(xié)程鲜漩,等前一個(gè)協(xié)程取消時(shí)款熬,再重新獲取鎖炕矮,保證同一時(shí)間只有一段代碼在訪問臨界區(qū)的Animatable相關(guān)狀態(tài)拦键,可以看到多次調(diào)用Animatable的animateTo方法左腔,會(huì)將當(dāng)前動(dòng)畫的進(jìn)度和速度保存下來(lái),作為新的起點(diǎn)臀晃,因此如果動(dòng)畫進(jìn)行到一半驶沼,動(dòng)畫被取消了爆办,動(dòng)畫會(huì)從一半的進(jìn)度繼續(xù)播放到目標(biāo)進(jìn)度为朋,如果動(dòng)畫進(jìn)行已經(jīng)結(jié)束了臂拓,這個(gè)時(shí)候再調(diào)用animateTo,則會(huì)相當(dāng)于重新執(zhí)行動(dòng)畫
Animtable將一些動(dòng)畫的內(nèi)部狀態(tài)保留在AnimationState
這個(gè)數(shù)據(jù)結(jié)構(gòu)中,
實(shí)際的動(dòng)畫邏輯在此處
endState.animate(
animation,
startTime
) {
updateState(internalState)
val clamped = clampToBounds(value)
if (clamped != value) {
internalState.value = clamped
endState.value = clamped
block?.invoke(this@Animatable)
cancelAnimation()
clampingNeeded = true
} else {
block?.invoke(this@Animatable)
}
}
首先我們看下animate函數(shù)本身做了什么
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) // 獲取動(dòng)畫的初始值
// 獲取動(dòng)畫的初始速度
val initialVelocityVector = animation.getVelocityVectorFromNanos(0)
// 如果前一個(gè)動(dòng)畫是被取消的习寸,那么這兩個(gè)值都會(huì)繼承
var lateInitScope: AnimationScope<T, V>? = null
try {
if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
val durationScale = coroutineContext.durationScale
animation.callWithFrameNanos {
lateInitScope = AnimationScope(
initialValue = initialValue,
typeConverter = animation.typeConverter,
initialVelocityVector = initialVelocityVector,
lastFrameTimeNanos = it,
targetValue = animation.targetValue,
startTimeNanos = it,
isRunning = true,
onCancel = { isRunning = false }
).apply {
// First frame
doAnimationFrameWithScale(it, durationScale, animation, this@animate, block)
}
}
} else {
lateInitScope = AnimationScope(
initialValue = initialValue,
typeConverter = animation.typeConverter,
initialVelocityVector = initialVelocityVector,
lastFrameTimeNanos = startTimeNanos,
targetValue = animation.targetValue,
startTimeNanos = startTimeNanos,
isRunning = true,
onCancel = { isRunning = false }
).apply {
// First frame
doAnimationFrameWithScale(
startTimeNanos,
coroutineContext.durationScale,
animation,
this@animate,
block
)
}
}
// Subsequent frames
while (lateInitScope!!.isRunning) {
val durationScale = coroutineContext.durationScale
animation.callWithFrameNanos {
lateInitScope!!.doAnimationFrameWithScale(it, durationScale, animation, this, block)
}
}
// End of animation
} catch (e: CancellationException) {
lateInitScope?.isRunning = false
if (lateInitScope?.lastFrameTimeNanos == lastFrameTimeNanos) {
// There hasn't been another animation.
isRunning = false
}
throw e
}
}
根據(jù)是第一次啟動(dòng)該動(dòng)畫(或者重新運(yùn)行)構(gòu)建一個(gè)AnimationScope,記錄下動(dòng)畫開始的正確時(shí)間戳,創(chuàng)建完該Scope對(duì)象傻工,調(diào)用該Scope對(duì)象上的doAnimationFrameWithScale方法霞溪,后續(xù)只要?jiǎng)赢嫑]有取消,就一直跟隨時(shí)鐘運(yùn)行動(dòng)畫
private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrameWithScale(
frameTimeNanos: Long,
durationScale: Float,
anim: Animation<T, V>,
state: AnimationState<T, V>, // endState
block: AnimationScope<T, V>.() -> Unit
) {
val playTimeNanos =
if (durationScale == 0f) {
anim.durationNanos
} else {
((frameTimeNanos - startTimeNanos) / durationScale).toLong()
}
// 根據(jù)外部的尺度來(lái)決定動(dòng)畫的速度
doAnimationFrame(frameTimeNanos, playTimeNanos, anim, state, block)
}
private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrame(
frameTimeNanos: Long,
playTimeNanos: Long,
anim: Animation<T, V>,
state: AnimationState<T, V>,
block: AnimationScope<T, V>.() -> Unit
) {
lastFrameTimeNanos = frameTimeNanos
value = anim.getValueFromNanos(playTimeNanos) // 根據(jù)時(shí)長(zhǎng)獲取當(dāng)前值
velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos)
val isLastFrame = anim.isFinishedFromNanos(playTimeNanos)
if (isLastFrame) {
// TODO: This could probably be a little more granular
// TODO: end time isn't necessarily last frame time
finishedTimeNanos = lastFrameTimeNanos
isRunning = false
}
updateState(state)
block()
}
getValueFromNanos的調(diào)用鏈路中捆,以TargetBasedAnimation
為例鸯匹,
調(diào)用該方法,實(shí)際將方法轉(zhuǎn)發(fā)給插值器泄伪,給插值器當(dāng)前時(shí)間殴蓬,以及初始狀態(tài),目標(biāo)狀態(tài)蟋滴,初始速度等等值給出當(dāng)前的值染厅,如果當(dāng)前動(dòng)畫在這一幀結(jié)束,就不再運(yùn)行動(dòng)畫津函,將部分狀態(tài)同步到state中肖粮,然后執(zhí)行block, block代碼如下
updateState(internalState)
val clamped = clampToBounds(value)
if (clamped != value) {
internalState.value = clamped
endState.value = clamped
block?.invoke(this@Animatable)
cancelAnimation()
clampingNeeded = true
} else {
block?.invoke(this@Animatable)
}
首先是將scope的值同步到internalState,internalState對(duì)外暴露了一個(gè)state,這個(gè)state里面保存了最新的值,外部通過觀察這個(gè)值尔苦,就能夠得到當(dāng)前動(dòng)畫的最新值
Compose動(dòng)畫中的插值器
https://blog.csdn.net/EthanCo/article/details/129882487
首先這里存在兩個(gè)概念涩馆,第一個(gè)是轉(zhuǎn)換器行施,轉(zhuǎn)換器是將我們外部希望進(jìn)行動(dòng)畫變更的狀態(tài)轉(zhuǎn)換成動(dòng)畫庫(kù)內(nèi)部使用的狀態(tài),第二個(gè)是插值器魂那,插值器能夠?qū)?nèi)部狀態(tài)根據(jù)動(dòng)畫當(dāng)前進(jìn)行時(shí)長(zhǎng)給出一個(gè)合理的值蛾号,插值器的命名都是XXXSpec,根據(jù)動(dòng)畫是否是一直運(yùn)行的,分為FiniteAnimationSpec和InfiniteRepeatableSpec,插值器在安卓原生動(dòng)畫中的命名是以xxInterpolator來(lái)命名的涯雅,插值器的暗示意味更強(qiáng)
目前思考下來(lái)鲜结,動(dòng)畫的中間狀態(tài)都保存在動(dòng)畫本身中,插值器可以設(shè)計(jì)為無(wú)狀態(tài)的斩芭,這樣插值器在各個(gè)動(dòng)畫之間復(fù)用都不會(huì)出現(xiàn)bug轻腺,實(shí)際看下來(lái)也驗(yàn)證了這個(gè)結(jié)論
Compose動(dòng)畫插值器接口聲明如下
interface AnimationSpec<T> {
/**
* Creates a [VectorizedAnimationSpec] with the given [TwoWayConverter].
*
* The underlying animation system operates on [AnimationVector]s. [T] will be converted to
* [AnimationVector] to animate. [VectorizedAnimationSpec] describes how the
* converted [AnimationVector] should be animated. E.g. The animation could simply
* interpolate between the start and end values (i.e.[TweenSpec]), or apply spring physics
* to produce the motion (i.e. [SpringSpec]), etc)
*
* @param converter converts the type [T] from and to [AnimationVector] type
*/
fun <V : AnimationVector> vectorize(
converter: TwoWayConverter<T, V>
): VectorizedAnimationSpec<V>
}
可以看到可以對(duì)任意類型做動(dòng)畫插值,前提是能夠?qū)⑦@個(gè)類型轉(zhuǎn)換成動(dòng)畫庫(kù)內(nèi)部使用的類型划乖,也就是AnimationVector,AnimationVector本身也是一個(gè)接口贬养,目前支持了最多四個(gè)維度的變化,其中每一個(gè)維度的數(shù)據(jù)限定為Float
所以實(shí)際進(jìn)行動(dòng)畫的是VectorizedAnimationSpec琴庵,我們首先
VectorizedAnimationSpec家族類圖
VectorizedFloatAnimationSpec
被spring動(dòng)畫和tween動(dòng)畫用來(lái)實(shí)現(xiàn)內(nèi)部邏輯误算,首先我們來(lái),
因此細(xì)化拆分迷殿,我們需要有一個(gè)對(duì)Float做動(dòng)畫的機(jī)制儿礼,能夠根據(jù)初始值,初始速度等等獲取目標(biāo)浮點(diǎn)值庆寺,VectorizedFloatAnimationSpec
就是這個(gè)邏輯蚊夫,它將相關(guān)進(jìn)度值的獲取委托給了FloatAnimationSpec
因此Tween的動(dòng)畫核心在FloatTweenSpec中g(shù)etValueFromNanos
override fun getValueFromNanos(
playTimeNanos: Long,
initialValue: Float,
targetValue: Float,
initialVelocity: Float
): Float {
// TODO: Properly support Nanos in the impl
val playTimeMillis = playTimeNanos / MillisToNanos
val clampedPlayTime = clampPlayTime(playTimeMillis)
val rawFraction = if (duration == 0) 1f else clampedPlayTime / duration.toFloat()
val fraction = easing.transform(rawFraction.coerceIn(0f, 1f))
return lerp(initialValue, targetValue, fraction)
}
可以看到進(jìn)度的確定是由easing來(lái)決定的,Easing有唯一一個(gè)實(shí)現(xiàn)CubicBezierEasing
class CubicBezierEasing(
private val a: Float,
private val b: Float,
private val c: Float,
private val d: Float
)
4個(gè)控制點(diǎn)懦尝,從我的理解來(lái)看知纷,x坐標(biāo)代表了時(shí)間,y坐標(biāo)了代表了這個(gè)時(shí)候的真實(shí)進(jìn)度,具體數(shù)學(xué)邏輯留待進(jìn)一步分析
Transition
公開給外部的有啟動(dòng)動(dòng)畫的接口animateTo,有結(jié)束Transition的接口onTransitionEnd,有添加各種動(dòng)畫的接口,以及創(chuàng)建子Transition的接口陵霉,
這部分邏輯應(yīng)該是最復(fù)雜的琅轧,所以留待最后一部分分析,Transition本質(zhì)上是根據(jù)狀態(tài)的變更去管理一群動(dòng)畫同時(shí)運(yùn)行,compose給我們提供的一個(gè)接口使用Transition的是updateTransition,這個(gè)函數(shù)可以驅(qū)動(dòng)Transition狀態(tài)變更踊挠,進(jìn)而驅(qū)動(dòng)動(dòng)畫的進(jìn)行
fun <T> updateTransition(
targetState: T,
label: String? = null
): Transition<T> {
val transition = remember { Transition(targetState, label = label) }
transition.animateTo(targetState)
DisposableEffect(transition) {
onDispose {
// Clean up on the way out, to ensure the observers are not stuck in an in-between
// state.
transition.onTransitionEnd()
}
}
return transition
}
Transition這里的各種各樣的增加動(dòng)畫接口是在Animation的基礎(chǔ)上擴(kuò)展出來(lái)的,通過TransitionAnimationState
進(jìn)行管理該動(dòng)畫乍桂,后續(xù)所有的這些動(dòng)畫由Transition進(jìn)行驅(qū)動(dòng)管理,至于子Transition,首先它本身也是Transition效床,其次在原始Transition的概念上嵌套就可以了,什么場(chǎng)景適合Transition呢睹酌,當(dāng)我們需要根據(jù)一個(gè)狀態(tài)的變化同時(shí)做多種動(dòng)畫,且動(dòng)畫可能本身比較復(fù)雜的時(shí)候扁凛,就可以使用Transition來(lái)管理我們的東西忍疾,如果動(dòng)畫本身比較簡(jiǎn)單,根據(jù)自己的場(chǎng)景去挑選其他動(dòng)畫接口就可了谨朝,如果從某種意義上來(lái)說(shuō)卤妒,Transition相當(dāng)于AnimationManager,但它本身只能控制動(dòng)畫同時(shí)播放(除非設(shè)置延遲)甥绿,不能保證動(dòng)畫播放的先后順序,不過在Compose的場(chǎng)景下也是完全夠用了
通篇看下來(lái)则披,可以說(shuō)Compose的動(dòng)畫系統(tǒng)和原生的動(dòng)畫系統(tǒng)完全不是一回事共缕,再次感嘆State系統(tǒng)設(shè)計(jì)得十分巧妙!