Compose Compose动画之AnimateSpec
- animateXXXAsState的使用
- Animatable
- 几种常用的AnimationSpec
animateXXXAsState的使用
举个栗子:实现一个红色方块,对它进行动画式的放大和缩放
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
// big 为 true,目标值是 96.dp;否则为 48.dp
val size by animateDpAsState(if (big) 96.dp else 48.dp)
Box(
Modifier
.size(size)
.background(Color.Red)
.clickable {
big = !big
})
}
}
使用Compose
为我们提供的animateXXXAsState
函数,就可以非常方便的设置,比如这里目标值的单位是dp
,所以就可以使用
animateDpAsState
函数。除此之外类似的函数,还有animateRectAsState
,animateIntAsState
等等。
注意:
size
不能定义成var类型:
animateDpAsState
是个State
而不是MutableState
,所以它是只读不能写的
写起来确实非常简单,我们都知道这是提供的APi内部封装好的缘故,封装好的另一面必然是抛弃了一定的灵活性。正是如此,如果想要对动画过程进行精细化的定制,比如想把方块从48dp瞬间移到200dp,再慢慢缩放到96dp,animateXXXAsState
就不能满足我们的需求了。
Animatable
对于方块从48dp瞬间移到200dp,再慢慢缩放到96d这个需求,我们需要用到Animatable
类,它是一个偏底层的动画类,上述提到的animateDpAsState
的内部实现就是借助了Animatable
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var big by mutableStateOf(false)
setContent {
val anim = remember{ Animatable(48.dp, Dp.VectorConverter) } //1
// 这里要使用Compose提供的协程
LaunchedEffect(big){ //2
// 瞬间移动目标值
anim.snapTo(200.dp)
// 动画的方式慢慢到目标值
anim.animateTo(96.dp)
}
Box(
Modifier
.size(anim.value) //3
.background(Color.Red)
.clickable {
big = !big
}
}
}
这段代码,需要注意三个地方:
1.Animatable(xxx)
使用Animatable
类的构造函数,可以定义动画的初始值,这里和animateDpAsState
不同的是,参数不能直接使用dp
,默认是float类型,所以需要后面添加一个Dp.VectorConverter
来帮我们作dp->float
的转换。其他类型的转换,官方也为我们提供了很多函数可直接调用。
2.LaunchedEffect(xxx)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
LaunchedEffect
函数的两个参数,key
表示任意类型的值,当key
的值发生变化,内部的代码块block
就会再次执行。
观察这个block
的类型,发现它用到了协程,这就意味着执行的动画需要是个挂起函数:
suspend fun snapTo(targetValue: T) {
mutatorMutex.mutate {
endAnimation()
val clampedValue = clampToBounds(targetValue)
internalState.value = clampedValue
this.targetValue = clampedValue
}
}
suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V> {
val anim = TargetBasedAnimation(
animationSpec = animationSpec,
initialValue = value,
targetValue = targetValue,
typeConverter = typeConverter,
initialVelocity = initialVelocity
)
return runAnimation(anim, initialVelocity, block)
}
那可以用自己launch
的协程吗?比如这样:
lifecycleScope.launch {
// 瞬间移动目标值
anim.snapTo(200.dp)
// 动画的方式慢慢到目标值
anim.animateTo(96.dp)
}
敲上这段代码,编译器就会提示要用LaunchedEffect
:
Calls to launch should happen inside a LaunchedEffect and not composition
这是因为,自己launch
的协程,它是不支持重组优化的,这就意味着重组过程会重复执行该动画。第一浪费资源,第二很可能不是我们期待的视觉效果。
而LaunchedEffect
,却可以根据某个值是否变化了,再次执行动画,这个过程变得可控了。如我们例子中的big
,在点击box控件后就会被改变,所以动画就会被再次执行。
3.anim.value
通过Animatable
的value
属性可以获取其具体值,设置到控件。
几种常用的AnimationSpec
TweenSpec
上述的动画变换的控制,还是过于简单。如果想做些更精细的控制,比如控制动画过程的时长,动画的出入场的速度。此时就要了解一下动画函数的其他参数。
回顾一下上面用到的animateTo
函数,之前只用到了targetValue
,它的第二个参数animationSpec
就可以帮助我们更精细的控制动画过程,它是AnimationSpec
的对象,并且提供了一个默认值defaultSpringSpec
,这个默认值我们后面在介绍。
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)
比如我们对一个方块做一个匀速动画,可以用TweenSpec
来设置动画函数的第二个参数animationSpec
TweenSpec(easing = LinearEasing, durationMillis = 2000)
其中durationMillis
表示动画时长2000ms, easing
就表示动画缓动的方式,和属性动画中的Interpolator
作用是一样的。LinearEasing表示匀速,官方提供了四种常用的缓动方式:
LinearEasing:匀速
FastOutSlowInEasing: 先加速后减速
FastOutLinearInEasing:加速出场
LinearOutSlowInEasing:减速入场
比如下面的代码就可以实现,2s内方块匀速放大缩小的动画效果:
Column {
LaunchedEffect(big) {
// 动画的方式慢慢到目标值
anim.animateTo(
if (big) 250.dp else 50.dp,
TweenSpec(easing = LinearEasing, durationMillis = 2000)
)
}
Box(
Modifier
.size(anim.value)
.background(Color.Red)
.clickable {
big = !big
})
}
除匀速缓动外,其他三种都是通过三阶贝塞尔曲线去实现的:
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
val LinearEasing: Easing = Easing { fraction -> fraction }
所以如果你有特殊定制的需求,可以通过自定义缓动方式,比如通过自定义CubicBezierEasing
构造函数,提供设置的四个值(是两个坐标点),另外两个点是默认(0,0),(1,1)。
mEasingMode = CubicBezierEasing(0f, 0.45f, 0.45f, 1.0f)
三阶贝塞尔曲线:通过P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线,可以通过下面这个网站玩一玩贝塞尔曲线: 玩一玩贝塞尔曲线
KeyframesSpec
TweenSpec
的控制的精细程度已经很不错了,但是某些场景还是不能满足。比如想在控制这个方块在某些个时间点到达某些个指定的位置,或许在具体的某个动画段先加速后减速,另一个动画片段匀速前进。这些情况TweenSpec
就捉襟见肘了,不过好在Compose
为我们提供了KeyframesSpec
,如其名中的Keyframes
,表示关键帧,可以通过控制几个关键帧来对动画过程进行控制。
举例:控制上述的方块控制,500ms时刻到100dp的位置,在1000ms时到250dp位置。其中0-500ms要匀速缓动,500ms-1000ms要先加速后减速缓动。
// 设置动画关键帧
anim.animateTo(if (big) 250.dp else 50.dp,
keyframes {
durationMillis = 1000
50.dp at 0 with LinearEasing // 0-500ms 匀速前进
100.dp at 500 with FastOutSlowInEasing // 500-1000ms 先加速后减速 500ms时刻到100dp
250.dp at 1000
}
)
需要注意的是:
- 缓动方式的设置,作用的是从这一帧到下一帧,上述的
FastOutSlowInEasing
作用的是500-1000ms这一段 keyframes
函数是提供的简便写法,使用KeyframesSpec
构造也是可以的
SnapSpec
最开始我们通过anim.snapTo()
函数快速缩放到某个大小,除了这种方式,animateTo
其实也可以做到,不过它需要借助SnapSpec
:
anim.animateTo(
if (big) 250.dp else 50.dp,
SnapSpec()
)
SpringSpec
上面介绍了三种TweenSpec
、KeyframesSpec
、SnapSpec
,它们有个共性,它们的父类都是DurationBasedAnimationSpec
,就是说它们是基于时间的动画。什么意思呢?还有不是基于时间的设置的动画,还真有?那就是弹簧模型,你可以结合现实中的物理弹簧想一下,是不是我们按压一下,无从得知多久停下来的,会弹簧的特性来决定什么时候停下来。
前面我们介绍animateTo
函数时 ,其中一个参数提供了一个默认值defaultSpringSpec
,它是Compose
提供的弹簧动画:
internal val defaultSpringSpec: SpringSpec<T> =
SpringSpec(visibilityThreshold = visibilityThreshold)
自定义一种弹簧也比较简单,只需要设置三个参数:
- 阻尼比:控制弹力大小,值越小弹力越大,默认是1(无弹力效果)
- 刚性:弹簧的硬度,越硬越弹不动
- 可见阈值:到某个值停止弹动,视觉上没有看到弹动,实际上可能还在微弱的弹动,设置这个阈值可以避免无用的性能损耗
比如这里设置一种高弹力、低硬度、无可见阈值的弹簧效果:
anim.animateTo(250.dp, spring(Spring.DampingRatioHighBouncy, Spring.StiffnessLow, null))
RepeatableSpec和InfiniteRepeatableSpec
RepeatableSpec
支持重复执行动画,可以通过repeatable
函数来实现:
fun <T> repeatable(
iterations: Int,
animation: DurationBasedAnimationSpec<T>,
repeatMode: RepeatMode = RepeatMode.Restart,
initialStartOffset: StartOffset = StartOffset(0)
): RepeatableSpec<T> =
RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)
四个参数:
iterations
:重复的次数animation
:动画效果,注意是DurationBasedAnimationSpec
类型的才可以repeatMode
:重复的模式,比如Restart
(从头开始)、Reverse
(反转)initialStartOffset
:初始偏移,注意这里偏移的是时间,比如延迟100ms、跳过100ms
比如下面的例子:重复执行三次,使用匀速、反转、跳过前100ms的方式
anim.animateTo(
250.dp,
repeatable(
3,
TweenSpec(easing = LinearEasing, durationMillis = 2000),
RepeatMode.Reverse,
StartOffset(100, StartOffsetType.FastForward) // 跳过前100ms
)
)
Note:
如果使用反转模式,记得次数要是奇数次,由于使用的是
animateTo
函数,它会以目标值为最终的结果,如果使用了偶数次重复,会出现重复结束后,闪跳到最终的目标值。
如果需要无限次数的重复,需要使用InfiniteRepeatableSpec
:
anim.animateTo(
250.dp,
infiniteRepeatable(
TweenSpec(easing = mEasingMode, durationMillis = 2000),
RepeatMode.Reverse,
StartOffset(100, StartOffsetType.FastForward)
)
)
Note:
当然这里的无限次数是指不被打断的情况,由于动画是在协程里,如果协程被取消了,这个无限动画也会停下来。