Compose Animations beyond the state change
Tags: android, compose, animations, ui
Published: 2021-08-24 17:14:23

Jetpack Compose hit 1.0 a few weeks ago, and it came with a wonderful and robust animations API. With this new API, you will have total control over animations when the state changes. But when it comes to more complex scenarios, you may face some non-obvious paths. This article will explore some of these scenarios and help you understand how you can achieve your goal.

I'm about to discuss the problems I found when trying to implement the [AVLoadingIndicatorView](https://github.com/yonglan-whl/AVLoadingIndicatorView) library in Compose. And to guide you through this journey, I'll use the following loading indicators as examples.

![Kapture 2021-08-18 at 18.02.33](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7qrttsqqftz8zuch0gr5.gif) 

Also, all the code we discuss in this article is available in this repository:

{% github faogustavo/loading-indicator-demo no-readme %}

## BallScaleIndicator

Let's start with some simple animation. The animation consists in reducing alpha while increasing the scale from a circle. We can do that with just one value that will move from 0 to 1. So the scale will be the current value, and the alpha will be the complementary value (`1 - currentValue`). As this is a loading animation, we will have to configure it to repeat forever.

TL;DR; we would have to do something like this:

```kotlin
@Composable
fun BallScaleIndicator() {
    val animationProgress by animateFloatAsState(
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 800)
        )
    )

    Ball(
        modifier = Modifier
            .scale(animationProgress)
            .alpha(1 - animationProgress),
    )
}
```

But when you run your app, this is the result:

![Loading indicator not working](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2y6vxpgvpn2nd0w6iris.gif)


Nothing happens. Why? In the first lines from this article, we said that Compose has an awesome API to animate state changes, but this is not some state change. We would have to add one variable to control the target state and change it to start the animation. Also, when the view is rendered, we would have to change the value to start the animation.

```kotlin
@Composable
fun BallScaleIndicator() {
    // Create one target value state that will 
    // change to start the animation
    var targetValue by remember { mutableStateOf(0f) }

    // Update the attribute on the animation
    val animationProgress by animateFloatAsState(
        targetValue = targetValue,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 800)
        )
    )

    // Use the SideEffect helper to run something
    // when this block runs
    SideEffect { targetValue = 1f }

    Ball(
        modifier = Modifier
            .scale(animationProgress)
            .alpha(1 - animationProgress),
    )
}
```

And now, everything works 🎉

![Loading indicator with side effect](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ryhruwmikkgk87yj2b1t.gif) 
 
But we have another problem now. The `SideEffect` will run every time the view is recomposed. So as the animation changes the state value on each iteration, we will run this effect many times. It makes the animation work, but it may not scale nicely and cause some UI issues. 

Compose also provides a way to animate values using transitions. One type of transition is the InfiniteTransition. It provides you a syntax with the initial and final values as parameters and it's automatically started when created (no need for SideEffects). To use it, you need to create an instance of it using the `rememberInfiniteTransition` method and call the `animateFloat` function to have an animated state.

After a small refactor, this is the result.

```kotlin
@Composable
fun BallScaleIndicator() {
    // Creates the infinite transition
    val infiniteTransition = rememberInfiniteTransition()

    // Animate from 0f to 1f
    val animationProgress by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 800)
        )
    )

    Ball(
        modifier = Modifier
            .scale(animationProgress)
            .alpha(1 - animationProgress),
    )
}
```

And the result animation is the same, but without using side effects on this function.

![Loading indicator with infinite transition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dgbtq690srggzutyze7p.gif)
 

## BallPulseSyncIndicator

Alright, we got the first one. Let's try something more complex. This one consists of three balls jumping in synchrony. The easiest way to achieve this is to delay the start of each animation. We can have an animation time of 600ms and start it with a delay of 70ms for each ball.

On a quick search into the compose animation API, we find that the `tween` animation has a property `delayMillis` that we can use to implement this behavior. And to animate the values, we can keep the InfiniteTransition. So let's start working with it.

```kotlin
@Composable
fun BallPulseSyncIndicator() {
    val infiniteTransition = rememberInfiniteTransition()
    val animationValues = (1..3).map { index ->
        infiniteTransition.animateFloat(
            initialValue = 0f,
            targetValue = 12f,
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 300,
                    delayMillis = 70 * index,
                ),
                repeatMode = RepeatMode.Reverse,
            )
        )
    }

    Row {
        animationValues.forEach { animatedValue ->
            Ball(
                modifier = Modifier
                    .padding(horizontal = 4.dp)
                    .offset(y = animatedValue.value.dp),
            )
        }
    }
}
```

Sound good, right? But when we see the animation, we will notice something weird.

![Loading indicator losing synchrony](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2yjut01re4h206h1eq37.gif)
 
You can see that, after some running time, the animation loses synchrony and starts behaving weirdly. The reason for that is the property that we used to delay the animation. It applies the delay to each iteration, not just the first one.

The solution to this is to use the Coroutines Animation API. It's provided by the Compose animations and has a method called `animate`. It has a syntax pretty similar to the `animateFloat` from transition. With that in mind, we can use the `delay` function from Coroutines before starting the animation. This will guarantee the correct behavior.

```kotlin
@Composable
fun BallPulseSyncIndicator() {
    val animationValues = (1..3).map { index ->
        var animatedValue by remember { mutableStateOf(0f) }

        LaunchedEffect(key1 = Unit) {
            // Delaying using Coroutines
            delay(70L * index)

            animate(
                initialValue = 0f,
                targetValue = 12f,
                animationSpec = infiniteRepeatable(
                    // Remove delay property
                    animation = tween(durationMillis = 300),
                    repeatMode = RepeatMode.Reverse,
                )
            ) { value, _ -> animatedValue = value }
        }

        animatedValue
    }

    Row {
        animationValues.forEach { animatedValue ->
            Ball(
                modifier = Modifier
                    .padding(horizontal = 4.dp)
                    .offset(y = animatedValue.dp),
            )
        }
    }
}
```

Now, the animation will keep synchronized, even after some time.

![Loading indicator with delay to start](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cukgnsidgxqzu6nuvj21.gif) 

## TriangleSkewSpinIndicator

Alright, let's go to the next one. This triangle indicator has two animations (rotation on the X-axis and Y-axis), but you need to wait for the previous one to execute to start the next one. So if we put that in a timeline, we will have something like this:

![Untitled](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/331147xkh4qvuijod005.png)

The easiest way to evaluate something like this is to handle the animation as one thing with groups. Each group will be formed by the value and the next item in the list and take the same amount to evaluate.

![Untitled (1)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c9tzqzqez975rsfejzug.png)

Rewriting the image in Kotlin, we would have something like this:

```kotlin
@Composable
fun animateValues(
    values: List<Float>,
    animationSpec: AnimationSpec<Float> = spring(),
): State<Float> {
    // 1. Create the groups zipping with next entry
    val groups by rememberUpdatedState(newValue = values.zipWithNext())

    // 2. Start the state with the first value
    val state = remember { mutableStateOf(values.first()) }

    LaunchedEffect(key1 = groups) {
        val (_, setValue) = state

        // Start the animation from 0 to groups quantity
        animate(
            initialValue = 0f,
            targetValue = groups.size.toFloat(),
            animationSpec = animationSpec,
        ) { frame, _ ->
            // Get which group is being evaluated
            val integerPart = frame.toInt()
            val (initialValue, finalValue) = groups[frame.toInt()]

            // Get the current "position" from the group animation
            val decimalPart = frame - integerPart

            // Calculate the progress between the initial and final value
            setValue(
                initialValue + (finalValue - initialValue) * decimalPart
            )
        }
    }

    return state
}
```

With this one implemented, the animation process will be pretty simple. Just create two variables to hold the X and Y rotation, and update the view using the `.graphicsLayer` modifier.

```kotlin
@Composable
fun TriangleSkewSpinIndicator() {
    val animationSpec = infiniteRepeatable<Float>(
        animation = tween(
            durationMillis = 2500,
            easing = LinearEasing,
        )
    )
    val xRotation by animateValues(
        values = listOf(0f, 180f, 180f, 0f, 0f), 
        animationSpec = animationSpec
    )
    val yRotation by animateValues(
        values = listOf(0f, 0f, 180f, 180f, 0f), 
        animationSpec = animationSpec
    )

    Triangle(
        modifier = Modifier.graphicsLayer(
            rotationX = xRotation,
            rotationY = yRotation,
        )
    )
}
```

And this is the result:

![Triangle indicator example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qglti4cizw2e9rmq6qk5.gif) 

## Final Thoughts

The Jetpack Compose comes with an incredible animation API. It provides you many ways to implement all kinds of animations you may need. But, the current API is a bit different from the imperative version, and some paths will not be that obvious.

To help you have a smooth transition to compose, Touchlab has started a project to help you with all these non-obvious paths.

{% github touchlab-lab/compose-animations %}

---

Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @faogustavo on [Twitter](https://twitter.com/faogustavo), the [Kotlin Slack](https://slack.kotlinlang.org/), or [AndroidDevBr Slack](http://slack.androiddevbr.org/). And if you find all this interesting, maybe you'd like to [work with](https://touchlab.co/contact-us/) or [work at](https://touchlab.co/careers-3/) Touchlab.