前面我们聊过 animateDpAsState
和 Animatable
两种动画 API 的用法,但只是简单了解了,这两个函数内部都有一个共同的核心参数:AnimationSpec。
@Composable
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring, // AnimationSpec
label: String = "DpAnimation",
finishedListener: ((Dp) -> Unit)? = null
)
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null,
val label: String = "Animatable"
) {
suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec, // AnimationSpec
initialVelocity: T = velocity,
block: (Animatable<T, V>.() -> Unit)? = null
)
}
这个 AnimationSpec 是什么?有什么用?
官方的说法:你可以通过可选的 AnimationSpec 参数来自定义动画规范(也就是可以实现不同类型的动画效果)。
AnimationSpec 是一个接口,有很多实现类:
这些 **Spec
就可以帮助你自定义出各种动画效果。
现在再回头看一下 animateDpAsState
和 Animatable.animateTo
的 AnimationSpec 参数:
animateDpAsState:
-> animationSpec: AnimationSpec<Dp> = dpDefaultSpring
animateTo:
-> animationSpec: AnimationSpec<T> = defaultSpringSpec
他们默认都是一个 SpringSpec 的弹簧效果,而且默认是不回弹。
@Immutable
class SpringSpec<T>(
val dampingRatio: Float = Spring.DampingRatioNoBouncy,
val stiffness: Float = Spring.StiffnessMedium,
val visibilityThreshold: T? = null
)
/**
* Damping ratio for a spring with no bounciness. This damping ratio will create a
* critically damped spring that returns to equilibrium within the shortest amount of time
* without oscillating.
*/
const val DampingRatioNoBouncy = 1f
了解到这里,你应该就清楚 AnimationSpec 有什么用了,接下来我们就会挨个研究 AnimationSpec 那么多实现类的具体动画效果,走起~
AnimationSpec
先看下 AnimationSpec 有哪些实现类:
📓 TweenSpec
TweenSpec:应该算是最简单的一类 Spec 了,它在指定的 durationMillis 内使用缓和曲线在起始值和结束值之间添加动画效果。
先来看一下它的构造函数:
@Immutable
class TweenSpec<T>(
val durationMillis: Int = DefaultDurationMillis,
val delay: Int = 0,
val easing: Easing = FastOutSlowInEasing
)
它有三个参数:
- durationMillis: 动画时长,不填的话默认 300ms
- delay:动画启动延时,默认不延时
- easing:Easing 类型,设置动画曲线,默认是 FastOutSlowInEasing 效果
除了 FastOutSlowInEasing,Compose 还提供了三个默认的动画曲线:
// 默认曲线:先加速再减速
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 }
按个试下效果:
LinearEasing:匀速运动
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, TweenSpec(easing = LinearEasing))
}
Box(
Modifier
.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
FastOutSlowInEasing:先加速再减速(默认曲线,我们可以不填)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, TweenSpec())
}
Box(
Modifier
.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
LinearOutSlowInEasing:全程减速
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, TweenSpec(easing = LinearOutSlowInEasing))
}
Box(
Modifier
.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
FastOutLinearInEasing:全程加速
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, TweenSpec(easing = FastOutLinearInEasing))
}
Box(
Modifier
.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
录制的 Gif 图效果真不咋地,这几种效果还是建议你自己试一遍吧。
但有点 low 啊,就这四个默认曲线完全满足不了我的需求,在实际开发中,我们往往需要的动画曲线各种各样,而 UX 设计师也会提供给我们一些设计好的动画曲线,比如:(0.0,0.0,0.1,1.0),(0.2,0.1,0.0,1.0)。
再来看一下之前的动画曲线:
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)
他们都用一个 CubicBezierEasing
包起来了,它有四个参数,这四个参数就是定义三阶贝塞尔曲线的。
三阶贝塞尔曲线有四个点,但其中有两个点是固定的,分别是:(0, 0) 和 (1, 1),我们只需要给出另外两个点即可定义出三阶贝塞尔曲线。
不懂的话,给你一个网站:
这个就是一个可以看到三阶贝塞尔曲线定制效果的工具网站,其中两个白点就是固定的 (0, 0) 和 (1, 1),而红色和绿色两个点就对应我们动画曲线的四个参数,即 2 个坐标。
你可以随意拖动红点和蓝点,上方的坐标参数会同步更新,所以,UX 设计师提供给我们的坐标点,就可以在这个网站测试动画效果。
- 横坐标:时间完成度,纵坐标:动画完成读
- 曲线越陡峭,速度越快,曲线越平缓,速度越慢。
我们来看下 Compose 提供的几个动画曲线的效果:
// 先加速后减速
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)
这里的效果要比我们之前 Gif 的图效果要好不少。
TweenSpec 的用法和核心知识点就这么多,我猜你应该知道怎么用 TweenSpec 定制动画了。
最后讲个小的技巧,Compose 提供了一个 tween
函数简化写法,等同于 TweenSpec
,它内部就是调用 TweenSpec。
@Stable
fun <T> tween(
durationMillis: Int = DefaultDurationMillis,
delayMillis: Int = 0,
easing: Easing = FastOutSlowInEasing
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)
所以以下写法随便你写哪个:
anim.animateTo(size, TweenSpec(easing = FastOutLinearInEasing))
anim.animateTo(size, tween(easing = FastOutLinearInEasing))
📓 SnapSpec
讲完 TweenSpec,我们再来看一个靠着它的 SnapSpec。
snap 是一个特殊的 AnimationSpec,它会立即将值切换到结束值。
我不知道你还有没有印象,在前面讲解 Animatable 的时候,我们提高过一个 snapTo
的函数,它可以指定目标值突变到某一个值,再来回顾下吧:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.snapTo(if (big) 192.dp else 0.dp)
anim.animateTo(size)
}
Box(Modifier.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
我们添加了一句:anim.snapTo(if (big) 192.dp else 0.dp)
。
- Box 由小变大时,size 会瞬间到 0dp,然后从 0 -> 48dp
- Box 由大变小时,size 会瞬间到 192.dp,然后从 192.dp -> 96dp
看下效果:
而 SnapSpec 其实跟它是差不多的意思,就是让动画效果突变,瞬间完成。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, SnapSpec())
}
Box(
Modifier
.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
看下效果:
但 SnapSpec() 还有个参数,可以指定 delayMillis 来延迟动画播放的开始时间。
class SnapSpec<T>(val delay: Int = 0) : DurationBasedAnimationSpec<T>
比如我们添加一个 2s 的延时:
跟 tween
一样,Compose 也提供了一个 SnapSpec
的简化写法:snap
。
anim.animateTo(size, SnapSpec())
anim.animateTo(size, snap())
📓 KeyframesSpec
讲完 TweenSpec、SnapSpec,我们再来看一个靠着它俩的 KeyframesSpec。
KeyframesSpce 就是关键帧的意思,可以让我们在动画过程中选取几个关键的时间点,并给出这些时间点对应的动画完成度,相当于分段式的 TweenSpec。
在讲解 KeyframesSpce 的使用方法之前,有个细节需要说一下:如果我们用它的构造函数来创建的话会比较麻烦,需要像下面这样写:
anim.animateTo(size, KeyframesSpec<Dp>(KeyframesSpec.KeyframesSpecConfig()).apply {
})
很长很啰嗦,Compose 同样提供了简便的 keyframes
来创建 KeyframesSpec:
@Stable
fun <T> keyframes(
init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit
): KeyframesSpec<T> {
return KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<T>().apply(init))
}
所以我们通常会这么写:
anim.animateTo(size, keyframes { })
现在我们可以写内部代码了:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, keyframes {
144.dp at 150
})
}
Box(
Modifier
.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
我们在 keyframes{}
内部加了一个 144.dp at 150
,这一行代码是什么含义?注意,这里有个坑。
我之前一直以为是这么个意思:
- 放大过程:动画从 0ms 开始,也就是 48.dp 开始慢慢增大,到 150ms 的时候,相当于走了默认时长 300ms 的一半,这个时候瞬间到达144.dp,然后剩下来的 150ms,从 144.dp 降到 96.dp。
- 缩小过程:动画从 0ms 开始,也就是 96.dp 开始慢慢减小,到 150ms 的时候,相当于走了默认时长 300ms 的一半,这个时候瞬间回到 144.dp,然后剩下来的 150ms,从 144.dp 降到 48.dp。
这个理解对吗?我们先看下实际效果:
不对劲啊,感觉动画效果完全不是我刚才说的那个流程。
实际上,KeyframesSpce 添加关键帧,是把动画给拆分了,就像前面说的,它相当于分段式的 TweenSpec。
正确的流程应该是:
- 放大过程:0ms - 150ms:48dp -> 144dp,150ms - 300ms: 144dp -> 96dp
- 缩小过程:0ms - 150ms:96dp -> 144dp,150ms - 300ms: 144dp -> 48dp
这个流程就和动画是一样的了。
我们再来加一个关键帧:
anim.animateTo(size, keyframes {
144.dp at 150
70.dp at 250
})
我们推理一下流程:
- 放大过程:0ms - 150ms:48dp -> 144dp,150ms - 250ms: 144dp -> 70dp,250ms:70dp -> 96dp,会是:放大 -> 缩小 -> 放大的效果
- 缩小过程:0ms - 150ms:96dp -> 144dp,150ms - 250ms: 144dp -> 70dp,250ms:70dp -> 48dp,会是:放大 -> 缩小 -> 缩小的效果
看下运行效果:
正如我们推理的流程,但是缩小的动画太不明显了,因为动画总时长就300ms,几乎是一瞬间的事,那如果我把动画时长设置的长一点呢?
anim.animateTo(size, keyframes {
144.dp at 150
70.dp at 250
durationMillis = 1000
})
我们可以设置 durationMillis
参数,手动控制动画时长,再来看一下效果:
这么看就很明显了,缩小的时候,第一阶段缩小很快,第二阶段缩小很慢。
另外 KeyframesSpce 也可以设置延迟:
anim.animateTo(size, keyframes {
144.dp at 150
70.dp at 250
durationMillis = 1000
delayMillis = 1500
})
讲到这里,基本上了解了 KeyframesSpce 怎么用了吧?它跟 TweenSpec 基本上一样,但是我们讲 TweenSpec 的时候,有一个重点知识就是:动画曲线,不同动画曲线速度是不一样的。
那 KeyframesSpce 呢?它能设置动画曲线吗???
也是可以的,比如:
anim.animateTo(size, keyframes {
144.dp at 150 with FastOutSlowInEasing
70.dp at 250
durationMillis = 1000
delayMillis = 1500
})
KeyframesSpace 是分段动画,可以给每段动画单独指定一个动画曲线,但是问题就来了:
144.dp at 150 with FastOutSlowInEasing
70.dp at 250
FastOutSlowInEasing
这个先加速后减速的动画曲线到底是 150ms 之前的动画还是 150ms 之后的动画?- - 之后的动画,也就是 150ms - 250ms 之间的动画。
那 150ms 之前想设置动画曲线怎么办?
anim.animateTo(size, keyframes {
48.dp at 0 with FastOutLinearInEasing
144.dp at 150 with FastOutSlowInEasing
70.dp at 250
durationMillis = 1000
delayMillis = 1500
})
就这么简单,再加一个动画从 0ms 开始的动画曲线,不过要注意:这个48dp只是一个示例,因为如果是从放大到缩小,这边就应该写出 96dp,所以根据实际情况,你要写对动画的初始值。
📓 SpringSpec
还记得我们刚开始讲动画的时候提到过 animateTo
有一个默认的动画效果吗?
suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T, V>.() -> Unit)? = null
)
internal val defaultSpringSpec: SpringSpec<T> =
SpringSpec(visibilityThreshold = visibilityThreshold)
SpringSpec
就是默认的动画:弹簧效果。
你要创建一个弹簧效果,代码写起来也很简单:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val size = remember(big) { if (big) 96.dp else 48.dp }
val anim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(big) {
anim.animateTo(size, spring())
}
Box(
Modifier
.size(anim.value)
.background(Color.Blue)
.clickable {
big = !big
})
}
}
}
如代码里面写的:Compose 也提供了一个 spring 快捷方法,用于创建 SpringSpec。就这样,啥也不用填就可以自定义一个弹窗动画了。
那我如果要定制更复杂一点的弹簧动画呢?那就看看 SpringSpec
构造函数有哪些参数呗:
@Immutable
class SpringSpec<T>(
val dampingRatio: Float = Spring.DampingRatioNoBouncy,
val stiffness: Float = Spring.StiffnessMedium,
val visibilityThreshold: T? = null
)
dampingRatio:阻尼比
**一句话解释:**就是弹簧弹的过程中会受到多大的阻力,默认值是:DampingRatioNoBouncy = 1f,没有任何弹簧效果。
DampingRatioNoBouncy = 1f
anim.animateTo(size, spring())
DampingRatioNoBouncy = 0.1f
anim.animateTo(size, spring(dampingRatio = 0.1f))
anim.animateTo(size, spring(dampingRatio = 10f))
stiffness: 刚度
一句话解释: 弹簧有多硬,越硬回弹越猛,默认值是:StiffnessMedium = 1500f。
DampingRatioNoBouncy = 0.1f, StiffnessMedium = 1500f
anim.animateTo(size, spring(dampingRatio = 0.1f, stiffness = 1500f))
DampingRatioNoBouncy = 0.1f, StiffnessMedium = 10_000f
anim.animateTo(size, spring(dampingRatio = 0.1f, stiffness = 1500f))
DampingRatioNoBouncy = 0.1f, StiffnessMedium = 400f
anim.animateTo(size, spring(dampingRatio = 0.1f, stiffness = 1500f))
visibilityThreshold: 可视阈值
一句话解释: 如果你把 DampingRatioNoBouncy 设置的特别特别小,它会一直弹,那么我们就可以通过设置 visibilityThreshold 阈值,让弹窗到达这个阈值后直接停止。
anim.animateTo(size, spring(dampingRatio = 0.05f, stiffness = 1500f, 10.dp))
支持,SpringSpec 弹窗动画效果也讲完了。
📓 RepeatableSpec
RepeatableSpec 主要用于反复运行基于时长的动画(例如 tween 或 keyframes),直至达到指定的迭代计数。
看下它的构造函数你就懂了:
@Stable
fun <T> repeatable(
iterations: Int,
animation: DurationBasedAnimationSpec<T>,
repeatMode: RepeatMode = RepeatMode.Restart,
initialStartOffset: StartOffset = StartOffset(0)
): RepeatableSpec<T> =
RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)
- iterations:迭代参数,就是要重复的次数,填 1 就是不重复
anim.animateTo(size, repeatable(3))
- animation:需要传入一个 DurationBasedAnimationSpec 类型的动画,哪些动画是 DurationBasedAnimationSpec 类型的?
anim.animateTo(size, repeatable(3, tween()))
这样就可以运行了:
- repeatMode:重复模式,重启还是倒放
anim.animateTo(size, repeatable(3, tween(), RepeatMode.Reverse)) // 倒放
注意:如果你设置了 repeatMode 为倒放,那么 iterations 迭代次数不能设置成双数,只能单数,比如:
anim.animateTo(size, repeatable(2, tween(), RepeatMode.Reverse))
动画由目标值,比如从小到大,倒放次数为2,会是这样的流程:
动画开始:48.dp -> 96.dp(第一次),96.dp -> 48.dp(倒放),但是目标值是96dp,所以又会瞬间回到 96,这样效果就不对了。
- initialStartOffset:初始启动偏移(时间偏移),默认值为0,即不偏移。
anim.animateTo(size, repeatable(3, tween(), RepeatMode.Reverse, StartOffset(1000)))
不过 StartOffset 还有第二个参数,可以设置是“延时型偏移”还是“快进型偏移”。
anim.animateTo(size, repeatable(3, tween(), RepeatMode.Reverse,
StartOffset(1000, StartOffsetType.Delay)))
效果应该是延时 1s 后再运行动画:(放大)大 -> 小 -> 大,(缩小)小 -> 大 -> 小
anim.animateTo(size, repeatable(3, tween(), RepeatMode.Reverse,
StartOffset(1000, StartOffsetType.FastForward)))
效果应该是快进 1s 后再运行动画:(放大) 小 -> 大,(缩小) 大 -> 小
至此,RepeatableSpec 也讲完了。
📓 InfiniteRepeatableSpec
InfiniteRepeatableSpec 与 RepeatableSpec 类似,唯一的区别就是:它会重复无限次的迭代。
class InfiniteRepeatableSpec<T>(
val animation: DurationBasedAnimationSpec<T>,
val repeatMode: RepeatMode = RepeatMode.Restart,
val initialStartOffset: StartOffset = StartOffset(0)
)
LaunchedEffect(big) {
anim.animateTo(size, infiniteRepeatable(tween(), RepeatMode.Reverse,
StartOffset(1000, StartOffsetType.Delay))
)
}
当协程取消后,动画即可停止。
至此,所有的 AnimationSpec 就全部讲完了。