Photo by Ryan Hutton on Unsplash

Animating the Compose Galaxy

Suleyman Fatih Giris
ProAndroidDev
Published in
5 min readMar 31, 2021

--

Since Jetpack Compose has joined our lives, it has been changing the way of our thinking and coding day by day. But more importantly, creating UI’s is becoming much easier compared to the old Android view system. This empowers developers to learn and adapt to the Compose environment in a much faster way (especially for animations 🥳).

After I started playing with Compose, I have built the following Compose Galaxy. In this post, I will share my learnings about animations in Jetpack Compose while building the Compose Galaxy.

Disclaimer: Compose version used in this post is 1.0.0-beta02 and there might be API changes in the future.

You can access the Compose Galaxy source code from here.

Draw into Canvas

First things first. We need to draw something to the screen in order to animate. Canvas composable can be used to draw any custom shape that can be represented in the coordinate system. In order to create a canvas, just use Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) composable function and place your drawings inside onDraw lambda. DrawScope provides stateless APIs to perform drawing into the canvas meaning that no need for maintaining the underlying canvas state.

Let’s draw a simple circle.

Draw a simple circle in the middle of the screen using canvas

Then we will have a circle in the middle of the screen.

Circle in the middle of the screen

Animation

This is looking great but isn't it a bit boring? Let’s bring this little one alive. Luckily, we have very handy animation APIs in Compose. Let’s look at some of them and their use cases.

animate*AsState

animate*AsState is the simplest animation API that is used to animate a single value. You only need to give the target value and whenever the target value changes the animation will get triggered again. Compose supports some of the commonly used data types like Int, Float, Dp, Color. But you can still use it to animate any type of value using animateValueAsState. Additionally, no animate*AsState is cancellable without removing this composable function from the tree.

Let’s fade in the circle using animateAsFloat by changing its alpha on click.

Changing the alpha value of a circle using animateFloatAsState

Then we get:

Fading in the circle

animate*AsState is very handy if you need to animate a single value. But what if you want to animate multiple values simultaneously? Then the answer is updateTransition.

updateTransition

updateTransition creates a transition that manages running animations between given states. It gives you an opportunity to animate multiple values simultaneously. Remember that the transition created with this composable function only acceptsFiniteAnimationSpec to animate the values. It means that you can only create finite animations with this transition but not infinite ones.

Let’s use updateTransition both to fade in the circle and increase its radius simultaneously.

Using updateTransition to fade in the circle and change its radius simultaneously

Here are the simultaneous animations.

Fading in the circle and animating its radius simultaneously

Note that the animation here is behaving like a one-shot animation and not repeating itself (It seems like repeating because of the gif image but the animation itself is not repeating). You could easily make it repeatable by wrapping the animation spec with repeatable() . But still repeatable != infinite. Let’s look into how we can create infinite animations.

rememberInfiniteTransition

rememberInfiniteTransition creates a transition that runs infinite animations. Note that this is also a composable function and cannot be called from a non-composable block.

Let’s add an infinite blinking animation to the circle.

Using rememberInfiniteTransition for infinitely blinking circle

Animatable

Both updateTransition and rememberInfiniteTransition works really well inside the composable blocks. The animation created with these transitions happens during composition. But what if your animation is independent of composition and the animation itself is not the only source of truth (For instance touch events such as animating a circle to the point where the click happens) This is where Animatable comes into play. Animatable is a float value holder which can be used to animate any float value without needing to be called from a composable block. It provides animateTo and animateDecay suspend functions that are used to start an animation.

Let’s move the circle to a random point every time the user clicks on the canvas.

Using Animatable to move the circle to a random point on each user click

Here is the result.

A circle moving to a random point on each click

Avoid starting animation inside onDraw block since it might be called many times during recomposition.

TargetBasedAnimation

Even though most of the aforementioned high-level APIs will be sufficient for most of the use cases, there are some cases we need to control the animation timing manually. (For instance, pausing and continuing the same animation) TargetBasedAnimation gives you an opportunity to control the animation playtime by yourself. Both the start and target values should be specified when creating the TargetBasedAnimation .

Let’s pause and resume moving the circle on each user click.

Pausing and resuming the animation with TargetBasedAnimation

And the result is:

Pausing and resuming the animation of a circle

DecayAnimation

DecayAnimation also gives you control over the animation playtime. Unlike TargetBasedAnimation, there is no target value for DecayAnimation. You only give an initial value and a velocity, then the target value is calculated based upon those values. You can think of DecayAnimation like an animation calculating engine which gives you the right animation value calculated from the value and the velocity for the corresponding playtime. If you do not need to control the animation time, you should be using Animatable.animateDecay instead of DecayAnimation.

It is not recommended to use DecayAnimation unless you need to control the animation timing manually.

Let’s pause and resume moving a car image using DecayAnimation on each click.

Using DecayAnimation to pause and resume the animation of moving an image

Then we have:

Pausing and resuming the animating car image with DecayAnimation

Conclusion

Animations have never been so easy like in Compose. If you want to animate a single value then use animate*AsState. But if you need multiple animations simultaneously, then updateAnimation is the way. You can repeat your animations by wrapping your animation spec with repeatable. If you want to repeat animations infinitely, prefer using rememberInfiniteTransition. Animatable is used when your animation is not the only source of truth. Also do not prefer using TargetBasedAnimation and DecayAnimation unless there is a need to control the animation timing manually.

Thanks Adam Bennett and Doris Liu for the reviews.

--

--