May 18th, 2023

Blooming love for Compose animation

Kristen Halper
SW/FW Engineer

Hello Jetpack Compose developers,

Today we’ll be continuing our blog series on animations in Jetpack Compose! This content was inspired by Nicole Terc’s Composable Sheep talk from droidcon NYC.

More basic animations

We’ll be continuing to use our drawSunflower method and Sketch composable to build animations in this post, so if you haven’t already, check out last week’s blog post to see how we built those functions!

Last week, we took a look at implementing a basic size animation for our sunflower garden.

Today, we’ll learn how to do animations with two other properties: angle and color.

Rotation animation

To add some rotation to our garden, we’ll first have to revisit our flower drawing code. This time, our animation state will need to be float that varies between 0f and 360f, to represent the angle offset. To incorporate the angle into our drawing code, we only need to make one simple change:

  fun DrawScope.drawSunflower(sizePct: Float = 1f, rotation: Float = 0f) {
      drawPetals(sizePct, rotation)
      drawCenter(sizePct)
  }

  // drawCenter remains the same

  fun DrawScope.drawPetals(sizePct: Float, rotation: Float) {
      val numPetals = 8
      var angle = rotation
      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 }
}

Instead of starting the angle variable at 0f when making calculations for where the petal ovals will be drawn, we simply start at the current angle offset in the rotation. The rotation angle only needs to be passed into the drawPetal function, so the drawCenter function can remain the same as before.

To set up the animation behavior, let’s remind ourselves of what the Sketch composable and animation spec methods infiniteRepeatable and tween look like:

  @Composable
  fun Sketch(
      modifier: Modifier = Modifier,
      targetValue: Float,
      animationSpec: AnimationSpec<Float>,
      onDraw: DrawScope.(Float) -> Unit
  )

  @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>

We already noted that our animation state should vary between 0f and 360f to cover every angle, so we’ll set the targetValue to 360f.

For the animation spec, I want my sunflowers to rotate at a constant speed infinitely. To make the rotation look seamless as it repeats, we’ll leave the repeatMode and delayMillis parameters at their default values. And to achieve a constant rotation speed, this time we’ll use a LinearEasing so the angle changes at a constant rate.

Putting all that together, we can write the AnimatedRotationSunflower composable:

  @Composable
  fun AnimatedRotationSunflower() {
      Sketch(
          modifier = Modifier.size(100.dp),
          targetValue = 360f,
          animationSpec = infiniteRepeatable(tween(durationMillis = 2000, easing = LinearEasing))
      ) { angle ->
        drawSunflower(rotation = angle)
      }
  }

Look at our garden now!

Color animation

As beautiful as the sunflowers are in yellow and brown, I think the garden could benefit from some additional colors! Let’s try to add some color-based animations.

Thanks to the composable sheep talk, we know that Color.hsv method is a great way to animate color based on angle, since the hue parameter must be in the range of 0f-360f.

To update our drawing code to accept our animated color, we only have to pass in the color parameter and use it when drawing our petal ovals:

  fun DrawScope.drawSunflower(sizePct: Float = 1f, rotation: Float = 0f, color: Color = Yellow) {
      drawPetals(sizePct, rotation, color)
      drawCenter(sizePct)
  }

  // drawCenter remains the same

  fun DrawScope.drawPetals(sizePct: Float, rotation: Float, color: Color) {
      val numPetals = 8
      var angle = rotation
      val size = Size(width = 30f, height = 75f) * sizePct

      repeat(numPetals) {
        rotate(angle) {
            drawOval(
                  color = color,
                  topLeft = Offset(center.x - size.width / 2, center.y),
                  size = size
              )
          }
        angle += 360 / numPetals
      }
  }

Super simple! As for our animation, I want very similar behavior to our rotation animation from above – seamless infinite looping, constant rate of change – so our final composable will look like this:

  @Composable
  fun AnimatedColorSunflower() {
      Sketch(
          modifier = Modifier.size(100.dp),
          targetValue = 360f,
          animationSpec = infiniteRepeatable(tween(durationMillis = 3000, easing = LinearEasing))
      ) { hue ->
        drawSunflower(color = Color.hsv(hue = hue, saturation = 1f, value = 1f))
      }
  }

As previously mentioned, the Color.hsv method accepts a hue between 0f and 360f, so we set our targetValue to 360f and pass in the animated value to generate a new color.

Here’s the result:

Pretty cool, but I think we can do even better! It would be awesome to have each individual petal change color over time for a sort of rainbow spinner effect. To do this, we’ll mostly have to update the drawing code so that each petal is drawn with a slightly different color. We’ll keep all the animation spec details the same, but instead of passing in one color for all the petals, we’ll now just pass in the hue and do calculations while drawing:

  @Composable
  fun AnimatedPetalColorSunflower() {
      Sketch(
          modifier = Modifier.size(100.dp),
          targetValue = 360f,
          animationSpec = infiniteRepeatable(tween(durationMillis = 3000, easing = LinearEasing))
      ) { hue ->
        drawSunflower(hue = hue)
      }
  }

  fun DrawScope.drawSunflower(sizePct: Float = 1f, rotation: Float = 0f, color: Color = Yellow, hue: Float? = null) {
      drawPetals(sizePct, rotation, color, hue)
      drawCenter(sizePct)
  }

  // drawCenter remains the same

  fun DrawScope.drawPetals(sizePct: Float, rotation: Float, color: Color, hue: Float?) {
      val numPetals = 8
      var angle = rotation
      val size = Size(width = 30f, height = 75f) * sizePct

      repeat(numPetals) { petal ->
        rotate(angle) {
            drawOval(
                  color = hue?.let { Color.hsv(hue = (hue + petal * 10f).mod(360f), saturation = 1f, value = 1f) }
                    ?: color,
                  topLeft = Offset(center.x - size.width / 2, center.y),
                  size = size
              )
          }
        angle += 360 / numPetals
      }
  }

You can see that we now call Color.hsv in drawPetals, and we use the current petal number to offset the hue value. Because we are adding to the hue, we do have to call mod(360f) to make sure the final hue value is within the required range.

And here’s the final result – look at how much cooler our garden is now:

And that’s it for the additional basic animations we’ll cover today. The possibilities really are endless – you could also animate the number of petals, the position of the flower, or any combination of the properties we already animated. Next week, we’ll finish up this blog series by finalizing our flower varieties and adding some green 🟩!

Resources and feedback

The content covered today is part of PR #2 in the animated-garden repo.

To learn more about Jetpack Compose animations and creative coding, check out these resources:

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.

Author

Kristen Halper
SW/FW Engineer

Works in the Surface Duo Developer Experience team to help with all aspects of dual-screen SDK development and customer engagement.

0 comments

Discussion are closed.