Android Compose之Animatable动画停止使用详解

前言

前面讲了Animatable的基础以及衰减动画的用法,本篇则主要讲解 Animatable 动画的停止,动画的停止情况主要分为四种,如下:

  • 动画正常运行完成后停止
  • 动画被打断停止
  • 主动停止动画
  • 动画触达边界停止

第一种很好理解,就是动画按照我们设计的参数正常运行完成的情况,这种情况属于正常停止,此时动画返回结果的 endReason为 AnimationEndReason.Finished(在《android Compose 动画Animatable的使用》一文中有详细介绍),本文主要介绍剩下三种停止情况。

打断停止

顾名思义即动画在运行过程中被打断,那么是被什么打断呢?被同一个 Animatable的另一个动画打断,简单的说就是当 Animatable在执行某一个动画的过程中,此时再使用同一个 Animatable 去开启另一个动画,此时就会打断正在运行中的动画,即停止正在运行中的话执行新的动画。

那么为什么要打断正在运行中的动画呢?不能两个动画一起执行吗?假设一个动画是将方块移动到 200dp 的位置,另一个动画是将方块移动到 0dp 的位置,如果不会被打断两个动画可以同时执行,那就会出现方块一会儿往 200dp 位置一会儿往 0dp 移动的闪烁情况,显然动画效果不符合预期,所以被设计成了后一个动画会打断前一个动画。

下面就用一个实例演示动画的打断,代码如下:

  1. // 方块颜色,默认为蓝色
  2. var backgroundColor by remember { mutableStateOf(Color.Blue) }
  3. // 动画实例
  4. val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }
  5. // 获取协程作用域
  6. val scope = rememberCoroutineScope()
  7. Box {
  8.      // 动画方块
  9.      Box(
  10.          Modifier
  11.              // 使用动画值
  12.              .padding(start = animatable.value, top = 30.dp)
  13.              .size(100.dp, 100.dp)
  14.              .background(backgroundColor)
  15.              .clickable { // 点击事件
  16.                  // 启动动画
  17.                  scope.launch {
  18.                      animatable.animateTo(200.dp,
  19.                      // 为了方便看到效果,动画时间设置为 1000ms
  20.                      animationSpec = tween(durationMillis = 1000)
  21.                      )
  22.                  }
  23.              }
  24.      )
  25.      // 按钮,用于开启新动画
  26.      Button(onClick = {
  27.          // 修改方块颜色,方便观察区分两个动画
  28.          backgroundColor = Color.Cyan
  29.          // 启动新动画
  30.          scope.launch {
  31.              animatable.animateTo(50.dp, animationSpec = tween(durationMillis = 1000))
  32.          }
  33.      }, Modifier.padding(top = 170.dp, start = 70.dp)) {
  34.          Text(text = “Next”, style = TextStyle(fontSize = 10.sp))
  35.      }
  36. }

界面上添加了一个方块和一个按钮,点击方块执行动画移动到 200dp 位置,点击按钮执行动画移动到 50dp 位置,为了区分两个动画动画执行时为方块设置了不同的颜色,运行效果如下:

-1

从上面的效果可以看出,对同一个 Animatable开启新的动画确实会打断正在运行的动画。

除了上面演示的 animateTo可以打断动画以外,Animatable 的snapToanimateDecay同样可以打断动画。

主动停止

前面介绍的是新动画打断正在运行的动画,那么如果我们想主动停止一个Animatable动画该怎么办呢?很简单,Animatable提供了stop方法用于停止动画。

示例代码如下:

  1. var backgroundColor by remember { mutableStateOf(Color.Blue) }
  2. // 动画实例
  3. val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }
  4. val scope = rememberCoroutineScope()
  5. Box {
  6.      // 动画方块
  7.      Box(
  8.          Modifier
  9.              // 使用动画值
  10.              .padding(start = animatable.value, top = 30.dp)
  11.              .size(100.dp, 100.dp)
  12.              .background(backgroundColor)
  13.              .clickable { // 点击事件,开启动画
  14.                  scope.launch {
  15.                      animatable.animateTo(200.dp,
  16.                      // 为了方便观察动画效果,动画时长设置为 1000ms
  17.                      animationSpec = tween(durationMillis = 1000)
  18.                      )
  19.                  }
  20.              }
  21.      )
  22.      // 停止按钮
  23.      Button(onClick = {
  24.          // 修改方块颜色
  25.          backgroundColor = Color.Cyan
  26.          // 停止动画
  27.          scope.launch {
  28.              animatable.stop()
  29.          }
  30.      }, Modifier.padding(top = 170.dp, start = 70.dp)) {
  31.          Text(text = “Stop”, style = TextStyle(fontSize = 10.sp))
  32.      }
  33. }

需要注意的是 stop方法也是一个挂起函数,需要在协程中执行,效果如下:

-2

触达边界停止

Animatable可以通过 updateBounds函数为动画设置边界值,当动画运动到边界时会立即停止动画,updateBounds定义如下:

  1. fun updateBounds(lowerBound: T? = this.lowerBound, upperBound: T? = this.upperBound)

updateBounds方法有两个参数 lowerBoundupperBound分别为动画的边界下限值和上限值,默认为 null即不做限制,可以单独设置上限和下限的值,当设置对应值后,动画运行过程中动画值达到边界值时就会立即停止动画。

示例代码如下:

  1. // 创建状态 通过状态驱动动画
  2. var moveToRight by remember { mutableStateOf(false) }
  3. // 动画实例
  4. val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }
  5. // 设置动画边界
  6. animatable.updateBounds(lowerBound = 10.dp, upperBound = 200.dp)
  7. val scope = rememberCoroutineScope()
  8. Box(
  9.      Modifier
  10.          // 使用动画值
  11.          .padding(start = animatable.value, top = 30.dp)
  12.          .size(100.dp, 100.dp)
  13.          .background(Color.Blue)
  14.          .clickable { // 点击事件
  15.              // 修改状态
  16.              moveToRight = !moveToRight
  17.              scope.launch {
  18.                  // 执行动画
  19.                  animatable.animateTo(
  20.                      // 根据状态设置动画的目标值,分别是向右到 400dp 和 向左到 -100dp 位置
  21.                      if (moveToRight) 400.dp else 100.dp,
  22.                      animationSpec = tween(durationMillis = 1000)
  23.                  )
  24.              }
  25.          }
  26. )

上面代码分别设置了下限值为 10dp、上限值为 200dp,同时动画目标值分别设置为 -100dp 和 400dp,看一下运行效果:

-3

可以看出来,虽然动画目标设置分别设置了 -100dp 和 400dp,但是因为我们设置了边界值为 10dp 和 200dp,所以动画向右运动时到达边界值即 200dp 位置时就停止了,向左同样的到达边界值 10dp 也停止了动画,这就是动画边界的作用。

多维边界

之前介绍了 Compose 动画是可以作用于多维数值的,比如作用于 Size、Offset、React 等数据时就是多维的动画,此时对动画设置边界后,动画目标值只要有其中一维的数值达到边界就会立即停止,并不会等到所有维的数值都达到边界才会停止。

下面用一个示例来举例说明,还是上面的方块动画,上面只进行了横向的动画,如果我们要同时进行横向和竖向的动画,可以使用 Offset 来进行动画,然后对其进行边界设置来观察效果,代码如下:

  1. // 创建状态 通过状态驱动动画
  2. var moveToRight by remember { mutableStateOf(false) }
  3. // 动画实例
  4. val animatable = remember { Animatable(Offset(10f, 30f), Offset.VectorConverter) }
  5. // 设置边界值,下限:Offset(10f, 30f) 上限:Offset(400f, 200f)
  6. animatable.updateBounds(lowerBound = Offset(10f, 30f), upperBound = Offset(400f, 200f))
  7. val scope = rememberCoroutineScope()
  8. Box {
  9.      Box(
  10.          Modifier
  11.              // 使用动画值
  12.              .padding(start = animatable.value.x.dp, top = animatable.value.y.dp)
  13.              .size(100.dp, 100.dp)
  14.              .background(Color.Blue)
  15.              .clickable {
  16.                  // 修改状态
  17.                  moveToRight = !moveToRight
  18.                  scope.launch {
  19.                      animatable.animateTo(
  20.                      // 根据状态设置动画的目标值
  21.                      // 分别为向右和向下的 Offset(400f,400f)
  22.                      // 向上和向左的 Offset(-100f,0f)
  23.                      if (moveToRight) Offset(400f,400f) else Offset(-100f,0f),
  24.                      animationSpec = tween(durationMillis = 1000)
  25.                      )
  26.                  }
  27.              }
  28.      )
  29. }

运行效果:

-4

可以发现,上限设置为 Offset(400f, 200f)即 x 轴最大为 400dp、y 轴最大为 200dp,动画目标值为Offset(400f,400f),当方块移动到 y 坐标为 200dp 时 y 坐标的值达到边界值,动画就停止了,此时 x 坐标值并未达到边界值。同样的往回执行时 x 坐标先触发达到边界值 10dp 时停止了动画。

如果就是想让动画都达到边界才停止,此时不应该采用多维动画的方式,而是应该使用多个单维动画,对其分别设置边界即可。

动画停止监听

动画停止可分为异常停止和正常停止,其中打断和主动停止动画属于异常停止,动画运行完成或达到边界后停止属于正常停止。

异常停止

一个动画打断另一个动画,或调用 stop主动停止动画都属于异常停止,此时动画会抛出 CancellationException的异常,在代码中可通过捕获该异常来监听动画的异常停止,代码如下:

  1. scope.launch {
  2.      try {
  3.          // 异常停止时动画会抛出异常,不会正常返回动画结果
  4.          val animationResult = animatable.animateTo(200.dp,
  5.              animationSpec = tween(durationMillis = 1000)
  6.          )
  7.          // 动画异常停止时会抛出异常,下面的代码不会被执行
  8.          // do something
  9.      } catch (e: CancellationException) {
  10.          Log.e(“ANIMATOIN”, “动画异常停止”)
  11.      }
  12. }

正常停止

动画触达边界停止属于正常停止,此时动画会正常返回结果,从结果中的 endReason可判断动画是否为触达边界停止,代码如下:

  1. scope.launch {
  2.      val animationResult = animatable.animateTo(200.dp,
  3.          animationSpec = tween(durationMillis = 1000)
  4.      )
  5.      // 判断动画是否触达边界停止
  6.      if(animationResult.endReason == AnimationEndReason.BoundReached){
  7.          // do something
  8.      }
  9. }

同时动画结果还能拿到动画停止时的速度等数据,这样就能通过监听动画边界停止进行自定义的处理,比如结合上一篇介绍的衰减动画让动画到达边界后反向运动,代码如下:

  1. @Preview
  2. @Composable
  3. fun AnimationBound3() {
  4.      // 动画实例
  5.      val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }
  6.      // 设置边界值
  7.      animatable.updateBounds(upperBound = 200.dp, lowerBound = 10.dp)
  8.      val scope = rememberCoroutineScope()
  9.      val splineBasedDecay = rememberSplineBasedDecay<Dp>()
  10.      Box(
  11.          Modifier
  12.              // 使用动画值
  13.              .padding(start = animatable.value, top = 30.dp)
  14.              .size(100.dp, 100.dp)
  15.              .background(Color.Blue)
  16.              .clickable {
  17.                  scope.launch {
  18.                      // 启动动画,设置初始速度为 3000dp
  19.                      val animationResult = animatable.animateDecay(3000.dp, splineBasedDecay)
  20.                      // 判断是否为到达边界停止
  21.                      if(animationResult.endReason == AnimationEndReason.BoundReached){
  22.                      // 执行反向动画,初始速度取动画结束时速度的负数
  23.                      reverseAnimation(animatable, animationResult.endState.velocity, splineBasedDecay)
  24.                      }
  25.                  }
  26.              }
  27.      )
  28. }
  29. /// 反向执行动画
  30. private suspend fun reverseAnimation(
  31.      animatable: Animatable<Dp, AnimationVector1D>,
  32.      initialVelocity: Dp,
  33.      splineBasedDecay: DecayAnimationSpec<Dp>
  34. ) {
  35.      val result = animatable.animateDecay(initialVelocity, splineBasedDecay)
  36.      // 判断是边界停止时递归执行反向动画直到动画非边界停止
  37.      if(result.endReason == AnimationEndReason.BoundReached){
  38.          reverseAnimation(animatable, result.endState.velocity, splineBasedDecay)
  39.      }
  40. }

运行效果如下:

-5

这样就通过监听动画的停止实现了动画到达边界后反向运动的效果。

最后

本篇介绍了 Animatable 动画的停止,包括打断停止、主动停止和到达边界停止,介绍了不同停止的实现方式以及对动画停止的监听处理。下一篇我们继续探索 Compose 动画的其他使用,请持续关注本专栏了解更多 Compose 动画内容。

标签

发表评论