Jetpack Compose“最短”旋转动画

问题描述 投票:0回答: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
)

一切看起来都很好,但是当

rotation
1
更改为
359
或以相反的方式更改时,就会出现问题。动画不会向左旋转
2
度,而是向右旋转
358
度,这看起来很糟糕。有没有什么方法可以用最短的方式制作旋转动画?

android kotlin animation android-jetpack-compose
5个回答
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
投票

我假设您可以(或可以获得)访问当前旋转值(即当前角度),请将其存储。

那么,

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


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
可能不会完全按照其应有的方式运行。


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

0
投票

对于那些通过比OP的原始问题更通用的Google搜索到达的人来说,这是一个简单的解决方案(例如“撰写动画旋转”)。

我喜欢将旋转逻辑封装在自定义

remember
函数中。

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue

@Composable
fun rememberAnimationRotation(key: Int): Float {
    val targetRotation by animateFloatAsState(
        targetValue = key * 360F,     // Rotate 360 degrees per `key` increment
        animationSpec = tween(1500),  // Complete animation in 1.5 seconds
        label = "rotation",
    )
    return targetRotation
}

从那里,您可以将旋转动画应用到任何支持

Modifier.rotate
的东西。考虑一下这个按钮,每次按下都会旋转。

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.Text

@Composable
fun RotatingButton() {
    /* Rotates on every button press */
    var pressCount by remember { mutableIntStateOf(0) }
    val targetRotation = rememberAnimationRotation(key = pressCount)
    Button(onClick = { pressCount++ }, modifier = Modifier.rotate(targetRotation)) {
        Text("Spin")
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.