androidkotlinanimationandroid-jetpack-compose

Jetpack Compose "shortest" rotate animation


I was trying to do a compass in jetpack compose. But I faced a problem with animating it. I have a @Composable that takes user phone rotation and rotate compass image in opposite direction. I use animateFloatAsState like this:

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
)

Everything looks fine but the problem occurs when rotation is changed from 1 to 359 or in the opposite way. Animation doesn't rotate 2 degrees to the left but goes 358 degrees to the right which looks bad. Is there any way to make rotate animation that would use the shortest way?


Solution

  • I ended up doing this:

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

    So basically I remembered the last rotation and based on this when a new rotation comes in I check which way (forward or backward) is shorter and then use it to update the target value.