Android Compose 动画使用详解(六)动画配置之SpringSpec

2,294 阅读6分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

前面文章介绍到的 TweenSpecSnapSpecKeyframesSpace都是实现自 DurationBasedAnimationSpec即动画时长都是确定的,看到这里可能有些同学就有疑问了?那难道还有动画时长不确定的动画么?是的,比如本篇就要介绍的 SpringSpec

SpringSpec的 Spring 在这里是弹簧的意思,即弹簧动画规格配置,本篇就带你详细了解其各参数的含义和使用。

SpringSpec

上面说SpringSpec是弹簧动画,那在现实世界中弹簧到底是怎么运动的呢?下面用一张示意图展示一下:

当弹簧被拉离原点放开后,弹簧会弹回原点然后在原点附近震荡数次后最终在原点停止,这就是弹簧动画效果,即动画运动到目标值后不会立即停止,而是会按照弹簧特性在目标值附近震荡数次后再停止在目标值,而震荡的次数和速度就是SpringSpec需要配置的参数。

来看一下 SpringSpec的定义,代码如下:

class SpringSpec<T>(
    val dampingRatio: Float = Spring.DampingRatioNoBouncy,
    val stiffness: Float = Spring.StiffnessMedium,
    val visibilityThreshold: T? = null
) : FiniteAnimationSpec<T>

SpringSpec构造方法有三个参数,且都有默认值,参数解析如下:

  • dampingRatio:弹簧阻尼比,数值越小震荡的次数越多
  • stiffness:弹簧刚度,刚度越大弹簧回到原点的速度越快,即动画运行得越快
  • visibilityThreshold:可视阈值,即当弹簧弹到某一个值的时候就不弹了然后直接运动到目标值

可通过构造方法或简便函数 spring方法使用,参数都是一致的,如下:

// 构造方法使用
val springSpec =
        SpringSpec(dampingRatio = 0.1f, 
                   stiffness = 500f, 
                   visibilityThreshold = 0.01.dp)

// spring 简便函数使用
val springSpec =
        spring(dampingRatio = 0.1f, 
               stiffness = 500f, 
               visibilityThreshold = 0.01.dp)

// 使用
animateDpAsState(targetValue, animationSpec = springSpec)

下面分别来介绍这三个参数的作用和效果。

dampingRatio

阻尼系数越大,弹性运动的震荡次数越少、震荡幅度越小,阻尼系数有 4 个区间值,分别如下:

  • dampingRatio = 0:无阻尼,弹簧处于永远震荡的状态
  • dampingRatio < 1:欠阻尼,弹簧进行指数递减的震荡运动
  • dampingRatio = 1:临界阻尼,弹簧以最短时间结束运动,无震荡运动
  • dampingRatio > 1:过阻尼,弹簧进行无震荡的减速运动

这里实现一个小球下落的动画效果示例,为了体现震荡的效果,在下落过程中同时增加小球向右的偏移量并用线条绘制出小球的运动轨迹,实现代码如下:

// 屏幕宽度
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
// 屏幕高度
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
// 绘制运动轨迹的点
val points = remember { mutableListOf<DpOffset>() }
// 球的大小
val ballSize = 50.dp
// 目标位置
val target = 150.dp

// 控制动画的状态
var moveToRight by remember { mutableStateOf(false) }
// 根据状态计算目标值
val targetValue = if(moveToRight) target else 10.dp

// 球距离顶部的位置
val topPadding by animateDpAsState(targetValue, animationSpec = spring())
// 球距离左边的位置
var leftPadding by remember { mutableStateOf(10.dp) }

// 监听 topPadding 的变化修改 leftPadding 值并添加轨迹点
val leftPaddingValue = remember(topPadding) {
    leftPadding += 1.dp
    points.add(DpOffset(leftPadding, topPadding))
    leftPadding
}

Box {
    // 目标位置线条
    Box(modifier = Modifier.padding(top = target + ballSize/2 - 1.5.dp).size(screenWidth, 3.dp).background(Color.Green))
    // 小球运动轨迹
    Box(modifier = Modifier.size(screenWidth, screenHeight)){
        Canvas(modifier = Modifier.fillMaxSize()) {
            val path = Path()
            path.moveTo(10.dp.toPx(), 10.dp.toPx()+(ballSize/2).toPx())
            if(points.isNotEmpty()){
                points.forEach {
                    path.lineTo(it.x.toPx(), it.y.toPx()+(ballSize/2).toPx())
                }
                drawPath(
                    path = path,
                    color = Color.Red,
                    style = Stroke(
                        width = 3.dp.toPx(),
                    )
                )
            }
        }
    }
    // 小球
    Box(
        Modifier
            // 设置左边和顶部的间距
            .padding(start = leftPaddingValue, top = topPadding)
            .size(ballSize, ballSize)
            .clip(RoundedCornerShape(25.dp))
            .background(Color.Blue)
            .clickable {
                leftPadding = 10.dp
                points.clear()
                moveToRight = true
            }
    )
}

将上面代码中的 spring参数 dampingRatio 设置不同值的看看效果。

  1. dampingRatio = 0
val topPadding by animateDpAsState(targetValue, animationSpec = spring(dampingRatio = 0f))

效果:

咦?效果不对啊,上面不是说当阻尼比为 0 的时候是永远处于震荡状态吗?怎么上面的效果并没有震荡效果呢?那是因为阻尼比为 0 是一种理论状态,现实世界是不存在的,而 Compose 不支持设置阻尼比为 0,当设置为 0 时虽然代码不会报错但执行实际是没有弹簧效果的。

  1. dampingRatio < 1
val topPadding by animateDpAsState(targetValue, animationSpec = spring(dampingRatio = 0.1f))

这里设置阻尼比为 0.1f 再来看一下效果:

震荡效果就很明显了,小球震荡了很多次后才停在了目标位置。

  1. dampingRatio = 1
val topPadding by animateDpAsState(targetValue, animationSpec = spring(dampingRatio = 1f))

效果如下:

没有震荡效果,小球减速运动到目标位置。

  1. dampingRatio > 1
val topPadding by animateDpAsState(targetValue, animationSpec = spring(dampingRatio = 10f))

效果:

跟设置 1 时一样没有震荡效果,但是运行速度更慢了,当大于 1 时值越大运动速度越慢,动画时长越久。

实际上 Compose 预置了 4 个阻尼比值,如下:

object Spring {

    // 震荡效果高
    const val DampingRatioHighBouncy = 0.2f

    // 震荡效果中
    const val DampingRatioMediumBouncy = 0.5f

    // 震荡效果低
    const val DampingRatioLowBouncy = 0.75f

    // 无震荡效果
    const val DampingRatioNoBouncy = 1f
}

分别代表震荡效果的高、中、低和无震荡效果,默认为无震荡效果,在实际开发中我们一般使用这四种预置的阻尼比值就能实现大部分动画效果,效果对比如下:

从上面的效果对比图中可以看出来,当阻尼比越小时动画震荡的次数越多、幅度越大,相反则震荡的次数越少、幅度越小,大于等于 1 时无震荡。

stiffness

stiffness是弹簧的刚度,刚度越大,抵抗弹簧变形的能力越强,回到原点位置的速度就越快,即动画得越快。

同样的 Compose 内置了 5 个刚度值,定义如下:

object Spring {

    // 高刚度
    const val StiffnessHigh = 10_000f

    // 中刚度
    const val StiffnessMedium = 1500f

    // 中低刚度
    const val StiffnessMediumLow = 400f

    // 低刚度
    const val StiffnessLow = 200f

    // 非常低刚度
    const val StiffnessVeryLow = 50f
}

将弹簧阻尼比设置为 DampingRatioHighBouncy时不同刚度值对应的效果如下:

在阻尼比相同的情况下不同刚度值对于动画震荡的幅度和次数是一样的,唯一不同的是速度,刚度值越高速度越快,动画时长越短,相反则速度越慢,动画时长越长。

visibilityThreshold

visibilityThreshold是动画的可视阈值,在这里的作用是当震荡幅度小于设置的可视阈值时动画将停止震荡并直接运动到目标值。

从文字上理解可能比较抽象,我们还是以上面的效果为例,当阻尼比设置为 DampingRatioHighBouncy、刚度设置为 StiffnessVeryLow时,通过设置可视阈值分别为默认值 null 、0.1.dp、1.dp、10.dp、100.dp 来看一下效果:

可以发现,当可视阈值设置得越大时,动画停止的越早。

实战

接下来我们就用 SpringSpec 实现一个简单的实战动画效果,一个点击图片缩放动画效果,使用SpringSpec来实现,代码如下:

// 目标大小
val target = 300.dp

// 缩放状态
var big by remember { mutableStateOf(false) }
// 根据状态的目标值
val targetValue = if (big) target else 100.dp

// 基于动画的图片大小
val size by animateDpAsState(
    targetValue,
    animationSpec = spring(
        // 阻尼比:中
        dampingRatio = Spring.DampingRatioMediumBouncy,
        // 刚度:低
        stiffness = Spring.StiffnessLow
    )
)


Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    Image(
        painter = painterResource(R.drawable.dog),
        contentDescription = "dog",
        modifier = Modifier
            .size(size)
            .clip(RoundedCornerShape(10.dp))
            .clickable {
                // 改变状态
                big = !big
            },
        contentScale = ContentScale.Crop
    )

}

最终动画效果:

可以发现,在一个普通的图片缩放动画上加上弹簧动画配置后,动画的视觉体验得到了成倍的增加,给用户带来了更好的体验。

最后

本篇详细介绍了弹簧动画规格 SpringSpec的参数配置及对应的使用效果,并用一个简单的实战效果体验了弹簧动画的魅力,至此 6 种动画规格配置已经探索了 4 个,下一篇我们将继续探索最后两个动画规格:RepeatableSpec(重复动画)、InfiniteRepeatableSpec(无限重复动画),请持续关注本专栏了解更多 Compose 动画相关内容。