Altline
Altline

Reputation: 383

Compose: Infinite animation with variable speed

I have this code to get an infinite rotation animation:

@Composable
fun RotatingObject(rpm: Int) {
    val infiniteTransition = rememberInfiniteTransition()
    val rotation by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 1000,
                easing = LinearEasing
            )
        )
    )

    Surface(
        Modifier
            .size(100.dp)
            .graphicsLayer { rotationZ = rotation},
        color = Color.Gray
    ) {}
}

I want the rpm parameter to define the number of revolutions per minute the object should make while spinning. I have tried setting durationMillis to 60000 / rpm but the speed stays the same after rpm changes.

How can I get the speed to change after the initial composition?

Requirements
The rotation angle should not jump or jitter - it should always continually change based on the current rpm.
The solution should be friendly to animated rpm values to allow for smoothly changing speed over time.
I would prefer a solution that doesn't cause avoidable recompositions.


Edit:

After trying countless different things, each of which had something wrong with it, I'm going to share the least wrong outcome.

@Composable
fun RotatingObject(rpm: Int) {
    var rotation by remember { mutableStateOf(0f) }
    val infiniteTransition = rememberInfiniteTransition()
    val ticker by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 1000,
                easing = LinearEasing
            )
        )
    )

    LaunchedEffect(ticker) {
        rotation += 0.1f * rpm
    }

    Surface(
        Modifier
            .size(100.dp)
            .graphicsLayer { rotationZ = rotation},
        color = Color.Gray
    ) {}
}

I track the rotation value as state and manually change it inside a LaunchedEffect. The relaunching of the effect is controlled by the infinite transition. This makes the rotation update at the expected frame rate. 0.1f * rpm is the conversion of rpm to 'degrees per frame' at 60Hz.

So, this behaves as I want it to, however, in order to get here I made an assumption that I feel is not safe to make. This code requires that the effect is relaunched at a constant frequency of 60Hz. Since I didn't see this defined anywhere, this might just be the case on my system.

Also, if I do more operations inside the effect, it sometimes decides not to update the rotation at all, meaning that this is not a reliable way to do this. Therefore I feel that this is not a correct solution and I'm afraid it may not behave the same on different systems or at different times even. I currently have no choice but to use this, so if you have any ideas or suggestions, please share.

Upvotes: 5

Views: 2061

Answers (1)

Jakub Kostka
Jakub Kostka

Reputation: 895

While the question is old, it may be helpful for others as I needed the same behavior and found a solution. In Compose AnimationSpec you can explicitly define the values for each frame. It is no different for infiniteRepeatable animation, as it has animationSpec argument. An example from AnimationSpec which is very telling is as follows:

keyframes {
            0f at 0 //ms
            0.4f at 75 // ms
            0.4f at 225 // ms
            0f at 375 // ms
            durationMillis = 375
        }

In my case, I wanted a simple solution for breathing animation, but the exhalation is longer than the inhalation. Thus, for my specific case, I implemented the infinite animation like this:

val infiniteTransition = rememberInfiniteTransition(label = "infiniteScaleBackground")
    val liveScaleBackground by infiniteTransition.animateFloat(
        initialValue = 1f,
        targetValue = 1.2f,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 5000
                1.2f at 2000 using LinearEasing // Takes 2 seconds to reach 1.2f
                1f at 5000 using LinearEasing // Takes 3 seconds to return to 1f
            },
            repeatMode = RepeatMode.Restart
        ),
        label = "liveScaleBackground"
    )

Note, that I have the repeatMode on RepeatMode.restart. That's because, within the keyFrames, I return to the starting point within the single cycle.

Upvotes: 0

Related Questions