Jetpack Compose "shortest" 旋转动画
Jetpack Compose "shortest" rotate animation
我试图在 Jetpack Compose 中做一个指南针。但是我在制作动画时遇到了问题。
我有一个 @Composable
可以让用户 phone 旋转并以相反的方向旋转罗盘图像。我这样使用 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 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 是一个自定义修饰符,应该不难实现。使用内置的旋转修改器来构建它。逻辑将保持不变
我最终这样做了:
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
)
)
所以基本上我记住了最后一次旋转,并基于此当新的旋转进来时我检查哪条路(向前或向后)更短,然后用它来更新目标值。
我设法通过将航向转换为正弦和余弦并对其进行插值来解决这个问题。这将使用最短旋转正确插值。
为了实现这一点,我创建了一个 TwoWayConverter
的实现,Compose 使用它来将值转换为 AnimationVector
。正如我已经提到的,我将度值转换为由正弦和余弦组成的二维向量。从他们那里,我 return 使用反正切函数返回度数。
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)
唯一的问题是,由于角度的正弦和余弦是动画的,我认为默认情况下过渡不是线性的,动画函数中定义的任何 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
)
)
我试图在 Jetpack Compose 中做一个指南针。但是我在制作动画时遇到了问题。
我有一个 @Composable
可以让用户 phone 旋转并以相反的方向旋转罗盘图像。我这样使用 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 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 是一个自定义修饰符,应该不难实现。使用内置的旋转修改器来构建它。逻辑将保持不变
我最终这样做了:
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
)
)
所以基本上我记住了最后一次旋转,并基于此当新的旋转进来时我检查哪条路(向前或向后)更短,然后用它来更新目标值。
我设法通过将航向转换为正弦和余弦并对其进行插值来解决这个问题。这将使用最短旋转正确插值。
为了实现这一点,我创建了一个 TwoWayConverter
的实现,Compose 使用它来将值转换为 AnimationVector
。正如我已经提到的,我将度值转换为由正弦和余弦组成的二维向量。从他们那里,我 return 使用反正切函数返回度数。
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)
唯一的问题是,由于角度的正弦和余弦是动画的,我认为默认情况下过渡不是线性的,动画函数中定义的任何 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
)
)