Jetpack Compose“最短”的旋转动画

5

我正在尝试在Jetpack Compose中实现指南针,但遇到了动画问题。 我有一个@Composable函数,它接受用户手机的旋转角度并将指南针图像以相反方向旋转。我使用animateFloatAsState来实现动画效果,代码如下:

val angle: Float by animateFloatAsState(
    targetValue = -rotation, \\ rotation is retrieved as argument
    animationSpec = tween(
        durationMillis = UPDATE_FREQUENCY, \\ rotation is retrieved with this frequency
        easing = LinearEasing
    )
)

Image(
    modifier = Modifier.rotate(angle),
    // rest of the code for image
)

一切看起来都很好,但是当将rotation1修改为359或者反过来时就会出现问题。动画不会向左旋转2度,而是向右旋转358度,这看起来不好。有没有办法制作旋转动画,使用最短的路径?

4个回答

4
我最终做了这个:

我最终做了这个:

val (lastRotation, setLastRotation) = remember { mutableStateOf(0) } // this keeps last rotation
var newRotation = lastRotation // newRotation will be updated in proper way
val modLast = if (lastRotation > 0) lastRotation % 360 else 360 - (-lastRotation % 360) // last rotation converted to range [-359; 359]
    
if (modLast != rotation) // if modLast isn't equal rotation retrieved as function argument it means that newRotation has to be updated
{
    val backward = if (rotation > modLast) modLast + 360 - rotation else modLast - rotation // distance in degrees between modLast and rotation going backward 
    val forward = if (rotation > modLast) rotation - modLast else 360 - modLast + rotation // distance in degrees between modLast and rotation going forward
    
    // update newRotation so it will change rotation in the shortest way
    newRotation = if (backward < forward)
    {
        // backward rotation is shorter
        lastRotation - backward
    }
    else
    {
        // forward rotation is shorter (or they are equal)
        lastRotation + forward
    }
    
    setLastRotation(newRotation)
}

val angle: Float by animateFloatAsState(
    targetValue = -newRotation.toFloat(),
    animationSpec = tween(
        durationMillis = UPDATE_FREQUENCY,
        easing = LinearEasing
    )
)

基本上,我记住了最后一个旋转方向,并且在新的旋转方向出现时,我会检查哪种方式(向前或向后)更短,然后使用它来更新目标值。


3

我通过将标题转换为正弦和余弦,然后进行插值来解决了这个问题。这样可以使用最短的旋转正确地进行插值。

为了实现这一点,我创建了一个TwoWayConverter的实现,这是Compose用于将值转换为AnimationVector的方式。正如我已经提到的,我将度数值转换为由正弦和余弦组成的二维向量。从它们中返回度数值时,我使用反正切函数。

val Float.Companion.DegreeConverter
    get() = TwoWayConverter<Float, AnimationVector2D>({
        val rad = (it * Math.PI / 180f).toFloat()
        AnimationVector2D(sin(rad), cos(rad))
    }, {
        ((atan2(it.v1, it.v2) * 180f / Math.PI).toFloat() + 360) % 360
    })

在此之后,您可以将旋转值进行动画处理:

val animatedHeading by animateValueAsState(heading, Float.DegreeConverter)

唯一需要注意的是,由于角度的正弦和余弦是动态的,转换默认情况下不是线性的,而且在 animate 函数中定义的任何 animationSpec 可能无法完全按照预期行事。


这真的很好。干得好。 我注意到当提供0和180时,此解决方案不会进行动画处理。 我弄清楚后会在这里更新。 - Rab Ross
实际上,这是因为在较小的数字和180之间,动画速度更快。 - Rab Ross

3

我假设你已经有(或可以获得)旋转的当前值(即当前角度),请将其存储。

然后,

val angle: Float by animateFloatAsState(
    targetValue = if(rotation > 360 - rotation) {-(360 - rotation)} else rotation
    animationSpec = tween(
        durationMillis = UPDATE_FREQUENCY, \\ rotation is retrieved with this frequency
        easing = LinearEasing
    )
)

Image(
    modifier = Modifier.rotateBy(currentAngle, angle), //Custom Modifier
    // rest of the code for image
)

rotateBy 是一个自定义修饰符,实现起来应该不难。使用内置的 rotate 修饰符来构造它。逻辑保持不变


也许我在问题上没有表述清楚。rotation 是一个介于 0359 之间的整数。它是相对于北方的手机旋转角度。因此,当我有这个值时,我必须将指南针图像向相反方向旋转,这就是为什么我使用 -rotation 的原因。我尝试了您的代码,但它的行为很奇怪。estAngle 应该在哪里使用? - iknow
如果旋转值在你的情况下是1到359中的358,并且大于另一种方式,即360-值(或这里的360-358=2),则将动画的目标值设置为后者。负号是因为假设正旋转顺时针旋转,而负旋转逆时针旋转。所以无论哪种方式更短,我们都会用适当的符号走那条路。我之前考虑过其他的方法。我认为estAngle现在没用了。 - Richard Onslow Roper
谢谢您的帮助:D 我必须以另一种方式完成它,但仍然感谢您的帮助。 - iknow

2
@Composable
private fun smoothRotation(rotation: Float): MutableState<Float> {
  val storedRotation = remember { mutableStateOf(rotation) }

  // Sample data
  // current angle 340 -> new angle 10 -> diff -330 -> +30
  // current angle 20 -> new angle 350 -> diff 330 -> -30
  // current angle 60 -> new angle 270 -> diff 210 -> -150
  // current angle 260 -> new angle 10 -> diff -250 -> +110

  LaunchedEffect(rotation){
    snapshotFlow { rotation  }
      .collectLatest { newRotation ->
        val diff = newRotation - storedRotation.value
        val shortestDiff = when{
          diff > 180 -> diff - 360
          diff < -180 -> diff + 360
          else -> diff
        }
        storedRotation.value = storedRotation.value + shortestDiff
      }
  }

  return storedRotation
}

这是我的代码

val rotation = smoothRotation(-state.azimuth)


val animatedRotation by animateFloatAsState(
  targetValue = rotation.value,
  animationSpec = tween(
    durationMillis = 400,
    easing = LinearOutSlowInEasing
  )
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接