【问题标题】:Jetpack Compose "shortest" rotate animationJetpack Compose“最短”旋转动画
【发布时间】:2022-04-13 23:07:47
【问题描述】:

我试图在 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 度,这看起来很糟糕。有什么方法可以使用最短的方式制作旋转动画?

【问题讨论】:

    标签: android kotlin animation android-jetpack-compose


    【解决方案1】:

    我假设您已经(或可以获得)访问当前旋转值(即当前角度)的权限,将其存储起来。

    那么,

    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 是一个自定义修饰符,应该不难实现。使用内置的旋转修改器来构建它。逻辑将保持不变

    【讨论】:

    • 也许我的问题并不清楚。 rotation 是介于 0359 之间的整数。这是相对于北方的电话旋转。所以当我有这个值时,我必须以相反的方向旋转指南针图像,这就是我使用-rotation 的原因。我试过你的代码,但它的行为很奇怪。 estAngle 应该在哪里使用?
    • 如果旋转值(在 1 到 359 的情况下为 358)大于反之,即 360 - 值(或此处为 360 - 358 = 2),则设置目标值后者的动画。负号是因为假设正旋转顺时针旋转而负旋转逆时针旋转。所以无论什么更短,我们都会带着适当的标志走那条路。我之前在想别的办法。我觉得 estAngle 现在没用了
    • 感谢您的帮助:D 我必须以另一种方式做到这一点,但您仍然帮助了我
    【解决方案2】:

    我设法通过将标题转换为其正弦和余弦并对其进行插值来解决这个问题。这将使用最短的旋转正确插值。

    为了实现这一点,我创建了 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 时没有动画。当我弄清楚时我会在这里更新。
    • 其实是小数字到180之间动画快很多。
    【解决方案3】:

    我最终这样做了:

    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
        )
    )
    

    所以基本上我记得最后一次旋转,当新的旋转进来时,我会检查哪种方式(向前或向后)更短,然后用它来更新目标值。

    【讨论】:

      【解决方案4】:
      @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
        )
      )
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-10-23
        • 1970-01-01
        • 1970-01-01
        • 2022-10-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多