AnimationSpec动画规格详解

前言

动画的各种属性,比如动画时长、动画曲线、动画是否重复播放等,都是通过AnimationSpec来进行配置的。

本文会讲述各种AnimationSpec的特点、用法和应用场景。

概述

AnimationSpec是动画系统中的一个重要接口,比如animateDpAsStateanimateTo 函数都有一个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------瞬时切换的动画

SnapSpecAnimatablesnapTo()函数是一样的效果,使用它时,会立即从初始值变为目标值,没有任何的平滑过渡。就像拍照一样捕捉了最终状态,这也是为什么它被命名为"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的效果是重复执行动画。参数是其它类型的动画(如 TweenSpecKeyframesSpec 等)。

它也有简便函数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是有用的,但现在不讲。

相关推荐
Wgllss5 小时前
Kotlin 享元设计模式详解 和对象池及在内存优化中的几种案例和应用场景
android·架构·android jetpack
alexhilton1 天前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack
bytebeats3 天前
Jetpack Compose 1.9: 核心新特性简介
android·android jetpack
Wgllss3 天前
雷电雨效果:Kotlin+Compose+协程+Flow 实现天气UI
android·架构·android jetpack
alexhilton4 天前
深入浅出着色器:极坐标系与炫酷环形进度条
android·kotlin·android jetpack
bytebeats5 天前
Jetpack Compose 1.8 新增了 12 个新特性
android·android jetpack
MettBarr7 天前
Jetpack Lifecycle 的本质
android jetpack
刘龙超7 天前
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(五)完结
android jetpack
alexhilton8 天前
用Compose中的Shader实现一个雪花飘飘弹窗效果
android·kotlin·android jetpack
刘龙超8 天前
如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(四)登录注册
android jetpack