Android Compose 动画使用详解(七)动画配置之RepeatableSpec、InfiniteRepeatableSpec

1,773 阅读8分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

前面用三篇文章分别介绍了 Compose 动画规格的 TweenSpec(补间动画配置)、SnapSpec(快闪动画配置)、KeyframesSpec(关键帧动画配置)和 SpringSpec(弹簧动画配置),本篇将继续介绍 Compose 动画规格的最后两个配置:RepeatableSpec(重复动画配置)、InfiniteRepeatableSpec(无限重复动画配置)

RepeatableSpec

RepeatableSpec顾名思义是重复配置的意思,可以让动画进行重复执行,定义如下:

class RepeatableSpec<T>(
    val iterations: Int,
    val animation: DurationBasedAnimationSpec<T>,
    val repeatMode: RepeatMode = RepeatMode.Restart,
    val initialStartOffset: StartOffset = StartOffset(0)
) : FiniteAnimationSpec<T> 

RepeatableSpec构造方法有四个参数,对应参数解析如下:

  • iterations:重复次数,即让动画重复执行多少次,必填参数
  • animation:重复执行的目标动画配置,必填参数
  • repeatMode:重复模式,有两种可选模式:重新播放、反向播放,默认为重新播放
  • initialStartOffset:初始启动偏移,偏移的对象是动画时间,默认为 StartOffset(0)即不偏移

除了直接使用 RepeatableSpec 构造函数创建 RepeatableSpec 外,Compose 同样为我们提供了简便函数 repeatable 让开发者使用,参数与构造函数参数一致,下面就详细介绍各个参数的使用和效果。

iterations

iterationsRepeatableSpec必填参数之一,是动画重复执行的次数,很好理解,就是我们想让动画重复执行多少次,那么这个参数就传多少就行了,注意类型为 Int 类型,即只能为整数,如我们想让动画重复执行 5 次,代码使用如下:

val repeatableSpec = repeatable<Dp>(iterations = 5, animation = ...)

animation

animation也是 RepeatableSpec 必填参数之一,是重复执行的目标动画配置,即我们要对那个动画配置进行重复执行。

通过 RepeatableSpec 的定义发现 animationDurationBasedAnimationSpec类型,还记得在开始介绍动画配置时的那张继承关系图么?再来看一下:

DurationBasedAnimationSpec 只有三个实现子类,也就是我们最开始介绍的三个动画配置:TweenSpec(补间动画配置)、SnapSpec(快闪动画配置)、KeyframesSpec(关键帧动画配置)。

也就是说 animation 参数我们只能传TweenSpecSnapSpecKeyframesSpec这三种动画配置对象,而上一篇介绍的 SpringSpec(弹簧动画配置)、 现在正在介绍的RepeatableSpec本身以及后面介绍的 InfiniteRepeatableSpec(无限重复动画配置)都不能用于 RepeatableSpec 进行重复执行。

我们还是以我们最熟悉的方块动画来体验一下 RepeatableSpec 的使用效果,代码如下:

var moveToRight by remember { mutableStateOf(false) }
val targetValue = if(moveToRight) 200.dp else 10.dp

// 补间动画
val animation1 = tween<Dp>(durationMillis = 500)
// 快闪动画
val animation2 = snap<Dp>()
// 关键帧动画
val animation3 = keyframes<Dp> {
    50.dp at 150 with FastOutSlowInEasing
    150.dp at 200
}
// 添加重复配置,重复次数三次
val startPadding1 by animateDpAsState(targetValue, animationSpec = repeatable(3, animation1))
val startPadding2 by animateDpAsState(targetValue, animationSpec = repeatable(3, animation2))
val startPadding3 by animateDpAsState(targetValue, animationSpec = repeatable(3, animation3))

Column {
    // 应用重复补间动画
    Text("RepeatableSpec--TweenSpec", modifier = Modifier.padding(top = 20.dp, start = 10.dp))
    Box(
        Modifier
            .padding(start = startPadding1, top = 10.dp, bottom = 10.dp)
            .size(100.dp)
            .background(Color.Blue)
            .clickable {
                moveToRight = !moveToRight
            }
    )

    // 应用重复快闪动画
    Text("RepeatableSpec--SnapSpec", modifier = Modifier.padding(top = 20.dp, start = 10.dp))
    Box(
        Modifier
            .padding(start = startPadding2, top = 10.dp, bottom = 10.dp)
            .size(100.dp)
            .background(Color.Blue)
            .clickable {
                moveToRight = !moveToRight
            }
    )

    // 应用重复关键帧动画
    Text("RepeatableSpec--KeyframesSpec", modifier = Modifier.padding(top = 20.dp, start = 10.dp))
    Box(
        Modifier
            .padding(start = startPadding3, top = 10.dp)
            .size(100.dp)
            .background(Color.Blue)
            .clickable {
                moveToRight = !moveToRight
            }
    )
}

创建三个方块,使用RepeatableSpec分别作用于TweenSpecSnapSpecKeyframesSpec运行一下看看效果:

作用于TweenSpecKeyframesSpec的动画效果很明显,肉眼可见的确实重复执行了 3 次,但是中间的SnapSpec动画怎么好像只执行了一次呢?这是因为 SnapSpec 的特性,前面文章我们介绍了 SnapSpec 的动画时间是 0,所以无论重复执行多少次动画总时间还是 0,所以动画效果都是一瞬间执行完成了,看着像只执行了一次。

repeatMode

repeatMode是重复模式,值是枚举类型 RepeatMode,有两个枚举值:Restart重新播放和 Reverse反向播放,定义如下:

enum class RepeatMode {
    // 重新播放
    Restart,

    // 反向播放
    Reverse
}

TweenSpec为例看一下使用效果:

val spec = repeatable(3, animation, repeatMode = RepeatMode.Restart)
val sepc2 = repeatable(3, animation, repeatMode = RepeatMode.Reverse)

对同一个动画重复 3 次,重复模式分别设置RestartReverse,看一下运行效果:

可以发现区别还是很明显的,使用 Restart模式时在每次执行完后会瞬间回到起点再重新执行,效果就会很突兀;而使用 Reverse 模式时,在每次执行完后会反向执行,视觉体验会好很多。当然开发中应根据实际需求设置对应的重复模式。

需要注意的是在使用 Reverse反向模式且重复次数为双数时,动画运行效果可能不符合预期,还是上面的例子将重复次数从 3 修改为 2 看一下运行效果:

val sepc2 = repeatable(2, animation, repeatMode = RepeatMode.Reverse)

可以发现,因为重复次数设置的 2 ,所以动画会从起点运动到目标位置,然后再从目标位置反向回到起点,到这里是符合预期的,但是在动画最后一瞬间又直接回到的目标位置,看起来就比较突兀,按照正常逻辑重复次数设置为 2 那动画应该停在起点才对,这是为什么呢?

这是因为我们使用的是 animateXxxAsState 动画,他的作用就是将对象以动画形式运动到目标点,而我们之前介绍的包括 RepeatableSpec 在内的动画配置都是对动画进行修饰的,他们只能改变动画的过程而不能改变动画的结果,所以上面的动画最后会瞬间回到目标终点。

在使用 RepeatableSpec 且重复模式设置为 Reverse 时,需要注意重复次数设置为双数是否符合你的预期。

initialStartOffset

initialStartOffset是初始启动偏移,可以设置动画偏移一定的动画时长,值为 StartOffset类型。

StartOffset构造方法有两个参数:

  • offsetMillis:偏移的动画时间,单位毫秒
  • offsetType:偏移类型,有两个可选值:
    • Delay:延迟启动,即等待 offsetMillis时长后再启动动画,是offsetType 的默认值
    • FastForward:快进启动,即跳过动画的 offsetMillis 时长,再执行后面的动画

需要注意的是 initialStartOffset 针对的是整个动画的时长做偏移而不是每次重复的动画做偏移。

比如单个动画时长为 300ms ,重复执行 3 次,那整个动画时长为 900ms,offsetMillis 设置 300ms,offsetType设置不同的类型时动画表现不一样,如下:

  • offsetType 使用 Delay 时,整个动画时长为 offsetMillis( 300ms )+ 900ms = 1200ms
  • offsetType 使用 FastForward时,动画时长为 900ms - offsetMillis( 300ms ) = 600ms

代码及效果如下:

val spec = repeatable(3, animation, repeatMode = RepeatMode.Reverse, initialStartOffset = StartOffset(300, StartOffsetType.Delay))
val spec2 = repeatable(3, animation, repeatMode = RepeatMode.Reverse,  initialStartOffset = StartOffset(300, StartOffsetType.FastForward))

当为 Delay 时动画延迟了 300ms 后执行,且重复执行了 3 次;而当为 FastForward时,动画是立即执行,但是因为设置的 offsetMillis 为 300ms ,且单次动画时长也为 300ms,所以动画跳过了第一次动画直接到达了终点,然后再重复执行了 2 次。

InfiniteRepeatableSpec

InfiniteRepeatableSpec是无限重复动画,即无限循环动画,使用方式与参数基本与 RepeatableSpec 相同,唯一的区别是少一个 iterations 参数,因为是无限重复所以不需要设置重复次数。

除了构造方法以外 Compose 同样提供了简便函数 infiniteRepeatable供开发者使用,使用如下:

val spec = infiniteRepeatable( animation, repeatMode = RepeatMode.Reverse, initialStartOffset = StartOffset(300, StartOffsetType.Delay))
val startPadding1 by animateDpAsState(targetValue, animationSpec = )

效果如下:

启动后动画会无限重复的播放。

那如果我们想停止无限重复播放怎么办呢?可以修改动画状态然后根据状态对动画重新设置一个 Spec,比如上面的动画当 moveToRight 为 false 时将 animationSpec 设置为 SnapSpec就可以停止动画了,代码如下:

// 动画状态
var moveToRight by remember { mutableStateOf(false) }
val targetValue = if (moveToRight) 200.dp else 10.dp

// 无限重复动画配置
val infiniteRepeatableSpec = infiniteRepeatable<Dp>(
    tween(),
    repeatMode = RepeatMode.Reverse,
    initialStartOffset = StartOffset(300, StartOffsetType.Delay)
)

// 根据状态设置动画配置,当 moveToRight 为 false 时设置 snap
val animationSpec = if (moveToRight) infiniteRepeatableSpec else snap()
val startPadding by animateDpAsState(
    targetValue,
    animationSpec = animationSpec
)
Column {
    Box(
        Modifier
            .padding(start = startPadding, top = 20.dp, bottom = 10.dp)
            .size(100.dp)
            .background(Color.Blue)
    )
    // 添加按钮改变 moveToRight 值
    Button(
        onClick = { moveToRight = !moveToRight },
        modifier = Modifier.padding(start = 10.dp)
    ) {
        Text(if (moveToRight) "停止" else "开始")
    }
}

运行效果如下:

这样就实现了手动停止无限循环动画的功能。

最后

本篇详细介绍了RepeatableSpec(重复动画配置)、InfiniteRepeatableSpec(无限重复动画配置)的详细配置使用并展示了不同配置呈现的效果, 至此关于 Compose 动画的六种配置我们就介绍完了,从下一篇开始我们继续探索 Compose 动画更深入的使用,请持续关注本专栏了解更多 Compose 动画相关内容。