前言
回顾上篇文章我们学习了 compose 中的 animateTo 、 snapTo 如何使用,和 animationSpec 的参数配置等等,这篇文章,我们学习 Animatable 中另外一个方法 animateDecay
animateDecay
衰减型动画, 它只有一个实现类 DecayAnimationSpecImpl
有了上一篇文章的经验,这里就直接上代码了,大体上都基本类似,直接上栗子
exponentialDecay
指数型衰减动画,它是一种自然衰减,以固定的比例逐渐减小时,称之为指数衰减
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
@Composable
fun DecayAnimationTest() {
val anim = remember {
Animatable(0.dp, Dp.VectorConverter)
}
/**
* 样条衰减动画
* 他是针对于设置像素的动画的,因为他内部使用了 density 密度值来根据不同的设备做了转换计算了, 像这里使用 dp 就是错了, 因为 dp
* 它本身就是已经是转换成像素了,已经做了一层转换了, 如果再使用 SplineBasedDecay<Dp> 那么他会再做一次转换, 就多余, 同时这个动画如果针对角度来做的,也不能
* 使用它, 因为角度无论在任意的设备上角度都是一样的,和密度值无关,如果你使用它了,会导致在不同的设备上,旋转的角度不一样了
*
* 所以使用 dp 不要使用它, 非 dp,仅仅是像素的可以使用它 (最简单的区别)
*/
val splineBasedDecay = rememberSplineBasedDecay<Dp>()
/**
* 指数型衰减动画
*
* 非 Dp 的衰减动画, 和 SplineBasedDecay 刚好相反的,和设备密度值无关
* 几个参数设置:
* frictionMultiplier:摩擦系数,默认1f,数值越大,惯性滑动的摩擦力越大,滑动的距离越短
* absVelocityThreshold:速度阈值的绝对值, 默认 0.1f,它是一个速度模型, 可以理解为在小于 0.1f 的时候, 动画就会停止下来,如果是 0.5, 那么速度在小于 0.5f 的时候就会停止下来,也就是停止的更早
*/
val exponentialDecay = remember { exponentialDecay<Dp>(frictionMultiplier = 1f, absVelocityThreshold = 9f) }
LaunchedEffect(Unit) {
delay(1000)
/**
* animateDecay 衰减型动画,惯性滑动慢慢减速的场景
* 其实有一种情况, animateTo 也可以做, 因为本质是开始是线性速度, 后面慢慢开始减速 LinearOutSlowInEasing, 但是本质不同是,一个需要设置终点值, 一个是根据速度来计算的
*/
/**
* initialVelocity 速度值, 一般会用 Velocitytrack 计算速度
* animationSpec: 衰减型动画, 只有一个实现类
*/
anim.animateDecay(1000.dp, exponentialDecay)
}
Box(
modifier = Modifier
.padding(top = anim.value)
.size(80.dp)
.background(Color.Green)
) {
Text(text = anim.value.toString(), modifier = Modifier.align(Alignment.Center))
}
}
|
这段代码在启动后,页面被加载后 1000 毫秒后,然后调用 animateDecay 方法,这里 animateDecay 的 1000 毫秒是一个 initialVelocity 初始速度值,这里的速度值,一般是用于对惯性滑动场景来做的,一般在列表会使用到,在上篇的文章也有介绍,可以看下上篇文章,然后第二个参数仍然是 animationSpec 的设置,这里我们设置的是 exponentialDecay
1
2
3
4
5
6
7
8
9
10
11
12
13
|
fun <T> exponentialDecay(
/*@FloatRange(
from = 0.0,
fromInclusive = false
)*/
frictionMultiplier: Float = 1f,
/*@FloatRange(
from = 0.0,
fromInclusive = false
)*/
absVelocityThreshold: Float = 0.1f
): DecayAnimationSpec<T> =
FloatExponentialDecaySpec(frictionMultiplier, absVelocityThreshold).generateDecayAnimationSpec()
|
其中可以设置的参数有两个,他们的区别如下
- frictionMultiplier 摩擦系数,默认1f,数值越大,惯性滑动的摩擦力越大,滑动的距离越短
- absVelocityThreshold:速度阈值的绝对值, 默认 0.1f,它是一个速度模型, 可以理解为在小于 0.1f 的时候, 动画就会停止下来,如果是 0.5, 那么速度在小于 0.5f 的时候就会停止下来,也就是停止的更早
代码运行后是这样的
splineBasedDecay
样条衰减动画,是基于屏幕像素进行的动画速度衰减,当像素密度越大动画减速越快,动画的时长越短,动画惯性滑动的距离越短;可以理解屏幕像素密度越大摩擦力越大,所以惯性滑动的距离就越短,使用这个动画,在不同的设备上可能会呈现不同的效果
其余的代码不变,我们仅仅是把 anim.animateDecay(1000.dp, exponentialDecay) 换成 anim.animateDecay(1000.dp, splineBasedDecay) 再运行一下
可以发现,我们设置的初始速度是一样的,但是动画运动后,停止的目标值位置是不一样的,这是因为 splineBasedDecay 他内部还做了一层的换算,他会拿 density 做一层换算,而我们这里的使用的初始值已经是一个 dp 值,已经是经过换算的了,所以就反而多余了
看看 splineBasedDecay 源码就知道了
1
2
3
4
5
6
7
8
|
actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> {
// This function will internally update the calculation of fling decay when the density changes,
// but the reference to the returned spec will not change across calls.
val density = LocalDensity.current
return remember(density.density) {
SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()
}
}
|
可以看到这里使用了到 density 并传入到 SplineBasedFloatDecayAnimationSpec 中了,其中就在这个地方对 density 和 value 值做了再一次的转换,这里的就不再贴源码了,有兴趣的可以自己去看一下
所以 splineBasedDecay 和 exponentialDecay 的区别就是如果本身设置的初始值已经是转换过的了,就使用指数型衰减动画,如果需要动画的运动速度曲线和设备密度相关联,那就使用样条衰减动画
监听动画的每一帧数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@Composable
fun FrameAnimTest() {
val topAnim = remember { Animatable(0.dp, Dp.VectorConverter) }
var top2 = remember { mutableStateOf(0.dp) }
val exponentialDecay = remember { exponentialDecay<Dp>()}
LaunchedEffect(Unit) {
topAnim.animateDecay(1000.dp, animationSpec = exponentialDecay) {
// 监听动画的每一帧数
top2.value = this.value
}
}
Row(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.padding(top = topAnim.value)
.size(100.dp)
.background(Color.Green)
)
/** 监听动画的每一帧变化 **/
Box(
modifier = Modifier
.padding(top = top2.value)
.size(100.dp)
.background(Color.Yellow)
)
}
}
|
这段代码的其实是使用 topAnim 开启了一个 animateDecay 的衰减型动画,然后在这个 block 的回调函数中,将当前的动画运行的每一帧赋值给 top2 变量,而top2 变量又是作用在黄色的 Box 上的 padding 上面的,所以在绿色方块的动画执行后,黄色的方块也会执行,代码运行后是这样的
动画的打断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
fun AnimInterruptionTest() {
val topAnim = remember { Animatable(0.dp, Dp.VectorConverter) }
val exponentialDecay = remember { exponentialDecay<Dp>() }
/** 场景一**/
LaunchedEffect(Unit) {
println("@@@@ <top>.AnimInterruptionTest start")
topAnim.animateDecay(2000.dp, animationSpec = exponentialDecay)
println("@@@@ <top>.AnimInterruptionTest animateDecay")
/** 这样写的话,动画会执行完后,才会执行 stop方法,这样等于没用了,因为动画已经结束了,这是为什么? **/
/**
* 这是因为 LaunchedEffect 是一个协程函数, 这里的方法都是按照顺序执行的,然后 animateDecay 是一个挂起函数,所以只有等 animateDecay 函数执行完成后,才会执行stop方法了
*/
topAnim.stop()
println("@@@@ <top>.AnimInterruptionTest topAnim.stop()")
}
/** 场景二 **/
// LaunchedEffect(Unit) {
// try {
// /** 当调用 stop() 方法后, 会抛出在这个协程中抛出异常,那么这个协程中后面的代码会无法执行 **/
// topAnim.animateDecay(2000.dp, animationSpec = exponentialDecay)
// println("@@@@ <top>.AnimInterruptionTest 后面的代码")
// } catch (e: CancellationException) {
// println("@@@@ <top>.AnimInterruptionTest ${e.message}")
// e.printStackTrace()
// }
// println("@@@@ <top>.AnimInterruptionTest 动画停止了") // 去掉 try catch,停止后,这行代码会无法执行
// }
LaunchedEffect(Unit) {
delay(100)
/** 100 毫秒后打断它 **/
// topAnim.stop()
}
Box(
modifier = Modifier
.padding(top = topAnim.value)
.size(100.dp)
.background(Color.Green)
)
}
|
动画的打断停止,就是调用 stop 方法,唯一需要注意的是(场景一),animateDecay 和 stop 都是 suspend 挂起函数,会阻塞当前协程,所以在调用的时候,需要避免一前一后的方式,这样会等上一个挂起函数执行完成后,才执行 stop 方法,这个就没有停止的效果了, 其二需要注意的是,调用 stop 的方式如果动画正在执行的话,会在当前协程抛出异常,所以需要注意,如果有后续的代码,可能会导致无法执行了。这段代码和上面运行有的动画基本是一致的,就不放图了
动画的边界, 上限和下限
动画的边界设置其实就是为了设置不超过某个值,比如在上面的动画代码中, 如果初始速度足够大的话,那么他就会超出屏幕了, 而设置边界的方法就是 updateBounds 方法, 他的内部有两个参数
1
|
fun updateBounds(lowerBound: T? = this.lowerBound, upperBound: T? = this.upperBound)
|
- lowerBound 动画的下限
- upperBound 动画的上限
这两个参数也比较简单,我们看下如下这段代码, 顺便使用一下这个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
@Composable
fun boundsTest() {
BoxWithConstraints {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
val animOffset = remember { Animatable(DpOffset.Zero, DpOffset.VectorConverter) }
/** 设置动画的边界, 不超过屏幕大小 **/
/**
* 动画的上限和下限意思是,动画往那个方向偏移的界限,只能在这个区域中
* 如果是垂直平移,下限就是 0, 上限就是屏幕宽度, 水平的时候则相反
* 但是,如果是斜着的时候,界限就是 x,y的值了
* lowerBound: 动画的下限
* upperBound: 动画的上限
*/
anim.updateBounds(lowerBound = 0.dp, upperBound = this.maxHeight - 100.dp)
/** 垂直平移动画 **/
LaunchedEffect(Unit) {
anim.animateDecay(3000.dp, exponentialDecay())
}
/** 斜着平移的 offset动画, 设置上界和下界, 防止超出屏幕 **/
val upperBound = remember { DpOffset(x = this.maxWidth - 100.dp, y = this.maxHeight - 100.dp) }
LaunchedEffect(Unit) {
/** 设置动画的边界 **/
animOffset.updateBounds(lowerBound = DpOffset.Zero, upperBound = upperBound)
/** initialVelocity设置初始速度,注:并不是目标位置 **/
animOffset.animateDecay(DpOffset(x = 1000.dp, y = 3600.dp), exponentialDecay())
}
/** 垂直平移 **/
Box(
modifier = Modifier
.padding(top = anim.value)
.size(100.dp)
.background(Color.Blue)
)
/** 斜方向平移 **/
Box(
modifier = Modifier
.padding(top = animOffset.value.y, start = animOffset.value.x)
.size(100.dp)
.background(Color.Red)
)
}
}
|
这里的代码是绿色方块设置了一个垂直移动的动画, 并且设置了下限 0 dp, 上限不超过屏幕大小, 所以最终只会在屏幕内进行移动, 然后触边后停止动画, 还有一个红色方块, 基本也是类似的, 唯一不一样的就是初始速度的设置是 offset 坐标, 它是以 2 个坐标点进行偏移的, 同时也设置的下限和上限是在屏幕内中, 最终运行的代码是这样的
动画的结果
动画的结果就是调用 animateDecay 的返回值 AnimationResult
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
@Composable
fun AnimEndResultTest() {
BoxWithConstraints {
val anim = remember { Animatable(initialValue = 0.dp, Dp.VectorConverter) }
LaunchedEffect(Unit) {
/** 设置边界 **/
anim.updateBounds(lowerBound = 0.dp, upperBound = this@BoxWithConstraints.maxWidth - 100.dp)
// animationResult 动画的结果
var animationResult = anim.animateDecay(4600.dp, exponentialDecay())
/**
* animationResult 内部属性
* endReason: 结束原因, finish,动画完成了, BoundReached, 动画到达边界了
* endState: 结束状态
**/
// if (animationResult.endReason == AnimationEndReason.BoundReached) { // BoundReached 动画到达边界了
// println("@@@@ <top>.AnimEndResultTest 到达边界了 ${animationResult.endState.isRunning} 撞边的速度${animationResult.endState.velocity}")
//
// /** 拿到撞边的速度,可以做一个弹簧动画 **/
// /** 这样只会回弹一次,可以让他反复回弹 **/
// anim.animateDecay(-animationResult.endState.velocity, exponentialDecay())
// }
/** 上面是一次回弹,使用 while 做一个反复回弹的效果 **/
while (animationResult.endReason == AnimationEndReason.BoundReached) { // 撞边
// velocity:拿到结束的那一刻的动画速度,设置一个反方向速度,让其不断的反弹
animationResult = anim.animateDecay(-animationResult.endState.velocity, exponentialDecay())
}
}
Box(
modifier = Modifier
.padding(start = anim.value)
.size(100.dp)
.background(Color.Blue)
)
}
}
|
上面的动画代码就是开启了一个横向的偏移的动画,不断的设置 padding 值,且设置了一个边界是在屏幕之内回弹,最终在动画撞边后,获取撞边停止时刻的速度,再开启一个反方向的动画,不断的反弹,直到速度衰减到停止下来
其中 anim.animateDecay 返回值 AnimationResult 就是动画的结果,也比较简单,看下他的源码,后面再看下动画运行后的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
|
suspend fun animateDecay(
initialVelocity: T,
animationSpec: DecayAnimationSpec<T>,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V> {
val anim = DecayAnimation(
animationSpec = animationSpec,
initialValue = value,
initialVelocityVector = typeConverter.convertToVector(initialVelocity),
typeConverter = typeConverter
)
return runAnimation(anim, initialVelocity, block)
}
|
AnimationResult
1
2
3
4
5
6
7
8
|
class AnimationResult<T, V : AnimationVector>(
val endState: AnimationState<T, V>,
val endReason: AnimationEndReason
) {
override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
}
|
其中 AnimationResult 是一个类,同时内部维护了 endState 和 endReason 结束状态和结束原因,同时他们也是类,我们先看看 endState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
class AnimationState<T, V : AnimationVector>(
val typeConverter: TwoWayConverter<T, V>, // 用于转换动画的值和动画的速度向量。
initialValue: T, // 表示动画开始时的值
initialVelocityVector: V? = null, // 是动画开始时的速度向量
lastFrameTimeNanos: Long = AnimationConstants.UnspecifiedTime, // 表示上一帧的时间
finishedTimeNanos: Long = AnimationConstants.UnspecifiedTime, // 表示动画结束的时间
isRunning: Boolean = false // 表示动画是否正在运行
) : State<T> {
// 当前动画的值
override var value: T by mutableStateOf(initialValue)
internal set
// 当前动画的速度向量
var velocityVector: V =
initialVelocityVector?.copy() ?: typeConverter.createZeroVectorFrom(initialValue)
internal set
// 上一帧的时间
@get:Suppress("MethodNameUnits")
var lastFrameTimeNanos: Long = lastFrameTimeNanos
internal set
// 动画结束的时间
@get:Suppress("MethodNameUnits")
var finishedTimeNanos: Long = finishedTimeNanos
internal set
// 动画是否正在运行
var isRunning: Boolean = isRunning
internal set
// 当前动画的速度,是从速度向量转换得来的
val velocity: T
get() = typeConverter.convertFromVector(velocityVector)
}
|
基本上就是动画的一些运行状态,速度等等,有需要的时候临时翻一下源码也很好理解,参数不多,接着来看看 endReason
1
2
3
4
5
6
|
enum class AnimationEndReason {
// 撞边,动画达到设置的边界
BoundReached,
// 动画已成功完成,没有任何中断(即是自然完成的,调用 stop 的情况,不会有 finishEd 结果返回)
Finished
}
|
endReason 就更加简单了,如注释那样,最后看看上面代码运行的效果