衰减型动画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() 是面向不是像素的一切类型,比如角度、比例等。

相关推荐
_一条咸鱼_3 小时前
揭秘 Android ListView:从源码深度剖析其使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
深入剖析 Android NestedScrollView 使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
揭秘 Android ScrollView:深入剖析其使用原理与源码奥秘
android·面试·android jetpack
_一条咸鱼_3 小时前
深入剖析 Android View:从源码探寻使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
揭秘 Android View 绘制原理:从源码剖析到极致理解
android·面试·android jetpack
_一条咸鱼_3 小时前
揭秘 Android FrameLayout:从源码深度剖析使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
揭秘 Android ViewGroup:从源码深度剖析使用原理
android·面试·android jetpack
_一条咸鱼_3 小时前
揭秘 Android TabLayout:从源码深度剖析使用原理
android·面试·android jetpack
_一条咸鱼_4 小时前
深入剖析 Android RecyclerView 的使用原理
android·面试·android jetpack