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是有用的,但现在不讲。

相关推荐
_一条咸鱼_6 小时前
Android ARouter 处理器模块深度剖析(三)
android·面试·android jetpack
_一条咸鱼_6 小时前
Android ARouter 基础库模块深度剖析(四)
android·面试·android jetpack
_一条咸鱼_6 小时前
Android ARouter 核心路由模块原理深度剖析(一)
android·面试·android jetpack
_一条咸鱼_6 小时前
Android ARouter 编译器模块深度剖析(二)
android·面试·android jetpack
木子予彤3 天前
Compose Side Effect(附带效应)
android·android jetpack
雨白3 天前
Modifier.composed() 和 ComposedModifier
android jetpack
_一条咸鱼_3 天前
大厂Android面试秘籍:上下文管理模块
android·面试·android jetpack
Wgllss4 天前
Android监听开机自启,是否在前后台,锁屏界面,息屏后自动亮屏,一直保持亮屏
android·架构·android jetpack
_一条咸鱼_4 天前
大厂Android面试秘籍:Activity 组件间通信
android·面试·android jetpack