本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
上一篇介绍了 TweenSpec
补间动画规格的配置和使用,本篇继续介绍关于动画规格的 SnapSpec
和 KeyframesSpec
的配置和使用。
SnapSpec
SnapSpec
为快闪动画规格,何为快闪呢?即动画从当前状态瞬间变化为目标状态,简单的说其实就是没有动画效果。
既然是没有动画效果那为什么还要使用动画再配置SnapSpec
呢?直接修改组件对应的值不就行了么?确实是这样,如果只是单一的使用 SnapSpec
配置动画跟直接修改组件对应值达到的效果是一样的,但是有时候在动画有多个状态时可能需要某一个状态的变化是没有动画效果,此时就可以使用 SnapSpec
来实现。
使用 SnapSpec
之前先看一下其定义:
class SnapSpec<T>(val delay: Int = 0) : DurationBasedAnimationSpec<T>
SnapSpec
构造函数只有一个 delay
参数,即动画的延迟时间,默认为 0。
使用:
val startPadding by animateDpAsState(targetValue, animationSpec = SnapSpec(1000))
同样的 Compose 也为 SnapSpec
提供了一个简便函数 snap
简化其使用,如下:
val startPadding by animateDpAsState(targetValue, animationSpec = snap(1000))
效果:
那在什么情况下会使用到 SnapSpec
呢?举个简单的例子,还是以上面的动画为例,假如需求是点击方块时方块以动画效果向右移动到目标位置,再点击方块时方块直接回到起点,此时就可以借助SnapSpec
来实现,代码如下:
var moveToRight by remember { mutableStateOf(false) }
val targetValue = if(moveToRight) 100.dp else 10.dp
val spec = if(moveToRight) tween<Dp>(1000) else snap<Dp>()
val startPadding by animateDpAsState(targetValue, animationSpec = spec)
运行效果:
KeyframesSpec
KeyframesSpec
是关键帧动画,即可以对动画的关键帧进行配置。那到底能对动画的关键帧进行哪些配置呢?还是先来看一下KeyframesSpec
的定义:
class KeyframesSpec<T>(val config: KeyframesSpecConfig<T>) : DurationBasedAnimationSpec<T>
构造方法只有一个参数 config
类型为 KeyframesSpecConfig
,看看 KeyframesSpecConfig
的源码:
class KeyframesSpecConfig<T> {
/**
* Duration of the animation in milliseconds. The minimum is `0` and defaults to
* [DefaultDurationMillis]
*/
/*@IntRange(from = 0)*/
var durationMillis: Int =
/**
* The amount of time that the animation should be delayed. The minimum is `0` and defaults
* to 0.
*/
/*@IntRange(from = 0)*/
var delayMillis: Int = 0
internal val keyframes = mutableMapOf<Int, KeyframeEntity<T>>()
/**
* Adds a keyframe so that animation value will be [this] at time: [timeStamp]. For example:
* 0.8f at 150 // ms
*
* @param timeStamp The time in the during when animation should reach value: [this], with
* a minimum value of `0`.
* @return an [KeyframeEntity] so a custom [Easing] can be added by [with] method.
*/
// TODO: Need a IntRange equivalent annotation
infix fun T.at(/*@IntRange(from = 0)*/ timeStamp: Int): KeyframeEntity<T> {
return KeyframeEntity(this).also {
keyframes[timeStamp] = it
}
}
/**
* Adds an [Easing] for the interval started with the just provided timestamp. For example:
* 0f at 50 with LinearEasing
*
* @sample androidx.compose.animation.core.samples.KeyframesBuilderWithEasing
* @param easing [Easing] to be used for the next interval.
*/
infix fun KeyframeEntity<T>.with(easing: Easing) {
this.easing = easing
}
override fun equals(other: Any?): Boolean {
return other is KeyframesSpecConfig<*> && delayMillis == other.delayMillis &&
durationMillis == other.durationMillis && keyframes == other.keyframes
}
override fun hashCode(): Int {
return (durationMillis * 31 + delayMillis) * 31 + keyframes.hashCode()
}
}
源码中定义了三个属性:
- durationMillis:动画时长
- delayMillis:动画延迟
- keyframes:关键帧集合
动画时长、延时
durationMillis
、 delayMillis
这两个属性我们通过上一篇文章已经很熟悉了,跟 TweenSpec
的前两个参数一样,在 KeyframesSpec
中使用方式如下:
// 创建 KeyframesSpecConfig
val config = KeyframesSpec.KeyframesSpecConfig<Dp>()
// 设置动画时长
config.durationMillis = 1000
// 设置动画延时
config.delayMillis = 1000
// 创建 KeyframesSpec
val spec = KeyframesSpec<Dp>(config)
// 使用 KeyframesSpec
val startPadding by animateDpAsState(targetValue, animationSpec = spec)
上面的代码看着很繁琐,可以使用 kotlin 的特性简化上面的代码:
val spec = KeyframesSpec<Dp>(KeyframesSpec.KeyframesSpecConfig<Dp>().apply {
durationMillis = 1000
delayMillis = 1000
})
val startPadding by animateDpAsState(targetValue, animationSpec = spec)
代码简洁多了,但是还不够简洁,前面介绍 TweenSpec
和 SnapSpec
时发现 Compose 都为其提供了简便的方法 tween
和 snap
供开发者使用,同样的 Compose 也为 KeyframesSpec
提供了keyframes
方法方便大家使用,实际上 keyframes
方法就是对上面代码的封装,源码如下:
fun <T> keyframes(
init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit
): KeyframesSpec<T> {
return KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<T>().apply(init))
}
使用方式如下:
val spec = keyframes<Dp> {
durationMillis = 1000
delayMillis = 1000
}
这样使用起来就更加简洁方便了,运行一下看看效果:
关键帧值
如果只是配置这两个属性,其实跟使用 TweenSpec
的效果没什么区别,而 KeyframesSpec
的关键就在于 keyframes
,那 keyframes
怎么用呢?通过源码发现keyframes
是一个 Map 类型 mutableMapOf<Int, KeyframeEntity<T>>
key 是 Int 类型,表示当前关键帧在动画中的哪个时间点,value 为 KeyframeEntity
为关键帧的动画配置, KeyframeEntity
定义如下:
class KeyframeEntity<T> internal constructor(
internal val value: T,
internal var easing: Easing = LinearEasing
)
其中 value 为动画在当前帧的值,而 easing 则是动画速率曲线,相信看了前一篇文章的同学一定不会陌生。
那么 keyframes
怎么用呢?直接向 Map 里添加对应的 key-value 么?好像不行,源码中 keyframes
属性被定义为internal
,即在 module 外无法调用,那该怎么添加这个关键帧呢?回到上面 KeyframesSpecConfig
的源码,除了三个属性以外还有一个 at
方法:
infix fun T.at(/*@IntRange(from = 0)*/ timeStamp: Int): KeyframeEntity<T> {
return KeyframeEntity(this).also {
keyframes[timeStamp] = it
}
}
该方法是一个扩展方法,其中 T
就是动画作用的数值类型,参数 timeStamp
为动画帧的时间,然后创建一个 KeyframeEntity
实例,value 为动画值,并将创建的 KeyframeEntity
添加到 keyframes
的 Map 中, key 为传入的动画时间,所以可以通过这个 at
方法来实现关键帧的配置,使用如下:
val targetValue = if(moveToRight) 100.dp else 10.dp
val spec = keyframes<Dp> {
durationMillis = 1000
delayMillis = 1000
// 关键帧设置
50.dp.at(300)
})
val startPadding by animateDpAsState(targetValue, animationSpec = spec)
上面的代码设置了当动画时间为 300ms 的关键帧时动画的数值为 50.dp
,看一下运行效果:
效果确实跟我们设置的一样,符合预期。
细心的同学会发现其实 at
方法是一个使用 infix
修饰的中缀函数,我们可以使用更简单的方法进行调用,如下:
val spec = keyframes<Dp> {
...
// 关键帧设置
50.dp at 300
})
这样让使用更加简单好理解,代表动画数值达到 50.dp 时的动画时间为 300ms。
多个关键帧
上面介绍了如何添加关键帧并设置关键帧的动画值,那么是否可以设置多个关键帧呢?答案是肯定的, keyframes
本身就是一个 Map 类型,可以为其添加多个 key-value 值。
前面设置了关键帧在 300ms 时动画值为 50.dp ,如果想再设置一个在 600ms 时动画值为 150.dp 的关键帧,代码实现如下:
var moveToRight by remember { mutableStateOf(false) }
val targetValue = if(moveToRight) 100.dp else 10.dp
val spec = keyframes<Dp> {
durationMillis = 1000
50.dp at 300
150.dp at 600
}
val startPadding by animateDpAsState(targetValue, animationSpec = spec)
运行一下看看效果:
因为动画的目标值为 100.dp 但是又在 600ms 时设置了关键帧为 150.dp,所以动画会出现回退的现象。
动画速率
KeyframeEntity
还有个 easing 属性,即动画速率曲线又该怎么设置呢?KeyframesSpecConfig
提供了一个 with
方法,源码如下:
infix fun KeyframeEntity<T>.with(easing: Easing) {
this.easing = easing
}
给 KeyframeEntity
添加了一个 with 的扩展方法传入 easing 参数,在方法实现里直接将参数 easing 赋值给 KeyframeEntity
的 easing 属性。既然是 KeyframeEntity
的扩展方法,那就需要通过KeyframeEntity
来进行调用,那么KeyframeEntity
哪里来呢?巧了,前面的 at
方法返回的不就是KeyframeEntity
类型吗,所以可以在 at 方法后接着使用 with 方法来设置动画速率曲线。而 with 方法也是一个通过 infix
修饰的中缀函数,所以可以如下使用:
val spec = keyframes<Dp> {
durationMillis = 1000
// 关键帧设置
50.dp at 300 with FastOutSlowInEasing
})
通过上面的代码我们就设置了一个 FastOutSlowInEasing
的动画速率曲线,既然是一个速率曲线,那肯定是对一个时间段的动画生效,但是这里只是设置的一个关键帧,那这个速率曲线是为哪段动画生效呢?是关键帧的前一段还是后一段呢?答案是后一段,以上面的例子为例,这里设置的是 300 ~ 1000ms 的动画时段的速率曲线。
那如果需要对前一段动画设置动画速率曲线怎么办呢?可以在前一段的起点位置再添加一个关键帧的配置,还是以上面代码为例,如果需要对 0 ~ 300ms 的动画时段设置速率曲线,可以添加如下关键帧:
val spec = keyframes<Dp> {
durationMillis = 1000
// 添加起始关键帧并设置速率曲线
10.dp at 0 with FastOutLinearInEasing
// 关键帧设置
50.dp at 300 with FastOutSlowInEasing
})
如果添加了关键帧不设置 easing 参数,即后面不添加 with 设置速率曲线则使用 LinearEasing
作为默认速率曲线。
缺陷
KeyframesSpec
有一个不是缺陷的缺陷,那就是不能反着执行,实际上这并不是KeyframesSpec
的缺陷,而是其特性如此,比如下面的动画代码:
var moveToRight by remember { mutableStateOf(false) }
val targetValue = if(moveToRight) 100.dp else 10.dp
val spec = keyframes<Dp> {
// 动画时长 1000ms
durationMillis = 1000
// 关键帧1:300ms 时动画值为 50.dp
50.dp at 300
// 关键帧2:600ms 时动画值为 150.dp
150.dp at 600
}
val startPadding by animateDpAsState(targetValue, animationSpec = spec)
Box(
Modifier
.padding(start = startPadding, top = 10.dp)
.size(100.dp, 100.dp)
.background(Color.Blue)
.clickable {
moveToRight = !moveToRight
}
)
为动画创建了 2 个关键帧,当 moveToRight 的状态从 false 变为 true 时动画效果是符合预期的,当 moveToRight 状态从 ture 变为 false 时我们期望的动画效果应该是之前的反向执行效果,但是实际运行效果却并不是这样,来看一下反向的运行的效果:
可以发现动画的执行是先从 100.dp 动画到 50.dp 然后再变为 150.dp 最后回到 10.dp ,为什么会这样呢?是因为反向执行的时候还是运用的两个相同的关键帧,即 300ms 时动画值为 50.dp、600ms 时动画值为 150.dp,且反向时动画的目标值为 10.dp 所以最后又会从 150.dp 动画到 10.dp。
那么如果要实现真正的反向效果应该怎么办呢?答案是重新定义一个反向的 spec,如下:
...
// 正向 sepc
val spec = keyframes<Dp> {
durationMillis = 1000
50.dp at 300
150.dp at 600
}
// 反向 spec
val reverseSpec = keyframes<Dp> {
durationMillis = 1000
// 正向为 150.dp 时动画时间为 600ms 而总时长为 1000ms 即距离终点 400ms,
// 所以反向时 150.dp 位置应该为 400ms
150.dp at 400
// 正向为 50.dp 时动画时间为 300ms 即距离终点 700ms,
// 所以反向时 50.dp 位置应该为 700ms
50.dp at 700
}
val targetSpec = if(moveToRight) spec else reverseSpec
val startPadding by animateDpAsState(targetValue, animationSpec = targetSpec)
...
这样就实现了真正的反向动画,效果如下:
DurationBasedAnimationSpec
DurationBasedAnimationSpec
是一个基于动画时长配置的基类,上一篇以及本篇介绍的TweenSpec
、SnapSpec
和KeyframesSpec
都是实现自 DurationBasedAnimationSpec
接口。
他们有如下特点:
- 都有明确的动画时长,
TweenSpec
和KeyframesSpec
是可以设置动画时长,SnapSpec
的动画时长为 0 - 都有
delayMillis
参数可以设置动画延时 KeyframesSpec
实际上就是多段的TweenSpec
最后
到目前为止我们了解了TweenSpec
、SnapSpec
和KeyframesSpec
的动画配置,6 种动画配置已经探索了一半,下一篇我们将继续探索其他动画配置的使用,请持续关注本专栏了解更多 Compose 动画相关内容。