前言
动画的各种属性,比如动画时长、动画曲线、动画是否重复播放等,都是通过AnimationSpec
来进行配置的。
本文会讲述各种AnimationSpec
的特点、用法和应用场景。
概述
AnimationSpec
是动画系统中的一个重要接口,比如animateDpAsState
、animateTo
函数都有一个animationSpec参数,我们可以通过这个参数来配置动画。
kotlin
@Composable
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
label: String = "DpAnimation",
finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
// ..
}
kotlin
suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V> {
// ..
}
这里有一个方块缩放的动画,现在给动画添加上弹跳效果。
kotlin
var big by remember { mutableStateOf(false) }
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim =
remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(
targetValue = size,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
) // 设置弹跳效果
}
Box(modifier = Modifier
.size(anim.value)
.background(Color.Green)
.clickable {
big = !big
}
)

这个spring函数会返回一个SpringSpec类型的对象,它是AnimationSpec接口的实现类,它提供了一系列基于弹簧的动画曲线。
如果我们不配置animationSpec参数,其实默认也是一个SpringSpec的对象,只不过是一种不会回弹的弹簧效果。
kotlin
suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>{ /*...*/ }
internal val defaultSpringSpec: SpringSpec<T> =
SpringSpec(visibilityThreshold = visibilityThreshold)
@Immutable
class SpringSpec<T>(
val dampingRatio: Float = Spring.DampingRatioNoBouncy, // 不会弹起
val stiffness: Float = Spring.StiffnessMedium,
val visibilityThreshold: T? = null
) : FiniteAnimationSpec<T> {/*...*/}
可以看到SpringSpec的dampingRatio(阻尼比)的默认值是Spring.DampingRatioNoBouncy,具体来说这种不回弹的弹簧,就是一种先加速再减速的动画曲线。
现在你心中应该有两个问题。
-
问题1:这种弹簧效果都有什么参数?我又该怎么设置啊?
-
问题2:除了弹簧效果,还有什么效果(AnimationSpec)?
我们先来看看AnimationSpec的继承树:
发现好多XxxSpec啊,而且不止这些,还有VectorizedAnimationSpec接口的继承树、DecayAnimationSpec接口的继承树、VectorizedDecayAnimationSpec接口的继承树、FloatDecayAnimationSpec接口的继承树。
这么多,怎么学?
别着急,一步步来,先来看最容易的TweenSpec。
TweenSpec------基于时间的渐变动画
TweenSpec是最基础的TweenSpec,其工作模式是:你给它一个动画时长和一个动画曲线,它就会自动计算出每一时刻动画的完成度。
"Tween" 来源于 "in-betweening"(中间过渡),指的是在起始值和结束值之间创建平滑的过渡。
它的构造函数:
kotlin
@Immutable
class TweenSpec<T>(
val durationMillis: Int = DefaultDurationMillis, // 指定的动画时长,默认是300ms
val delay: Int = 0, // 动画启动的延时,默认是0ms
val easing: Easing = FastOutSlowInEasing // 动画曲线
)
其中Easing的官方翻译是缓动,指的是动画是怎么动的,说白了,就是动画的速度变化曲线。
这个动画曲线大部分情况下不用我们手写,只要在Compose设定好的4个中,去选一个使用就可以了。
kotlin
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f) // 快速启动缓慢结束,就是先加速再减速,适合元素变换场景,比如组件位置、大小、角度发生变化
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f) // 匀速启动然后缓慢停止,类似飞机降落,适合元素的入场动画
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f) // 快速启动匀速结束,适合元素的消失动画
val LinearEasing: Easing = Easing { fraction -> fraction } // 线性曲线,也就是匀速运动,适用于旋转等需要匀速的场景,一般不使用这种动画曲线
这几种缓动曲线的可视化对比:

一般使用前面三种就够用了,最后一种的使用场景非常少,如果这几种都无法满足需求,你也可以去实现一个自定义的Easing:
kotlin
@Stable
fun interface Easing {
fun transform(fraction: Float): Float
}
参数是时间的完成度,返回值是动画的完成度。
自定义缓动曲线的示例:
kotlin
var big by mutableStateOf(false)
setContent {
val offset = remember(big) { if (big) (-48).dp else 48.dp }
val offsetAnim = remember { Animatable(offset, Dp.VectorConverter) }
LaunchedEffect(big) {
offsetAnim.animateTo(
offset,
animationSpec = TweenSpec(easing = Easing { it })
)
}
Box(modifier = Modifier.fillMaxSize()){
Box(modifier = Modifier
.offset(offsetAnim.value,offsetAnim.value)
.size(48.dp)
.background(Color.Green)
.clickable {
big = !big
}
)
}
}
其中easing = Easing { it }
的动画曲线的效果就是匀速运动,也就是上面的LinearEasing
动画曲线,你会发现和官方的定义是一模一样的。
你也可以使用贝塞尔曲线作为动画曲线,就比如前面的FastOutSlowInEasing
缓动曲线其实就是创建三阶贝塞尔曲线对象完成的,四个参数是确定两个点的坐标的,分别是x <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 _1 </math>1、y <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 _1 </math>1、x <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 _2 </math>2、y <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 _2 </math>2,已经有两个点确定了,所以我们只需确定两个点的坐标。
kotlin
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
在线查看三阶贝塞尔曲线。
TweenSpec对象可以用一个tween()
函数来快捷创建。
更多的缓动曲线,你可以点击查看实际应用的效果
SnapSpec------瞬时切换的动画
SnapSpec
和Animatable
的snapTo()
函数是一样的效果,使用它时,会立即从初始值变为目标值,没有任何的平滑过渡。就像拍照一样捕捉了最终状态,这也是为什么它被命名为"Snap",即为"快照"的意思。
它的构造函数:
kotlin
@Immutable
class SnapSpec<T>(
val delayMillis: Int = 0 // 延迟时间
)
它和snapTo
函数的唯一区别就是,我们还可以设置一个延时,在延迟时间结束后,再进行突变。
SnapSpec也是有一个快捷函数snap()
。
KeyframesSpec------关键帧动画
Keyframe是关键帧的意思。允许我们在关键的时间点,控制动画的值。本质上是一个多段TweenSpec,与TweenSpec相比,我们可以创建出更复杂的动画效果。
使用Keyframe的构造函数来创建对象非常复杂,要像下面这样,写一大段,才可以正式开始配置动画。
kotlin
animationSpec = KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<Dp>().apply {
/*...*/
})
而这个写法几乎是固定的。所以它也有简便写法:
kotlin
animationSpec = keyframes {
// 在这里设置关键帧
durationMillis = 总动画时长
delayMillis = 延迟时间
}
其实简便写法就是专门为了KeyframesSpec而存在的,只不过为了接口的统一性,也给TweenSpec、SnapSpec也加了简便写法。
那动画该如何配置呢?
我们只要设置动画某一刻的具体值即可,像下面这样:会在动画进行到150ms时,让size的值变为192dp。
kotlin
anim.animateTo(
size,
animationSpec = keyframes {
192.dp at 150 // 设置关键帧
durationMillis = 450 // 设置动画的时长(ms)
delayMillis = 100 // 设置动画的启动延时(ms)
}
)
为某一段动画设置速度曲线可以使用using
函数,我们每次设置的动画曲线,都是为当前时刻的后续设置的。 比如这里是为[150ms,250ms]的时间段设置的FastOutSlowInEasing动画曲线。
如果不添速度曲线,默认是匀速的,也就是LinearEasing
。
kotlin
anim.animateTo(
size,
animationSpec = keyframes {
192.dp at 150 using FastOutSlowInEasing // 为[150ms,250ms]设置动画曲线
88.dp at 250
durationMillis = 450
delayMillis = 100
}
)
如果要设置起始动画的速度曲线,只需在0ms时刻添加一个关键帧,再设置速度曲线即可。
你要正确地设置初始值,不要无脑填0dp,而是当前动画应该的初始值,比如这里是48dp:
kotlin
anim.animateTo(
size,
animationSpec = keyframes {
48.dp at 0 using FastOutLinearInEasing
192.dp at 150 using FastOutSlowInEasing
24.dp at 300
durationMillis = 450
delayMillis = 100
}
)
注意 :使用
KeyframesSpec
会降低动画的复用性。在设计动画时,需要为不同的动画过程(如正向和反向)分别定义关键帧动画,而不能简单地反转同一个动画。
SpringSpec------基于物理弹簧的动画
前面的TweenSpec、SnapSpec、KeyframesSpec的动画时长都是固定的,这不是不能更改的意思,而是动画的时长是可以被我们知道的,是确定了的。
SpringSpec就属于动画时长不确定的动画,动画时长取决于弹簧参数和初始条件。
因为弹簧效果是基于物理模型的,它模拟了真实世界中的弹簧特性,需要实时地演算,才能知道每一刻的状态,直到停止,所以你是不能设置精确的停止时间的。
SpringSpec的简便函数是spring(),其默认实现是一个不具有弹跳效果的弹簧,这我们在前面已经知道了。
kotlin
fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy, // 是物理上的概念,阻尼比。阻尼比越大,振动的衰减速度更快,默认值是1f
stiffness: Float = Spring.StiffnessMedium, // 也是物理上的概念,刚度。刚度越大,振动频率越高。
visibilityThreshold: T? = null // 可视阈值,和动画的精确度有关
)
阻尼比
阻尼比决定了弹簧"动力"的衰减速度,不同的阻尼比的效果对比:
理论上当阻尼比为0时,弹簧不会停止运动。但是Compose是不允许我们这样的,阻尼比只能大于0,你设置为0,弹簧就不弹了,会瞬间到达终点。
阻尼比大于1的值是可以设置的,会使得弹簧回弹更慢。
刚度
刚度决定了弹簧的"紧实"程度,影响振动频率,不同的刚度的效果对比:

以上的阻尼比相同,为MediumBouncy。
一般来说参数使用Compose提供好的值就可以了,而不是自定义。
可视阈值
visibilityThreshold是可视阈值的意思,如果我们完全按照物理模型去模拟弹簧,弹到后面,屏幕的像素已经体现不出它的振动了,人眼也已经无法察觉到,这时它还在振动。但这种振动已经没有意义了。
所以我们要设置一个阈值:当振动幅度小于多少时,强制让弹簧停止。
visibilityThreshold的默认值是0.01。
我们要控制visibilityThreshold的值,不至于让用户察觉到动画的突然终止,并且还可以省去无意义的动画,节省资源。
千万不要觉得默认的0.01值够小了,有时我们的状态值,并不是作为最终的参数,用到这个值地方,需要将这个值乘上一个很大的系数,那么我们现在的0.01,精度可能就不太够了,可能需要是0.000001才行。
RepeatableSpec------重复播放的动画
RepeatableSpec
的效果是重复执行动画。参数是其它类型的动画(如 TweenSpec
、KeyframesSpec
等)。
它也有简便函数repeatable():
kotlin
animationSpec = repeatable()
函数原型:
kotlin
fun <T> repeatable(
iterations: Int, // 动画的重复次数
animation: DurationBasedAnimationSpec<T>, // 其他类型的动画,是 DurationBasedAnimationSpec 接口的实现类
repeatMode: RepeatMode = RepeatMode.Restart, // 重复模式,每次重复时,如何播放动画,重启还是倒放
initialStartOffset: StartOffset = StartOffset(0) // 初始延迟偏移,是时间上的偏移,而不是位置的偏移
)
重复模式定义了如何重复播放动画。
RepeatMode.Restart
:每次重复时,都从初始值开始,到目标值RepeatMode.Reverse
:每次重复时,初始值和目标值对调,会形成来回摆动的效果
如果重复次数为偶数,并且重复模式是倒放,动画可能会出现突变。因为动画结束后,会变为本来的目标值,而最后一次重复时,结果值与目标值是相对的,所以就会有一个值突变的过程,实际显示效果就是闪了一下。
重复次数为2次,倒放的突变效果:

为什么偏移不是一个Long类型、或者Int类型,而是一个StartOffset对象呢?
因为它还有第二个参数,可以设置偏移类型StartOffsetType。
延时型StartOffsetType.Delay,它没有任何特殊之处,就是给动画添加了一个启动延时。
而快进偏移StartOffsetType.FastForward,它可以快进到你指定的时间点,再开始动画。你可以理解为跳过前面的一段动画过程。
InfiniteRepeatableSpec------无限重复动画
InfiniteRepeatableSpec是无限循环的动画效果。它和RepeatableSpec类似,这两个的底层没有什么本质的区别,唯一的区别就是参数不同,一个有动画执行次数,一个没有。
InfiniteRepeatableSpec只有在animateTo函数结束了,它才会结束。
那怎么让animateTo函数结束呢?
只要让animateTo函数所在的协程被取消即可。
其他Spec
起作用的动画配置就以上六种,还有一些我们用不到的Spec。
FloatAnimationSpec接口以及它的实现类:FloatSpringSpec、FloatTweenSpec,是专门为 Float 类型优化的动画规格。
这些专用实现可以提供更高的性能,但这是给 Compose 动画底层做辅助使用的,我们不需要使用它们。
VectorizedAnimationSpec ,它也是辅助的,用于 Compose 动画系统的内部计算,当我们使用普通的 AnimationSpec
时,Compose 会在内部将其转换为对应的 VectorizedAnimationSpec
,以便进行矢量化计算(通过 AnimationVector
类)。
DecayAnimationSpec是有用的,但现在不讲。