衰减型动画animateDecay()

简介

衰减型动画的效果是动画会有一个初速度,然后像受到阻力一样,速度随时间逐渐减慢,直到完全停止。这种动画模拟了现实世界中的物理运动规律,为用户界面带来自然、流畅的交互体验。

它的主要使用场景是模拟具有惯性和自然减速效果的动画。比如:快速滑动列表后,列表的惯性滚动效果。

这是微信根据时间筛选账单的页面,其中时间选择器就使用了衰减动画来实现平滑的惯性滚动:

接下来我们来了解如何在 Compose 中实现这种动画效果。

animateDecay() 和 animateTo() 的区别

animateDecay()animateTo() 动画函数的区别是:

  1. 它需要指定初始速度,因为它是基于物理模型的,模拟现实世界中物体在有初速度的情况下,受到摩擦力的作用下的运动,自然需要有一个初始速度。并且这个初速度通常来自用户手势的速度,符合自然运动效果。

  2. 它没有预定的目标值,并且对它来说,目标值也是没有意义的,因为最终的位置应该取决于初始速度和摩擦系数,而不是初始预定。而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() 最重要的区别有两点:

  1. 它是不会根据像素密度去做修正------自动调整摩擦系数,它之所以不做这种修正,也是故意的,它可以针对任何类型去做动画。

  2. exponentialDecay 提供了两个可调节的参数:

    kotlin 复制代码
    fun <T> exponentialDecay(
        @FloatRange(from = 0.0,fromInclusive = false)
        frictionMultiplier: Float = 1f, // 摩擦力系数
        @FloatRange(from = 0.0,fromInclusive = false)
        absVelocityThreshold: Float = 0.1f // 速度阈值的绝对值,当速度低于此值时动画停止
    )

    因为数学上的指数曲线与x轴不相交,也就是永远不会真正到达零,如果没有阈值,动画是无法停止的。并且速度可能是负数,也需要这个阈值来确保动画停止。

总结

  1. animateDecay() 是一个基于物理模型的衰减型动画,它一般用于惯性滑动以及各种各样的惯性场景,比如说惯性旋转。

  2. DecayAnimationSpec 对象可以使用 splineBasedDecay()exponentialDecay() 函数创建,其中splineBasedDecay() 是只能面向像素,exponentialDecay() 是面向不是像素的一切类型,比如角度、比例等。

相关推荐
我命由我123452 天前
Android 对话框 - 对话框全屏显示(设置 Window 属性、使用自定义样式、继承 DialogFragment 实现、继承 Dialog 实现)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Jeled2 天前
Android 本地存储方案深度解析:SharedPreferences、DataStore、MMKV 全面对比
android·前端·缓存·kotlin·android studio·android jetpack
我命由我123452 天前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
alexhilton7 天前
Kotlin互斥锁(Mutex):协程的线程安全守护神
android·kotlin·android jetpack
是六一啊i8 天前
Compose 在Row、Column上使用focusRestorer修饰符失效原因
android jetpack
用户0609052552210 天前
Compose 主题 MaterialTheme
android jetpack
用户0609052552210 天前
Compose 简介和基础使用
android jetpack
用户0609052552210 天前
Compose 重组优化
android jetpack
行墨10 天前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack