引言
- 在上篇我们详细介绍了 Compose 封装的各种高级别的 API,让我们知道能用极少的代码来写出自己想要的动画效果,并且体会到了声明式编程的魅力。
- 本篇会进一步深入了解 Compose 的动画使用,了解一些更底层的 API。
一、动画的底层 API 调用
我们建议结合协程来管理你的自定义动画(Coroutine-based Animations)
- Jetpack Compose 的动画,最终都离不开
Animation<T, V : AnimationVector>
这个接口,我们可以看下这张图
- 图中表示官方提供了实现
Animation
接口的两个类,分别是TargetBasedAnimation
和DecayAnimation
,他们分别使用着独特的动画规范,AnimationSpec<T>
和DecayAnimationSpec<T>
。 TargetBasedAnimation
是为基于目标的动画服务的。- 它会一直持有的动画的起始值和起始速度,以及不因动画进行和发生改变的结束值,保存下来的值会提供便捷的 API 供外部访问。
- 需要注意的是,Compose 的动画是可以被打断的。若当前的 TargetBasedAnimation 被打断,会使用当前状态的数值和速度来构造一个新的实例;
- 建议直接使用
Animatable.animateTo
来使用TargetBasedAnimation
。 DecayAnimation
直译为衰减动画。指的是随着时间的推移,初始设定的速度会逐渐变慢。- 同样的,提供了便捷的 API 来访问动画过程中的状态,如初始速度、初始速度方向、动画规范、类型转化器等;
- 手动控制
DecayAnimation
的触发时机是没有作用的,建议直接使用高级 API 如Animatable.animateDecay
等。 - 因为使用的思路大致相同,本文会重点聊聊
Animatable.animateTo
,Animatable.animateDecay
将不详述。
二、今天的主角:挂起函数 AnimateTo
2.1 先来聊聊 Animatable
Animatable
是被设计成数据容器的类,用于保存目标数值。当动画执行的时候(如执行animateTo
),它所持有的数据会根据设定进行改变;- 上面提到,当动画被打断时,创建的新的
TargetBasedAnimation
会“继承”当前的数值和速度,而数值就是从 Animatable 对象中获取的; - 在代码中,我们可以通过以下方式创建一个 Animatable 对象
val alpha = remember { Animatable(0f) } // 指的是 alpha 是一个 Animatable 对象,它当前持有的数值为 0f
2.2 接着是主角 animateTo
// Animatable.kt
suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>
animateTo
是一个挂起函数,也是Animatable
对象的拓展函数;- 在当前方法中,我们可以设置
TargetBasedAnimation
的目标值、动画规范和初始速度; - 其中,
block
参数回调的是每一帧动画返回的Animatable
对象,即我们可以在这里监听并获取动画的实时状态; - 方法的返回值返回的是动画的结果
AnimationResult
- 由于这是挂起函数,所以方法的结束表明了动画的结束。在同一个协程域中,在方法后执行的逻辑都表明在动画结束后执行。
- 动画的结果
AnimationResult
包含两部分,一部分是动画的结束状态endState
,包含了动画停止时最后一帧的状态(详见AnimationState
);另一部分是动画的结束原因,包含正常结束或者是触达边界结束(详见AnimationEndReason
)。
2.3 更便捷的写法 animate
- 官方在
SuspendAnimation.kt
中定义了如下方法:
suspend fun animate(
initialValue: Float,
targetValue: Float,
initialVelocity: Float = 0f,
animationSpec: AnimationSpec<Float> = spring(),
block: (value: Float, velocity: Float) -> Unit
)
- 这是一种更简洁的写法,优点是代码量更少,无需提前定义
animatable
变量,缺点是当前方法不会返回任何当前动画相关的属性; - 同样的,衰减动画也有与之对应的 API
animateDecay
。
三、多动画并发执行实践
3.1 监听动画的开始和结束
- 得益于协程,我们可以非常方便地监听到执行动画中的不同阶段;
- 也不要再想什么
onAnimationStart()
和onAnimationEnd()
的监听器了,在动画的挂起函数里面是没有的。
// 顺序动画
val scope = rememberCoroutineScope()
scope.launch {
animate(/*...*/) // 先执行 animationA
viewmodel.doSomething() // animationA 指定完成后执行 viewModel 逻辑
animate(/*...*/) // 等待 viewModel 执行完成后执行 animationB
}
3.2 并行执行动画
- 我们建议在 CoroutineScope 中使用
launch
创建新的协程来并发执行动画;
// 并行动画
val scope = rememberCoroutineScope()
scope.launch {
launch {
animate(/*...*/) // 动画 A
}
launch {
animate(/*...*/) // 动画 B
}
}
// 上述写法,动画 A 和动画 B 会在同时执行。
四、简单说说动画的触发机制
- 对Compose 有一定了解的同学都会知道,Compose 界面的重组都是依靠
State<T>
来触发的,而动画也不例外。 - 动画触发 UI 刷新的类是
AnimationState<T, V>
,上面提到的返回最后一帧的动画状态也就是前面提到的这个类。而在动画的执行过程中,负责动画的挂起函数会持续发送新的 State 到 UI 上,具体可以看到这个方法Animatable.runAnimation
// All the different types of animation code paths eventually converge to this method.
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 {
// ...
endState.animate(
animation,
startTime
) {
updateState(internalState) // 关键逻辑
// ...
}
val endReason = if (clampingNeeded) BoundReached else Finished
endAnimation()
AnimationResult(endState, endReason)
} catch (e: CancellationException) {
endAnimation()
throw e
}
}
}
五、小结
- 了解 Compose 动画的底层 API ,知道了
Animation
、Animatable
、AnimationState
、animateTo
之间的关系; - 知道了 Compose 动画主要划分为
TargetBasedAnimation
和DecayAnimation
两种; - 知道了更高定制化的动画可以使用
Animatable
中的挂起函数实现; - 了解到多个动画顺序和并发执行的写法;
- 了解到动画在源码中的触发机制入口,知道了内部是
AnimationState
触发数据刷新;
完结撒花。
参考文献:
- 知识储备篇
- Jetpack Compose 官方文档 —— 动画
- Youtube 视频 —— 重新思考动画系统 ← 推荐一定要看
- Jetpack Compose 源码
相关文章:
预告:下篇写写 Jetpack Compose 手势。(有一起学习 Compose 的小伙伴来交流呀!)