Compose Compose动画之DecayAnimation

  1. DecayAnimation的使用
  2. 参数block的作用

DecayAnimation

上一篇Compose动画之AnimateSpec介绍了多个常用的AnimationSpec,它们都是作用在animateTo函数上的,比如weenSpec

anim.animateTo(96.dp, TweenSpec(easing = LinearEasing))

本篇介绍一个和它们不太一样的AnimationSpec - DecayAnimationSpecDecay是衰变的意思。

它主要是作用于惯性类的动画,和之前介绍的动画有个很大的区别是,它是不用指定终点(目标)值的,需要指定的是初始速度。

其实从物理世界的角度还是很容易理解的,一个惯性动作的终点是由开始那一刻的速度决定,至于目标到哪里停下来,是由外界因素(比如摩擦力)影响。

所以,对于这种动画效果,之前使用的animateTo就不合适了。要使用animateDecay函数:

suspend fun animateDecay(
    initialVelocity: T,
    animationSpec: DecayAnimationSpec<T>,  // look it
    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)
}

可以清晰的看到,第一个参数是初始速度initialVelocity,第二个参数就是DecayAnimationSpec

initialVelocity

初始速度这个参数,也是值得一说的。它的类型是泛型T,说明是支持多种类型的。

比如常用的DP,如果指定1000dp,就代表初始速度是每秒1000dp

DecayAnimationSpec

针对第二个参数,官方也给封装了两个常用的函数:splineBasedDecayexponentDecay

1.splineBasedDecay:基于样条的衰减

fun <T> splineBasedDecay(density: Density): DecayAnimationSpec<T> =
    SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()

需要理解一下为什么需要density这个参数:

首先需要知道这个函数是针对像素的。对于不同像素密度的手机来说,如果初始速度一致,像素密度高的,那么停下来的距离(屏幕的物理尺寸)必然短。所以为了保证在不同设备上的效果一致,就必须根据屏幕的密度做相应的调整。所以这个参数的意义就在于此。

由于这个参数获取方式很统一,所以官方也封装了一种更简单的函数rememberSplineBasedDecay

 @Composable
actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> {
    val density = LocalDensity.current
    return remember(density.density) {
        SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()
    }
}

该函数使用了remember,因为尺寸有可能会在使用过程中被用户更改,所以在被更改时,也要做相应的变化。

2.exponentDecay:指数式衰减

fun <T> exponentialDecay(frictionMultiplier: Float = 1f,absVelocityThreshold: Float = 0.1f): DecayAnimationSpec<T> =
    FloatExponentialDecaySpec(frictionMultiplier, absVelocityThreshold).generateDecayAnimationSpec()

这两个参数还是比较好理解的:

  • frictionMultiplier:摩擦乘数,指示动画停止的速度。该值要大于0 ,默认值为1.0
  • absVelocityThreshold: 动画被认为足够接近以停止动画完成的速度。

指数函数是无限趋近于0,所以第二个参数absVelocityThreshold的目的就是给个阈值速度提前结束动画。一半情况使用默认提供的值就可以了。

区分两者的使用场景

1.splineBasedDecay基于样条的衰减: 用于像素单位的衰减,由于内部会根据密度做校正,所以如果使用其他单位,会出现不可预期的效果。

2.exponentDecay:指数式衰减,适用于像素外的其他单位,内部不做任何校正。适用场景广,一般都用这个。

代码示例:对一个红色方块做衰减式动画

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
            // 基于样条的衰减: 用于像素单位的衰减
            val splineBasedDecay = rememberSplineBasedDecay<Dp>()
            // 指数式衰减:适用范围广,一般使用这个
            val exponentDecay = exponentialDecay<Dp>()
            LaunchedEffect(key1 = Unit) {
                delay(1000)
                // 以每秒1000dp的速度
//                anim.animateDecay(1000.dp, splineBasedDecay)
                anim.animateDecay(1000.dp, exponentDecay)
            }
            Box(
                Modifier
                    .padding(0.dp, top = anim.value, 0.dp, 0.dp)  // 先padding后background:margin的效果
                    .size(100.dp)
                    .background(Color.Red)
            )
        }
    }

参数block的作用

在介绍animateDecay函数参数时,略过了第三个参数block,在Compose动画之AnimateSpec中介绍animateTo函数中也是存在这个参数的:

suspend fun animateDecay(
    initialVelocity: T,
    animationSpec: DecayAnimationSpec<T>,  // look it
    block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>

这个block函数参数,作用是用来监听动画的每一帧。比如在上述的示例代码中,再添加一个蓝色方块跟着红色方块移动,就可以在block函数体中监听红色方块动画过程,不断更新蓝色方块的位置,实现两个方块的同步移动:

代码示例:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
            var padding by remember { mutableStateOf(anim.value) }   // 控制蓝色方块的位置
            // 指数式衰减:适用范围广,旋转
            val exponentDecay = exponentialDecay<Dp>()
            LaunchedEffect(key1 = Unit) {
                delay(1000)
                anim.animateDecay(1000.dp, exponentDecay, block = {
                    Log.d("AnimateDecay", "animate block: $value")
                    // 目标(⭐️):跟随红色的动画过程移动     
                    padding = value
                })
            }
            Row {
                Box(
                    Modifier
                        .padding(
                            0.dp,
                            top = anim.value,
                            0.dp,
                            0.dp
                        )  // 先padding后background:margin的效果
                        .size(100.dp)
                        .background(Color.Red)
                )
​
                Box(
                    Modifier
                        .padding(
                            0.dp,
                            top = padding,
                            0.dp,
                            0.dp
                        )  // 先padding后background:margin的效果
                        .size(100.dp)
                        .background(Color.Blue)
                )
            }
        }
    }

results matching ""

    No results matching ""