简介
衰减型动画的效果是动画会有一个初速度,然后像受到阻力一样,速度随时间逐渐减慢,直到完全停止。这种动画模拟了现实世界中的物理运动规律,为用户界面带来自然、流畅的交互体验。
它的主要使用场景是模拟具有惯性和自然减速效果的动画。比如:快速滑动列表后,列表的惯性滚动效果。
这是微信根据时间筛选账单的页面,其中时间选择器就使用了衰减动画来实现平滑的惯性滚动:
接下来我们来了解如何在 Compose 中实现这种动画效果。
animateDecay() 和 animateTo() 的区别
animateDecay()
和 animateTo()
动画函数的区别是:
-
它需要指定初始速度,因为它是基于物理模型的,模拟现实世界中物体在有初速度的情况下,受到摩擦力的作用下的运动,自然需要有一个初始速度。并且这个初速度通常来自用户手势的速度,符合自然运动效果。
-
它没有预定的目标值,并且对它来说,目标值也是没有意义的,因为最终的位置应该取决于初始速度和摩擦系数,而不是初始预定。而
animateTo()
总是会精确到达指定的目标值。
函数原型
让我们看看 animateDecay()
函数的定义:
kotlin
// Animatable.kt
suspend fun animateDecay(
initialVelocity: T, // 动画的初始速度,表示物体开始运动时的速度
animationSpec: DecayAnimationSpec<T>, // 衰减动画规格,定义了摩擦系数等物理参数
block: (Animatable<T, V>.() -> Unit)? = null
)
初始速度应该设置多少才合适?
应该设置为用户抬起手指时的滑动速度,在 Compose 中可以使用VelocityTracker
这个类,来跟踪手势速度,不过现在不关心它,了解到这就够了。
那它的单位又是什么?
单位是多少个每秒,啊?多少个?因为这个单位是动态的,取决于Animatable
对象的泛型,例如,如果是 Dp
类型,那么单位就是 "dp每秒"。
接下来填写衰减动画规格,其中animationSpec
参数的类型 DecayAnimationSpec
是一个接口,它只有一个实现类 DecayAnimationSpecImpl
:
kotlin
private class DecayAnimationSpecImpl<T>(
private val floatDecaySpec: FloatDecayAnimationSpec
) : DecayAnimationSpec<T> {
..
}
由于DecayAnimationSpecImpl
类是私有的,我们需要用别的方式来创建它的对象,有三个函数可以创建:
kotlin
exponentialDecay<>()
splineBasedDecay<>()
rememberSplineBasedDecay<>() // splineBasedDecay 的 remember 版本
实际上只有两个,rememberSplineBasedDecay()
只是 splineBasedDecay()
带remember的版本。接下来我们来看看 splineBasedDecay()
和 exponentialDecay<>()
函数。
衰减动画规格:splineBasedDecay
我们先来看 splineBasedDecay<>()
函数,splineBasedDecay表示它是基于spline的曲线,spline是数学概念------样条,指由多个多项式曲线段连接形成的平滑曲线。它常用于创建平滑、自然的减速效果,也是Android原生的惯性滑动曲线算法。
函数定义如下:
kotlin
fun <T> splineBasedDecay(density: Density): DecayAnimationSpec<T> =
SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()
这个函数有一个参数 density
,代表设备的像素密度。
但我们通常是不用手动填写这个参数的,只需要调用这个函数的带remember版本,即可自动填写,不信?点进去看看rememberSplineBasedDecay
的实现:
kotlin
@Composable
actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> {
val density = LocalDensity.current // 自动获取当前像素密度
return remember(density.density) {
SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()
}
}
那什么时候,我们需要手动填写 density
,要用到 splineBasedDecay<>()
函数呢?
直接用带remember的版本即可,不用理会splineBasedDecay<>()
函数。
为什么衰减动画需要像素密度参数?
为什么需要有这个像素密度 density
参数呢?
因为像素密度 density
可以调整不同设备上惯性滑动的摩擦系数。像素密度越高的设备上,相同物理距离对应更多像素,在屏幕上滑动,摩擦力越大,惯性滑动越早停止。
这是非常合理的,它确保了用户以相同的力度在不同的设备上滑动,内容会滑动相同的物理距离,而非相同的像素距离。这符合直觉。如果不考虑像素密度,用户在高分辨率的手机上,轻轻一划,可能会翻过很多页内容,而在平板上相同的滑动,却只能滑动很短的距离,这违背了我们的对物理世界的认知。
通过考虑像素密度,能在各种设备上提供一致的物理体验。
示例
了解完这些以后,我们来写个示例:绿色方块经过1s延迟后以一个初速度向下惯性滑动,然后再经过1s延迟以一个初速度向上惯性滑动,周而复始进行运动。

kotlin
@Composable
fun SlideBox() {
val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
val decay = rememberSplineBasedDecay<Dp>()
var slide by remember { mutableStateOf(true) }
LaunchedEffect(slide) {
delay(1000)
anim.animateDecay(if (slide) 1000.dp else (-1000).dp, decay)
slide = !slide
}
Box(Modifier.border(1.dp, Color.LightGray)) {
Box(
modifier = Modifier
.padding(top = anim.value) // 使用边距来改变方块的位置
.background(Color.Green)
.size(48.dp)
.align(Alignment.Center)
)
}
}
注意事项
注意:虽然 splineBasedDecay<>()
是一个带有泛型的函数,好像可以用它来计算各种类型,但是不要用它来计算Dp,因为这个函数对于不同的像素密度的设备,会有不同的摩擦力系数,这就是一种修正,它针对的参数是像素,而当你的参数是Dp时(Dp会对不同像素密度的设备有不同的显示效果),Dp自身就是一种修正,所以这样会导致双重修正,值反而不正确了。
并且当参数代表角度时,也不能使用 splineBasedDecay<>()
,因为不管在什么设备上,最终转过的角度应该是一致的,不需要基于设备的像素密度进行调整。
所以我们上述示例代码的写法是不对的,我们应该把参数值转为像素单位再进行使用。所以这个函数是相对不实用的,它非常适合处理像素单位的动画。
示例的正确实现
kotlin
@Composable
fun SlideBoxCorrect() {
// 使用像素单位而不是 Dp
val anim = remember { Animatable(0f) }
val decay = rememberSplineBasedDecay<Float>()
val density = LocalDensity.current
var slide by remember { mutableStateOf(true) }
LaunchedEffect(slide) {
delay(1000)
// 将 Dp 转换为像素
val velocityInPixels = with(density) {
if (slide) 1000.dp.toPx() else -1000.dp.toPx()
}
anim.animateDecay(velocityInPixels, decay)
slide = !slide
}
Box(Modifier.border(1.dp, Color.LightGray)) {
Box(
modifier = Modifier
// 将像素转回 Dp 用于布局
.padding(top = with(density) { anim.value.toDp() })
.background(Color.Green)
.size(48.dp)
.align(Alignment.Center)
)
}
}
衰减动画规格:exponentialDecay
我们再来看看另一个函数 exponentialDecay<>()
,exponentialDecay的意思是指数衰减。
指数衰减是一种常见的物理现象,其速度按指数函数随时间减小:
exponentialDecay<>()
和 splineBasedDecay()
最重要的区别有两点:
-
它是不会根据像素密度去做修正------自动调整摩擦系数,它之所以不做这种修正,也是故意的,它可以针对任何类型去做动画。
-
exponentialDecay 提供了两个可调节的参数:
kotlinfun <T> exponentialDecay( @FloatRange(from = 0.0,fromInclusive = false) frictionMultiplier: Float = 1f, // 摩擦力系数 @FloatRange(from = 0.0,fromInclusive = false) absVelocityThreshold: Float = 0.1f // 速度阈值的绝对值,当速度低于此值时动画停止 )
因为数学上的指数曲线与x轴不相交,也就是永远不会真正到达零,如果没有阈值,动画是无法停止的。并且速度可能是负数,也需要这个阈值来确保动画停止。
总结
-
animateDecay()
是一个基于物理模型的衰减型动画,它一般用于惯性滑动以及各种各样的惯性场景,比如说惯性旋转。 -
DecayAnimationSpec
对象可以使用splineBasedDecay()
和exponentialDecay()
函数创建,其中splineBasedDecay()
是只能面向像素,exponentialDecay()
是面向不是像素的一切类型,比如角度、比例等。