Floating in Space - Animations with Compose and Canvas

Reading time: about 4 minutes

Published

In my previous blog post, Paint the Stars — Drawing with Compose and Canvas, I shared how I wanted to be better with Canvas and Compose and created an illustration with planets and stars. This blog post shares how to animate those elements. In the end, the result will look like this:

Full code is available in this snippet.

Animations

Stars

Let's start with the stars. In the previous blog post, we defined the Star data class like this:

data class Star(
    val size: Float,
    val topLeft: Offset,
    val color: Color,
)

From these values, we want to animate the size of the stars. We'll do it by defining a multiplier that we will use with the star sizes to create the effect of the stars twinkling. We do this with infiniteTransition and animateFloat:

val infiniteTransition = 
    rememberInfiniteTransition(label = "infinite")

val starSizeMultiplierOne by infiniteTransition
    .animateFloat(
        initialValue = 1f,
        targetValue = 1.5f,
        animationSpec =
            infiniteRepeatable(
                animation =
                    tween(
                        durationMillis = 3000,
                        easing = animationEasing,
                    ),
                repeatMode = RepeatMode.Reverse,
            ),
        label = "starSizeMultiplierOne",
    )

For the starSizeMultiplierOne, we give the initial value of 1, meaning that as it will act as a multiplier, the size would be 1 * size. The target value is 1.5, and as the repeat mode is Reverse, the float will animate between the size multiplied by 1 and 1.5. This transition creates a growing and shrinking effect.

As we want the stars to look realistic and not animate at the same speed, we need to define two animated multipliers. Let's define another one:

val starSizeMultiplierTwo by infiniteTransition
    .animateFloat(
        initialValue = 0.7f,
        targetValue = 1.7f,
        animationSpec =
            infiniteRepeatable(
                animation =
                    tween(
                        durationMillis = 2300,
                        easing = animationEasing,
                    ),
                repeatMode = RepeatMode.Reverse,
            ),
        label = "starSizeMultiplierTwo",
    )

This one has a bit shorter animation duration, and the initial and target values are different, so the twinkling happens at a different rate from the first multiplier.

Now we'll just need to pass these multipliers to stars:

val stars =
    starsList.mapIndexed { index, star ->
        val multiplier = if (index % 2 == 0) 
            starSizeMultiplierOne 
        else 
            starSizeMultiplierTwo

        star.copy(
            size = star.size * multiplier,
        )
    }

We map through the list of stars defined previously, return copies of the star with the multiplier attached, and store them in a new list. Later, when drawing the stars, we use this new list.

In the case of the code snippet, this mapping through the list might feel redundant - why just not put the multipliers in when defining the stars? Well, in this case, it would work. But if we defined the stars outside the component, animated values can be hard to attach, so this would be the strategy.

Planet

The next thing we want to animate is the planet - or the moon orbiting around it, to be exact.

In the previous post, we defined how to draw the planet and the moon, and the source code showed an extension function called drawMoon. Let's extend it and pass in a parameter called degrees:

fun DrawScope.drawMoon(
    center: Offset,
    outlineStyle: Stroke,
    degrees: Float,
) {
...
}

Then we wrap the contents of drawMoon with a rotate, which uses the degrees for rotation and center for pivot so that the rotation happens around the center of the planet:

rotate(degrees = degrees, pivot = center) {
...
}

In the top-level component, we define the degrees, which is an animated float value:

val degrees by infiniteTransition
    .animateFloat(
        initialValue = 360f,
        targetValue = 0f,
        animationSpec =
            infiniteRepeatable(
                animation =
                    tween(
                        durationMillis = 3000 * 6,
                        easing = LinearEasing,
                    ),
            ),
        label = "degrees",
    )

For the initial value, we use 360f, and as the target value, 0f. These values create the effect that the moon orbits counter-clockwise around the planet.

Saturn

The last item we animate is Saturn. Its movement is subtle - slightly moving up and down to create a floating effect.

In the previous blog post, we defined an extension function, drawSaturn, that takes in top-left coordinates and outline style. We can use the top-left coordinates for the effect.

First, let's define center offset, an animated float value we're going to use:

val centerOffset by infiniteTransition
    .animateFloat(
        initialValue = 0f,
        targetValue = 2f,
        animationSpec =
            infiniteRepeatable(
                animation =
                    tween(
                        durationMillis = 3000,
                        easing = EaseIn,
                    ),
                repeatMode = RepeatMode.Reverse,
            ),
        label = "centerOffset",
    )

As you can see, the initial value is 0f, and the target value is 2f. We can then use this and change the parameters we pass to drawSaturn by adding the centerOffset to the top-left coordinates:

drawSaturn(
    center =
        Offset(
            size.width * 0.25f + centerOffset,
            size.height * 0.25f + centerOffset
        ),
    outlineStyle = outlineStyle,
)

This way, the x and y coordinates have an extra amount of animating from 0f to 2f, creating the floating effect.

Wrapping Up

In this blog post, we've covered animating drawings on Canvas. All the animations used animated floats in different ways. And as we can see, small changes add a lot of movement to the Canvas.

I hope you've enjoyed this blog post and learned something!

Links in the Blog Post