Hello Jetpack Compose developers,
Last week, we did some Compose animation work to build a typing indicator in a chat app. For our next blog series, I’ll be diving into more animation topics with a project inspired by Nicole Terc’s Composable Sheep talk from droidcon NYC. Her composable sheep talk series inspired me to work on my own colorful and playful animated garden project while learning Compose Multiplatform!
Inspiration
For those of you who haven’t had a chance yet, I’d highly recommend watching the original Compose Sheep talk. There’s also a Composable Sheep sequel from droidcon London, where Nicole and Tasha partner up to show us some even cooler animation (and mathematical) concepts.
I used the sample repositories from these talks as reference for my own investigations:
- nicole-terc/composable-sheep (github.com)
- drinkthestars/composable-sheep-sketches: Funky composable sheep 😎 🐏 (github.com)
Like the original sheep project, we’ll be using the Compose Graphics libraries (ex: Canvas
, DrawScope
…) and animation APIs to draw our projects frame by frame. All of the code covered today can be referenced in the Animated garden part 1 PR.
Draw a flower
In order to start growing my garden, the first step was to figure out how to draw a flower. In DrawScope
, you have access to many different functions for drawing text, shapes, and images.
To build a basic flower, we need two basic parts: the petals and the center of the flower. To draw these, we’ll use the DrawScope
methods drawOval
and drawCircle
.
After some experimentation, I found the ideal sizes and colors for the first flower in my garden – a sunflower 🌻! The code to draw this flower is simple:
private val Brown = Color(0xFF742C0D) private val Yellow = Color(0xFFF8D314) @Composable fun Sunflower() { Canvas(Modifier.size(100.dp)) { drawSunflower() } } fun DrawScope.drawSunflower() { drawPetals() drawCenter() } fun DrawScope.drawCenter() { val radius = 20f drawCircle(color = Brown, radius = radius, center = center) } fun DrawScope.drawPetals() { val numPetals = 8 var angle = 0f val size = Size(width = 30f, height = 75f) repeat(numPetals) {
rotate(angle) { drawOval( color = Yellow, topLeft = Offset(center.x - size.width / 2, center.y), size = size ) }
angle += 360 / numPetals }
}
Some subtle but important details from the code snippet:
-
When using
Canvas
, you must provide a size modifier, otherwise it will default to zero and your drawings will not be shown - Order matters in drawing operations – since we want the center shown above the petals, we have to make sure to draw it last
-
In
DrawScope
, you gain access to fields likecenter
andsize
that are really helpful for making calculations
No respectable garden has only one flower, so of course we have to add some code to grow the garden:
@Composable fun MyGarden() { LazyVerticalGrid( modifier = Modifier.fillMaxSize(), columns = GridCells.Adaptive(100.dp) ) { items(8) { Sunflower() } } }
And with that, we have a nice basic garden full of sunflowers 😊
Add an animation canvas
The next step to beautifying our garden is adding some animations. To set up our animations, the first thing we need to do is copy over a simplified version of the Sketch
composable from the composable-sheep-sketches repo:
@Composable fun Sketch( modifier: Modifier = Modifier, targetValue: Float, animationSpec: AnimationSpec<Float>, onDraw: DrawScope.(Float) -> Unit ) { val animationState = remember { AnimationState(0f) } LaunchedEffect(targetValue) { while (isActive) { animationState.animateTo( targetValue = targetValue, animationSpec = animationSpec, sequentialAnimation = true ) } } Canvas(modifier = modifier) { onDraw(animationState.value) }
}
This will allow us to draw our animations frame by frame. Now, instead of wrapping our drawings with Canvas
, we’ll use Sketch
, and in the onDraw
method you can see that you’ll have access to the animationState
value. The targetValue
and animationSpec
parameters will allow you to customize the speed and style of animation, as you will see in later examples.
Build a basic animation
Now that we have the animation canvas set up with the Sketch
composable, we can start to build some fun animations for our garden.
In all basic animations, we’re essentially choosing one property to change over time. Once you’ve decided on a property, you can figure out what values you want to animate in between, how quickly the value should change, if the animation should repeat, etc.
For our first basic flower animation, we’ll be trying to animate the size of the flowers. To do that, we’ll have to:
- Update the flower drawing logic
-
Connect the
drawSunflower
method to the animation canvas
Update drawing logic
To change how big the flowers are drawn, we’ll have to change the code in two places:
-
The
radius
parameter when we calldrawCircle
for the center of the flower -
The
size
parameter when we calldrawOval
for the petals of the flower
To ensure that the center and petals grow at the same rate, we can describe the size as a percentage instead of a specific value. This way, we won’t need to keep track of two different size values and we can easily create an animation state that varies between 0f
and 1f
.
I like the size of the basic sunflowers from the previous section, so let’s say we want to animate between zero up to the hardcoded sizes from earlier. To achieve that, the updated drawSunflower
method would look like this:
fun DrawScope.drawSunflower(sizePct: Float) { drawPetals(sizePct) drawCenter(sizePct) } fun DrawScope.drawCenter(sizePct: Float) { val radius = 20f * sizePct drawCircle(color = Brown, radius = radius, center = center) } fun DrawScope.drawPetals(sizePct: Float) { val numPetals = 8 var angle = 0f val size = Size(width = 30f, height = 75f) * sizePct repeat(numPetals) { rotate(angle) { drawOval( color = Yellow, topLeft = Offset(center.x - size.width / 2, center.y), size = size ) } angle += 360 / numPetals } }
You can see the only differences are that we now have the sizePct
float parameter, which will be our animation state that changes between 0f
and 1f
, and that we multiply the radius
and size
values by this percentage.
Connect to animation canvas
The last step is to animate our sunflowers is to call drawSunflower
inside our new Sketch
animation canvas and pass along the animation state.
Let’s take a look at the Sketch
composable’s parameters:
@Composable fun Sketch( modifier: Modifier = Modifier, targetValue: Float, animationSpec: AnimationSpec<Float>, onDraw: DrawScope.(Float) -> Unit )
We have some typical composable parameters, including a modifier and a content parameter (onDraw
) with the DrawScope
receiver and Float
parameter that will give us access to the animation state.
The targetValue
parameter controls the end value of the animation state (the start value is always 0f
in our examples). For our sizePct
animation state, we want the animation state to vary between 0f
and 1f
, so we’ll set targetValue
to 1f
.
The animationSpec
parameter lets you customize animation behavior, such as how quickly the animation state changes over time, if it repeats, etc. You can read more about the different options in the Customize animations documentation, but for our purposes, we’ll be using infiniteRepeatable
to infinitely loop our animation and tween
to describe our animation behavior.
Both functions accept three parameters:
@Stable fun <T> infiniteRepeatable( animation: DurationBasedAnimationSpec<T>, repeatMode: RepeatMode = RepeatMode.Restart, initialStartOffset: StartOffset = StartOffset(0) ): InfiniteRepeatableSpec<T> @Stable fun <T> tween( durationMillis: Int = DefaultDurationMillis, delayMillis: Int = 0, easing: Easing = FastOutSlowInEasing
): TweenSpec<T>
Let’s try calling these functions with their default behavior and see what the flower animation looks like:
@Composable fun AnimatedSizeSunflower() { Sketch( modifier = Modifier.size(100.dp), targetValue = 1f, animationSpec = infiniteRepeatable(animation = tween()) ) { sizePct ->
drawSunflower(sizePct = sizePct) } }
Not bad! However, I have a few tweaks I want to make:
-
Slow down the animation
To do this, we’ll update the
tween
call with a longerdurationMillis
value. -
Have the flower shrink as well as grow
We can update the
infiniteRepeatable
call by setting therepeatMode
toRepeatMode.Reverse
. That way, the flower will grow, then shrink, instead of restarting the growing animation each time. -
Add a pause in between growing and shrinking
We can also address this by updating the
tween
call, this time by setting thedelayMillis
parameter to a non-zero value. This will add a delay before the animation starts. -
Change the rate at which the flower grows and shrinks
For this, we’ll investigate different values for the
easing
parameter in thetween
call. There are many easing options available for use, and you can experiment with them (and read up on Material Design) to find out which one works best for your use case.
After some experimentation, I settled on the following values for our animation spec:
@Composable fun AnimatedSizeSunflower() { Sketch( modifier = Modifier.size(100.dp), targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(durationMillis = 800, delayMillis = 50, easing = LinearOutSlowInEasing), repeatMode = RepeatMode.Reverse
) ) { sizePct -> drawSunflower(sizePct = sizePct) } }
Take a look at the garden now:
So now, you know more about graphics in Compose, creating a frame-by-frame animation canvas, and building basic animations! Next week, we’ll dive into animating more flower properties (hint: 🌈) to make our garden even cooler 😎
Resources and feedback
The content covered today is part of PR #1 in the animated-garden repo.
To learn more about Jetpack Compose animations and creative coding, check out these resources:
- nicole-terc/composable-sheep (github.com)
- drinkthestars/composable-sheep-sketches: Funky composable sheep 😎 🐏 (github.com)
- Composable Sheep – A Compose Animations Journey
- Composable Sheep – The Creative Coding Epilogue
- Graphics in Jetpack Compose
- Animations in Jetpack Compose
- Customize animations in Jetpack Compose
If you have any questions, use the feedback forum or message us on Twitter @surfaceduodev.
We won’t be livestreaming this week, but you can check out the archives on YouTube.
0 comments