· 5 min read Posted by Gustavo Fão Valvassori

Our journey with compose animations

Let's chat about a few pain points we had when we started doing animations with compose, how we tried to solve them, and how you can solve the problems now!
Vlad Bagacian - https://unsplash.com/pt-br/fotografias/mulher-sentada-no-penhasco-cinzento-d1eaoAabeXs
Credit: Vlad Bagacian - https://unsplash.com/pt-br/fotografias/mulher-sentada-no-penhasco-cinzento-d1eaoAabeXs

We at Touchlab have been playing with Compose since it’s beginning. Even before the first stable release, we were experimenting and trying to make the life of our clients easier.

One cool experiment we had in the past (around Aug 2021), was with Compose and Animations. Back then, compose already had a good API for animations, but there were a couple of things missing, and others that were a bit “verbose” to achieve. We discussed it in the “Compose Animations beyond the state change” article.

This year, almost 3 years later, we started working with Compose Multiplatform (CMP) for a client and decided to update it to be multiplatform and finally publish it. It worked as a “migration experiment” before we start doing real client work.

The Problems

The biggest pain point we had as an early adopter, is that we had to deal with missing features. When we started with Compose, the animations API wasn’t as robust as it is today.

Among the pain points we had the most relevant were: the lack of Easing animations, an easy way to delay the start of an infinite transition, and having a good APIs for multiple values animations.

The Project

To solve those problems, in 2021, we wrote two libraries:

  1. Easings: Implementation of all the Easing functions available in this site, fixing the problem with missing easing functions.
  2. Value Animator: Implementation of the delay, multiple value animation and a couple of syntax sugar/complementary APIs that are a bit verbose or not supported yet;

This year I converted this library into a CMP project, as an opportunity to practice migrating from Compose to CMP. This migration also helped me write the Jetpack Compose to Compose Multiplatform: Transition Guide.

The Compose Way

Nowadays, the Compose Animations API is much more mature and complete. Most of the pain points we had were solved in the Compose core library. Here is how you can solve those problems:

For the easings, the 1.2.0 release included the missing methods, so you can just use the official library. You can find the documentation here. All methods we had implemented are also available there.

For the “delay” problem, the Compose team implemented a more complete API and released in the 1.1.0 release. You can use the initialStartOffset argument in many animations APIs. This argument allows you to delay the start of the animation later, or move the animation to a “future state”.

// Before
private fun BouncingDots() {
    val ballsRange = (0..2)
    val ballSize = 12.dp
    val animators = ballsRange.map { index ->
        animateFloatValuesAsState( // Our animation library method
            values = floatArrayOf(0f, ballSize.value, 0f),
            startDelay = (1 + index) * 70L, // Delay the start of the animation
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 200 * ballsRange.size,
                ),
            )
        )
    }

    // Using animators values
}

// Now with official APIs
@Composable
private fun BouncingDots() {
    val ballsRange = (0..2)
    val ballSize = 12.dp
    val infiniteTransition = rememberInfiniteTransition("BouncingDots")
    val animators = ballsRange.map { index ->
        infiniteTransition.animateFloat( // Official API
            initialValue = 0f,
            targetValue = ballSize.value,
            animationSpec = infiniteRepeatable(
                animation = tween(
                    durationMillis = 100 * ballsRange.size,
                ),
                repeatMode = RepeatMode.Reverse,
                initialStartOffset = StartOffset((1 + index) * 70, StartOffsetType.Delay), // Setting the Delay
            ),
        )
    }

    // Using animators values
}

Lastly, for the multiple values animations, you can use the keyframes. The keyframes allow you to define the values at specific times, and the library will interpolate between them.

// Before
@Composable
fun ColorFadingTriangle() {
    val colorAnimation by animateColorValuesAsState( // Using our library method
        values = arrayOf(Color.Red, Color.Green, Color.Blue), // Just set the 
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 2500,
                easing = LinearEasing,
            ),
            repeatMode = RepeatMode.Reverse,
        ),
    )

    // Using animators values
}

// Now with official APIs
@Composable
fun ColorFadingTriangle() {
    val infiniteTransition = rememberInfiniteTransition("ColorFadingTriangle")
    val colorAnimation by infiniteTransition.animateValue( // Call animate value in the InfiniteTransition
        initialValue = Color.Red,
        targetValue = Color.Blue,
        typeConverter = remember { Color.VectorConverter(ColorSpaces.LinearSrgb) }, // Set the Vector Converter manually
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 2500

                // Define the "when" each color should appear 
                Color.Red atFraction 0f
                Color.Green atFraction 0.5f
                Color.Red atFraction 1f
            },
            repeatMode = RepeatMode.Reverse,
        ),
    )

    // Using animators values
}

Also, worth mentioning that all those pain points are already fully compatible with Compose Multiplatform!

Final Thoughts

When I first implemented the library, I thought of it as complimentary to the existing APIs. I understand that some of those solutions are not that easy to implement in the core library as it can introduce issues/bugs for other features, like the “Lookahead”. Our library didn’t implement anything magical that replaces the core library, but acted as an additional piece to improve DevEx.

Now that many of those pain points are solved by the core library, my library is not as useful anymore. Therefore, I decided to stop working on it.

The official library still has some items that can be improved, like the lack methods for the Infinite Transition. But that’s something that will probably be added with time. In the meantime, there are workarounds for those problems, and we are excited to continue seeing adoption and support improve.