Intro to animations with Jetpack Compose
Canvas transformations and transitions
Notice
This article was written in 2020, before Jetpack Compose 1.0 came out, and much has changed in the APIs since.
I might update the code examples at some point — until then, I’m keeping this warning here. The article hopefully still has its value in the thought process described.
Your most up-to-date info on the actual animation APIs should be https://developer.android.com/jetpack/compose/animation
Inspiration
Earlier on, I got inspired by the Kotlin-Pokedex project and its Flutter counterpart: flutter_pokedex, and started experimenting with a Compose-based solution for it, out of which compose-pokedex was born.
All these projects are based on the beautiful original design done by Saepul Nahwan:
This original design has many amazing animations as well:
The Flutter Pokedex has a lot of the above beauty implemented in it, which got me thinking if the same is possible to achieve with Compose.
I especially love the carousel like selector of Pokemon on the dashboard screen, but as of yet, I only have some vague ideas how to implement that.
So let’s start with something simpler though as a first step!
There’s this rotating Pokeball animation behind the individual Pokemon:
This seems like a good first step.
Let’s get started!
Drawing the image on canvas
We can use the DrawImage Composable to show a static image on screen. To make it constrained to a size, let’s wrap it in a Container:
So we get:
We can also add tinting and opacity to it:
But this is a static image. How do you rotate something with Compose?
Canvas transformations
The Draw Composable gives us a way to manipulate the canvas.
We can use it to draw directly on it, e.g.:
But we can also use it to apply transformations on the canvas to change how child Composables render themselves.
For example, this setup results in the child rendering slightly to the left:
Don’t forget to save and restore canvas state before / after invoking drawChildren so that your changes only affect the rendering of your child Composable.
We can use canvas transformations for rotations too!
Apply rotation
Let’s try to rotate the canvas by 45 degrees:
But this is what happens:
It’s even weirder when we change the rotation value to 180 degrees:
What’s going on here?
It looks like as though it’s not only rotating, but also adding an offset somehow?
This commit in the AOSP repo means we’ll be getting “show layout bounds” functionality for Compose soon. Until then, a simple visual debugging tool you can use whenever you feel stuck is to wrap the Composable in question in a Surface to visualise the box it is being rendered in:
In this case, adding a coloured background and trying the rotation with different values helps to understand that we’re rotating the canvas around its top-left corner ((0,0) coordinates), and not around its center as we might have expected it to:
To fix this, we can apply:
- translation by half of the width and height of the child — this moves the imaginary (0,0) point to the center
- rotation
- reversing the translation — so that all further actions are done relative to the original (0,0) coordinate and not the center
In code:
Now it looks correctly rotated around its center with any arbitrary rotation value:
That’s great!
But if we could only apply a rotation once to create a static rotated image, it wouldn’t be much fun.
How do we make it animated?
A super quick intro to Transitions
Animations in Compose can be done with the Transition Composable, but — depending on where you’re coming from — it might not be fully intuitive how it works.
Transition doesn’t know about your UI, it operates on a more abstract level, and animates only values without making assumptions what they will be used for. (If you ever worked with ValueAnimator, it’s a similar concept). To demonstrate, let’s see something very basic without any UI parts first:
Transition works on a TransitionDefinition to get from one defined state to another one. All you do is set up keys and values associated to them — Transition will call the lambda you provided repeatedly with intermediate values between the states you defined.
The above example would print lines to the console similar to:
Of course, you are tied neither to the console, nor to just animating one float value!
The funny thing is that you define what keys you want to have, what kind of values do they even reflect, and how many different “endpoint” states you want to have.
You can have as many keys and states as you wish, giving you complete freedom to define any complex animation based on these values.
The androidx.animation package also provides you with other useful PropKeys too: IntPropKey, FloatPropKey, DpPropKey, ColorPropKey, PxPropKey, PxPositionPropKey.
In addition, as PropKey is an interface with generic <T> type, you can write your own interpolation for whatever other thing too.
Now that we know this, it shouldn’t be too difficult to apply it to rotate our image!
Rotate by transition
To animate the rotation value, we could create a TransitionDefinition such as:
And then passing this Transition, we’ll use the latest animated value for the canvas rotation inside our Draw block:
The above code results in this animation:
We’re getting there!
Final touches
Though on the above gif the animation is continuous, it’s only because the gif itself is looped. If we launched our code on a device, we would see that the animation actually finishes after a single rotation is complete and then does nothing else after that.
To fix this, let’s change the TransitionDefinition to:
Also, though tween is using a nice easing by default, in our case we don’t want that. Let’s just use a linear one:
The above changes result in this animation:
Generic solution
We can arrive at a generic solution by:
- extracting a Rotate Composable that rotates the canvas by a given degree and draws another Composable once
- building on top of the previous, providing a RotateIndefinitely Composable that uses Transition to play the animation
- allowing duration to be parameterised
Here’s the complete solution:
And now client code becomes:
Of course, we can now rotate any other Composable too:
See you next time
I hope you enjoyed the above! I’d like to experiment with some of the more complex animations of the original design too, in which case you can expect more articles to come.
The solution presented above is part of the Jetpack Compose Pokedex app.
You can check it out on: https://github.com/zsoltk/compose-pokedex
You can also follow me on Twitter.