不论是 animateXxxAsState() 还是 Animatable 的 animateTo() 都可以传入 AnimationSpec 以配置动画的规格:
@Composable
fun animateDpAsState(targetValue: Dp,animationSpec: AnimationSpec<Dp> = dpDefaultSpring,label: String = "DpAnimation",finishedListener: ((Dp) -> Unit)? = null
)suspend fun animateTo(targetValue: T,animationSpec: AnimationSpec<T> = defaultSpringSpec,initialVelocity: T = velocity,block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>
比如对于上节方块变大变小的例子,可以指定一个弹簧效果:
anim.animateTo(size, spring(Spring.DampingRatioMediumBouncy))
spring() 会返回 SpringSpec,是 animateTo() 要求的 AnimationSpec 的实现类。为了全面了解 AnimationSpec,我们先查看一下 AnimationSpec 的继承树。
Android Studio 使用技巧:将光标放在想要查看的类上,通过 Ctrl + H 查看该类的继承关系。
Compose 中有多个 AnimationSpec 的接口,除了 AnimationSpec,还有 VectorizedAnimationSpec、DecayAnimationSpec、FloatDecayAnimationSpec、VectorizedDecayAnimationSpec 等接口都有各自的继承树。这一节我们主要介绍 AnimationSpec 继承树中的内容:
1、TweenSpec
看到 Tween 这个单词容易联想到试图动画 View Animation 中的补间动画 Tween Animation,但是这两个 Tween 从程序角度上看没有任何关联,不要试图以补间动画的知识去理解 TweenSpec。
Tween 是一个有些古老的词汇,它来自于动画领域的 Inbetween,是指动画主创在完成关键帧后,由助手完成关键帧之间的补帧工作。后续发展成 Tween 这个词,在程序领域中,指程序员指定起始帧和结束帧,然后由程序完成两帧之间的动画补全。
TweenSpec 的主构造有三个参数:
@Immutable
class TweenSpec<T>(// 动画时长,默认值是 300msval durationMillis: Int = DefaultDurationMillis,// 动画启动延时val delay: Int = 0,// 缓动,动画曲线设置val easing: Easing = FastOutSlowInEasing
) : DurationBasedAnimationSpec<T>
easing 这个单词不论是 Google 的官方翻译还是业界的通用翻译都是“缓动”,指动画是如何进行渐变的,也就是动画曲线。通常我们会从 Compose 提供的四个 Easing 中选取一个使用:
/**
* 从静止状态开始并结束于静止状态的元素使用这种标准的缓动效果。它们会快速加速并逐渐减速,
* 以强调过渡的结束部分。
* 标准缓动通过在减速阶段分配比加速阶段更多的时间,将细微的注意力集中在动画的结尾部分。
* 这是最常见的缓动形式。
* 这相当于 Android 中的 FastOutSlowInInterpolator。
*/
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)/*** 进入的元素使用减速缓动进行动画处理,这种缓动效果使过渡以峰值速度(元素运动的最快点)开始,* 并以静止状态结束。* 这相当于 Android 中的 LinearOutSlowInInterpolator。*/
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)/*** 退出屏幕的元素使用加速缓动效果,它们从静止状态开始,并以峰值速度结束。** 这相当于 Android 中的 FastOutLinearInInterpolator。*/
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)/*** 它直接返回未经修改的分数值。这在需要一个 [Easing] 但实际上不需要任何缓动效果的情况下,* 作为默认值非常有用。*/
val LinearEasing: Easing = Easing { fraction -> fraction }
四种 Easing 的简介:
- FastOutSlowInEasing 相当于属性动画中的 AccelerateDecelerateInterpolator,它加速入场并减速出场,也就是说入场和出场时都是慢速的,在中间过程是快速的
- LinearOutSlowInEasing 是入场速度较快,匀速减速到 0 的曲线,适合做元素入场动画,相当于属性动画的 DecelerateInterpolator
- FastOutLinearInEasing 从静止开始加速退出屏幕,相当于 AccelerateInterpolator
- LinearEasing 的动画曲线是一条直线,也就是匀速的
使用时,可以通过 TweenSpec 的构造函数:
anim.animateTo(size, TweenSpec(easing = FastOutSlowInEasing))
也可以使用 Compose 提供的简便函数:
anim.animateTo(size, tween())
简便函数大概只是为了让我们少写几个字母吧,它的参数以及默认值与 TweenSpec 构造函数完全一样:
@Stable
fun <T> tween(durationMillis: Int = DefaultDurationMillis,delayMillis: Int = 0,easing: Easing = FastOutSlowInEasing
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)
大多数情况下使用现有的四个 Easing 即可,但开发过程难免有需要自己定制的时候,参照现成的实现,都是使用了三阶贝塞尔曲线 CubicBezierEasing():
@Immutable
class CubicBezierEasing(private val a: Float,private val b: Float,private val c: Float,private val d: Float
) : Easing
三阶贝塞尔曲线取消四个点才能确定,其中起点固定为 (0,0),终点固定为 (1,1),剩下两个点的四个坐标就是 CubicBezierEasing() 的四个参数。至于如何确定这四个参数,可以借助一个三阶贝赛尔曲线的动画网站来确定:
左侧坐标的横轴表示时间,纵轴表示动画进度,如上图所示就是 CubicBezierEasing 的图像。
2、SnapSpec
SnapSpec 与 TweenSpec 是兄弟,它的效果与前面介绍过的 Animatable 的 snapTo() 是一样的效果。当 animateTo() 使用 SnapSpec 时,与直接使用 snapTo() 的唯一区别是 SnapSpec 可以增加一个延时:
anim.animateTo(size, SnapSpec(3000))
SnapSpec 也有个简便函数 snap():
anim.animateTo(size, snap(3000))
3、KeyframesSpec
Keyframes 是关键帧的意思,意味着在动画运行过程中可以选取几个关键的时间点,给出对应时间点的动画完成度,KeyframesSpec 就可以根据这些信息计算出整个动画完整的速度曲线。可以看作分段式的 TweenSpec,即将整个动画按照时间点分段,每一段都是一个 TweenSpec。
KeyframesSpec 的构造函数只有一个参数 KeyframesSpecConfig,可以配置动画属性:
@Immutable
class KeyframesSpec<T>(val config: KeyframesSpecConfig<T>) : DurationBasedAnimationSpec<T> {class KeyframesSpecConfig<T> {// 动画时长var durationMillis: Int = DefaultDurationMillis// 动画延时启动var delayMillis: Int = 0// 关键帧internal val keyframes = mutableMapOf<Int, KeyframeEntity<T>>()// 中缀函数用于指定时间点infix fun T.at(/*@IntRange(from = 0)*/ timeStamp: Int): KeyframeEntity<T> {return KeyframeEntity(this).also {keyframes[timeStamp] = it}}...}
}
基于以上源码,如果想通过 KeyframesSpec 的构造函数创建动画,大致代码如下:
// 创建 KeyframesSpecConfig 对象通过 apply() 指定具体属性
anim.animateTo(size, KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<Dp>().apply {durationMillis = 450delayMillis = 500144.dp at 150 with FastOutLinearInEasing20.dp at 300
}))
这样写起来很麻烦,因此 Compose 提供了简便方法 keyframes():
// size 是动画的目标值,也就是终点值
anim.animateTo(size, keyframes {// 指定动画时长为 450msdurationMillis = 450delayMillis = 500// 指定在 150ms 时 size 为 144dp,并且从 150ms 这个时间点// 开始的这一段动画曲线为 FastOutLinearInEasing144.dp at 150 with FastOutLinearInEasing// 指定在 300ms 时 size 为 20dp,没有显式指定 Easing,默认// 使用 LinearEasing20.dp at 300
})
看到这里应该能理解为什么 TweenSpec 和 SnapSpec 有简便方法了,其实主要还是为了给 KeyframesSpec 提供简便方法 keyframes(),同时为了保持一致,就把另外两个也带上了。
使用 KeyframesSpec 有一个限制,就是这样写动画复用性降低了。比如要实现一个反向动画,只能 需要重新写一个 keyframes()。
到这里,DurationBasedAnimationSpec 的三个子类就介绍完了,DurationBased 意思是动画时长都是确定的。
4、SpringSpec
与 DurationBasedAnimationSpec 是兄弟关系的 Spec 有两个 —— SpringSpec 和 RepeatableSpec,本节我们先介绍 SpringSpec。
既然 SpringSpec 是 DurationBasedAnimationSpec 的兄弟,那意味着 SpringSpec 不具备 DurationBasedAnimationSpec 可以确定动画时长的特性。动画有不基于时长的吗?似乎原生的属性动画都是有动画时长的,Compose 却没有?实际上,这种认识是错误的。原生的属性动画确实都有动画时长,但是却没有 SpringSpec 这种基于物理模型的弹簧动画。因此 Compose 不仅没有落后于属性动画,反而是相对于原生增加了一种大的动画类别。
此外,Jetpack 库也提供过弹簧动画的扩展库,同样也是不能设置动画时长的,因为基于物理模型的动画没办法设定一个准确的动画时长。
具体使用上,我们来看前面已经提到过的 spring 函数:
/**
* 创建一个使用给定弹簧常量(即阻尼比和刚度)的 SpringSpec。可选的 visibilityThreshold
* 定义了当动画在视觉上足够接近目标值时,可以将其舍入到目标值。
* 参数:
* dampingRatio - 弹簧的阻尼比。默认为 Spring.DampingRatioNoBouncy。
* stiffness - 弹簧的刚度。默认为 Spring.StiffnessMedium。
* visibilityThreshold - 可选参数,指定可见性阈值。
*/
@Stable
fun <T> spring(dampingRatio: Float = Spring.DampingRatioNoBouncy,stiffness: Float = Spring.StiffnessMedium,visibilityThreshold: T? = null
): SpringSpec<T> =SpringSpec(dampingRatio, stiffness, visibilityThreshold)
dampingRatio 是弹簧的阻尼比,它决定了弹簧有多“弹”。合法值是一个大于 0 的 Float,值越小,弹簧越弹。默认值为 1,此时弹簧不会来回弹,而是到达目标值之后就不弹了。当值大于 1 时,值越大,弹簧的弹性越差,到达目标值的时间就越长。
stiffness 是弹簧的刚度,指弹簧在受到外力作用时,产生单位变形所需的载荷。刚度越大,压缩之后越容易回弹。假如向做一个阻尼小,刚度也小的动画:
// 阻尼 0.2f,刚度 50f
anim.animateTo(size, spring(Spring.DampingRatioHighBouncy, Spring.StiffnessVeryLow))
那么展现出的效果就是弹簧来回弹的次数很多(阻尼低),但是弹的速度很慢(刚度低)。
最后一个参数 visibilityThreshold,可见性阈值,指弹簧偏离原点多远时强制让弹簧的运动停下来。设置它主要是为了防止阈值过小导致肉眼不可见或阈值过大导致用户还能看到界面时就突然停止了。
animateTo() 使用 SpringSpec 时可以搭配初速度实现震动效果:
anim.animateTo(size,spring(Spring.DampingRatioHighBouncy, Spring.StiffnessVeryLow),2000.dp // 初始速度
)
5、RepeatableSpec
RepeatableSpec 用于重复播放动画,我们直接看它的简便方法 repeatable():
/**
* 创建一个 RepeatableSpec,用于播放基于时间的 DurationBasedAnimationSpec(例如 TweenSpec、
* KeyframesSpec),并按照 iterations 指定的次数重复播放。
* 迭代次数描述了动画将运行的次数。1 表示不重复。如果需要创建无限重复的动画,建议使用 infiniteRepeatable。
*
* 注意:在 RepeatMode.Reverse 模式下重复时,强烈建议使用奇数的迭代次数。否则,动画
* 在完成最后一次迭代时可能会跳转到结束值。
*
* initialStartOffset 可用于延迟动画的开始或将动画快进到给定的播放时间。此起始偏移量不会重复,
* 而动画中的延迟(如果有)将会重复。默认情况下,偏移量为 0。
*/
@Stable
fun <T> repeatable(// 总迭代次数,应大于 1 以实现重复iterations: Int,// 需要重复的动画animation: DurationBasedAnimationSpec<T>,// 动画重复的方式,是从头开始(即 RepeatMode.Restart)还是从末尾开始(即 RepeatMode.Reverse)repeatMode: RepeatMode = RepeatMode.Restart,// 动画的起始偏移量initialStartOffset: StartOffset = StartOffset(0)
): RepeatableSpec<T> =RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)
参数讲解:
- iterations:总迭代次数,就是动画播放的次数,大于 1 才能重复播放动画,填 1 不重复,填 0 报错
- animation:需要重复的动画,类型必须是 DurationBasedAnimationSpec,也就是其子类的 TweenSpec、SnapSpec 以及 KeyframesSpec 之一
- repeatMode:动画重复的方式,有两种选择,从头开始(即 RepeatMode.Restart)还是从末尾开始(即 RepeatMode.Reverse),需要注意如果是 RepeatMode.Reverse 的话,iterations 必须是一个奇数,否则,动画在完成最后一次迭代时可能会跳转到结束值
- initialStartOffset:动画起始偏移量,这个是时间上的偏移量,而不是位置上的偏移量
解释一下为什么 repeatMode 是 RepeatMode.Reverse 时 iterations 必须是奇数。比如动画的初始值是 48,目标值是 96,Reverse 模式下 iterations 为 2。那么 48 -> 96 是第一次,第二次倒放就是 96 -> 48,由于我们是通过 animateTo() 指定目标值并展示动画,因此动画结束时必须在目标值 96 上,因此这里会有一个 48 直接跳到 96 的变化,这与设定的倒放 2 次最终应该处于 48 的结果不符。
一种解决方案是使用 KeyframesSpec 设置起始值和目标值都为 48,中间放一个关键帧,值为 96,这样就做出了上面那种 48 -> 96 -> 48 的重复效果了。
最后再详细说一下 initialStartOffset,其类型是 StartOffset,主构造被设置成 private 的,次构造提供了两个参数:
@kotlin.jvm.JvmInline
value class StartOffset private constructor(internal val value: Long) {/*** 为 [repeatable] 和 [infiniteRepeatable] 创建一个起始偏移量。[offsetType] 可以是以下两种之一:* [StartOffsetType.Delay] 和 [StartOffsetType.FastForward]。[offsetType] 默认为 * [StartOffsetType.Delay]** [StartOffsetType.Delay] 会将动画的开始延迟 [offsetMillis] 指定的时间,而* [StartOffsetType.FastForward] 会立即从动画的 [offsetMillis] 处开始播放动画。*/constructor(offsetMillis: Int, offsetType: StartOffsetType = StartOffsetType.Delay) : this((offsetMillis * offsetType.value).toLong())
}
第一个参数就是以毫秒为单位的偏移量,第二个参数 offsetType 有两种类型可选:
- Delay:延时模式,即为动画设置启动延时,在 offsetMillis 这么长时间后再开始播放动画
- FastForward:立即播放模式,跳过前 offsetMillis 毫秒的动画,快进到 offsetMillis 这个时间点开始播放后续动画
6、InfiniteRepeatableSpec
前面讲到的 DurationBasedAnimationSpec、RepeatableSpec 以及 SpringSpec 是 FiniteAnimationSpec 接口的子类或子接口,而接下来要介绍的 InfiniteRepeatableSpec 与 FiniteAnimationSpec 是兄弟关系。
InfiniteRepeatableSpec 用于展示无限循环的动画,它与 RepeatableSpec 在本质上是没有区别的,只不过循环次数一个是有限的,一个是无限的,并且有限循环的 RepeatableSpec 可以计算出总的动画时长。
如果一个动画使用了 InfiniteRepeatableSpec,那动画何时会结束,这个过程中会涉及到内存泄漏或者性能损耗吗?
animateTo() 结束的时候,动画就结束了。因为 animateTo() 是挂起函数,它需要在 LaunchedEffect() 营造的协程环境中才能调用,当 LaunchedEffect() 结束时,就会让 animateTo() 结束。而 LaunchedEffect(key) 会在 key 发生变化时重新启动,当它重启时,上一次执行的 LaunchedEffect() 就会自动结束。
当 animateTo() 所在的协程结束时动画就会停止,不会有性能损耗或内存泄漏。
7、其他 Spec
AnimationSpec 接口一共有三个子类或子接口:FiniteAnimationSpec 和 InfiniteRepeatableSpec 我们已经讲过,还剩下一个 FloatAnimationSpec 接口:
@JvmDefaultWithCompatibility
interface FloatAnimationSpec : AnimationSpec<Float> {...}
这个接口把 AnimationSpec 的泛型类型直接实例化为 Float,因此它的两个子类 FloatTweenSpec 与 FloatSpringSpec 就是针对 Float 类型的动画规格了。
既然已经有了支持通用类型的 TweenSpec 和 SpringSpec,为什么还要单独为 Float 类型做对应的类呢?实际上 FloatTweenSpec 与 FloatSpringSpec 是为 Compose 更底层的动画计算做辅助工作的,不是给上层开发用的。因此,不要去使用这两个类。
在 AnimationSpec 之外,还有其他的 AnimationSpec 接口:
- VectorizedAnimationSpec:也是辅助底层动画计算的接口,它的继承树结构与 AnimationSpec 的十分相似。Vectorized 译为向量化,因为 Compose 底层计算动画都是要先统一转成向量,也就是密封类 AnimationVector 中的 AnimationVector1D ~ AnimationVector4D
- DecayAnimationSpec:消散型动画,马上会与 Animatable 的 animateDecay() 一起讲解