Using Animatable to achieve Custom Canvas Animations
Imagine the smoothest app experience you’ve seen… Everything moving along with silky smooth transitions and sensible reactions to your interactions…. Are you drooling yet? 🤤 Well you no longer need to dream, as adding animations with Compose will make this dream a reality. Don’t believe me? Read on.
Compose has a range of Animation APIs that cover many different use cases. The simplest animation API is <a class="au lx" href="https://developer.android.com/reference/kotlin/androidx/compose/animation/package-summary#AnimatedVisibility(kotlin.Boolean,androidx.compose.ui.Modifier,androidx.compose.animation.EnterTransition,androidx.compose.animation.ExitTransition,kotlin.String,kotlin.Function1)" target="_blank" rel="noopener ugc nofollow">AnimatedVisibility</a>
. We can use it to animate the visibility of any Composable by wrapping it in an AnimatedVisibility
Composable and providing a boolean that toggles the visibility:
From our partners:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ var showText by remember { mutableStateOf(true) } AnimatedVisibility(visible = showText) { RoundedRect() }

This is a quick way to get fluid user interactions in your Compose apps. But what if we wanted to do something more complex, like animating the rotation or color of our custom drawing?

The Animation documentation covers the different APIs that will enable you to achieve these sorts of animations and we are going to dive into one of those options: Animatable.
But before we jump into how to achieve this with Compose, let’s remind ourselves how to achieve custom animations in the standard view system:
Reminiscing on the Standard View approach 😶🌫️
Without Compose, you’d need to use something like ValueAnimator
or ObjectAnimator
to animate properties of your custom view. I covered this example in a previous talk.
Below is a sample of how to animate two properties of a custom view that draws a rounded rect, namely a rotation angle
and a color
Int. We use the ValueAnimator.ofPropertyValuesHolder
to animate multiple properties at the same time, calling invalidate()
on the view. This triggers a redraw and consequently, the animation runs as the properties are updated.
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ class CustomCanvasAnimationView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : View(context, attrs, defStyle) { private var angle = 0f @ColorInt private var color: Int = Color.GREEN set(value) { field = value paint.color = value } private val paint = Paint() private val sizeBoxPx = SIZE_BOX.toPx fun animateViewChanges(toAngle: Float, @ColorInt toColor: Int) { val propAngle = PropertyValuesHolder.ofFloat(PROP_ANGLE, angle, toAngle) val propColor = PropertyValuesHolder.ofInt(PROP_COLOR, color, toColor) ValueAnimator.ofPropertyValuesHolder(propColor, propAngle).apply { setEvaluator(ArgbEvaluator()) duration = 3000 addUpdateListener { animation -> [email protected] = animation.getAnimatedValue(PROP_ANGLE) as Float [email protected] = animation.getAnimatedValue(PROP_COLOR) as Int invalidate() // performs animation as it calls onDraw() again } start() } } override fun onDraw(canvas: Canvas) { // use newly updated angle and color, this onDraw function is called multiple times to // produce the animation canvas.save() canvas.rotate(angle, sizeBoxPx / 2f, sizeBoxPx / 2f) canvas.drawRoundRect(0f, 0f, sizeBoxPx, sizeBoxPx, 16f, 16f, paint) canvas.restore() } companion object { private const val PROP_ANGLE = "angle" private const val PROP_COLOR = "color" private const val SIZE_BOX = 200f } } val Float.toPx get() = this * Resources.getSystem().displayMetrics.density
Using Compose to Animate Custom Drawing 🎨
To implement this animation in Compose, we create two remembered Animatable
states–one for the angle, and one for the color. The angle will animate from 0–360° and the color will animate from Color.Green
to Color.Blue
, we will then use these two values to draw our elements on the Canvas.
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ @Composable fun CustomCanvasAnimation() { val angle = remember { Animatable(0f) } val color = remember { Animatable(green, typeConverter = Color.VectorConverter(ColorSpaces.LinearSrgb)) } LaunchedEffect(angle, color) { launch { angle.animateTo(360f, animationSpec = tween(3000)) } launch { color.animateTo(blue, animationSpec = tween(3000)) } } Canvas(modifier = Modifier.size(200.dp), onDraw = { rotate(angle.value) { drawRoundRect( color = color.value, cornerRadius = CornerRadius(16.dp.toPx()) ) } } ) }
All the animation logic is in the same Composable as the drawing logic. Leveraging the same classes and functions that are used in Composables — such as coroutines and Compose’s State
class to store the animation progress.
You’ll notice the color animatable looks a bit different: We’ve defined a <a class="au lx" href="https://developer.android.com/reference/kotlin/androidx/compose/animation/core/TwoWayConverter" target="_blank" rel="noopener ugc nofollow">TwoWayConverter</a>
to convert the color into an AnimationVector
. The Color.VectorConverter
transforms between a Color
and a 4 part AnimationVector
storing the red, green, blue and alpha components of the color. In the previous view sample, we needed to add the ArgbEvaluator
to achieve the same effect.
To start the animations as soon as the Composable is added to the Composition, we use the <a class="au lx" href="https://developer.android.com/jetpack/compose/side-effects#launchedeffect" target="_blank" rel="noopener ugc nofollow">LaunchedEffect</a>
class. This will only run again when the provided keys change. We then use the animateTo
function on the previously defined Animatable
, this is a suspend function that needs to be called from a coroutine context.
In order to run the animations in parallel, we call launch twice, calling each animateTo
in a new coroutine. If we wanted to run the animations sequentially, we would change the above LaunchedEffect
block to use the same coroutine:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ LaunchedEffect(angle, color) { launch { angle.animateTo(360f, animationSpec = tween(3000)) color.animateTo(blue, animationSpec = tween(3000)) } }
Running the animations in the same coroutine will suspend the thread until angle.animateTo()
has completed running, then color.animateTo()
will run. For more detail around using coroutines for animations — check out this video.
The last thing that remains is to use these two properties to draw something! ✏️ The Canvas onDraw
function uses the animatable values — angle.value
and color.value
to rotate and set the color of the rounded rectangle. Compose automatically redraws when these values change. We don’t need to call invalidate
.

That’s it! Using Animatable
, we can achieve custom animations in the same way as we were previously using ValueAnimator
. We’ve achieved a more readable version and less lines of code using the Compose version than the ValueAnimator
approach.
And if less code hasn’t convinced you yet… Using <a class="au lx" href="https://developer.android.com/reference/kotlin/androidx/compose/animation/core/KeyframesSpec" target="_blank" rel="noopener ugc nofollow">keyframes</a>
might be the real drawcard. Keyframes in Compose give you the ability to change key sections (frames) of your animations at certain time intervals.
For example, if we want to set specific colors to animate at certain points of our animation, we can do the following:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ LaunchedEffect(angle, color) { launch { angle.animateTo(360f, animationSpec = tween(3000)) } launch { color.animateTo(Color.Black, animationSpec = keyframes { durationMillis = 3000 Color.Red at 500 with LinearEasing Color.Yellow at 1000 with FastOutSlowInEasing Color.Green at 1500 with FastOutLinearInEasing Color.Blue at 2000 with FastOutLinearInEasing Color.Magenta at 2500 with FastOutLinearInEasing } ) { // callback with animation values if you need them } } }
The above example creates a keyframes
AnimationSpec
object, and provides it with the different Colors that should be animated to at specific time intervals with a specified Easing Curve.

It is worth noting that Animatable
is a low level animation API offered by Compose, many of the higher level convenience APIs are based on top of Animatable
. For example, animateColorAsState()
wraps the above logic for Color conversions using TwoWayConverter
under the hood. animateFloatAsState()
could also be used for animating the angle. The animate*AsState
APIs allow you to animate independently based on a state change, whereas Animatable
offers more flexibility and control of your animation: allowing coordinating animations, indeterminate animations triggered by gestures etc.
For a more real world example, have a look at the custom date selection pill animation in the Crane sample.

For more information on different kinds of Animation in Compose, check out the Animation docs, the Animation codelab, or the Motion Compose sample on Github for more examples.
Share your drool-worthy animations with me on Twitter @riggaroo.
Bye for now! 👋
By Rebecca Franks
Source Android
For enquiries, product placements, sponsorships, and collaborations, connect with us at [email protected]. We'd love to hear from you!
Our humans need coffee too! Your support is highly appreciated, thank you!