我试图在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
度,这看起来很糟糕。有没有什么方法可以用最短的方式制作旋转动画?
我最终这样做了:
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
)
)
所以基本上我记住了最后一次旋转,并基于此,当新的旋转出现时,我检查哪种方式(向前或向后)更短,然后用它来更新目标值。
我假设您可以(或可以获得)访问当前旋转值(即当前角度),请将其存储。
那么,
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 是一个自定义修饰符,实现起来应该不难。使用内置的旋转修饰符来构造它。逻辑将保持不变
我通过将标题转换为正弦和余弦并进行插值来解决这个问题。这将使用最短旋转正确插值。
为了实现这一目标,我创建了
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
可能不会完全按照其应有的方式运行。
@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
)
)
对于那些通过比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")
}
}