Recommended Reading

20 Minutes
Jetpack Compose Animations: A Complete Guide for Android Developers
Fix Bugs Faster! Log Collection Made Easy
Jetpack Compose is Android’s modern UI toolkit. It unifies and simplifies the experience by consolidating state and logic in the rendering with a more declarative approach. Among its most notable features is a powerful animation framework that helps developers create fluid and performant animations with ease. Jetpack Compose allows you to easily inject animations into your UI, which is a great way of providing visual feedback and enhancing interactions that directly affect the user experience.
In this article, we will drill down on the top-level concepts and skills of Jetpack Compose animations. After reading this post, you will have an understanding of how to create complex animations, learn how to work with a variety of animation APIs, and render smoother, more performance-friendly animations. We will cover:
- Animations that are specific to a state of your UI, so even when the animation ends, they stay balanced.
- Interactions of the user to gesture-driven animations
- Transition animations animating multiple properties at once
- Advanced handling of the appearance and disappearance of composables using
AnimatedVisibility
.
This article is mainly for experienced Android developers who have worked with the basics of Jetpack Compose and are looking to level up their animation game. It will also be advantageous for developers who previously developed UI using XML and transitioned to Jetpack Compose, and are now looking to make user-friendly apps with dynamic interfaces that looks amazing. Regardless of whether you are aiming for greater visual aesthetics, or if you want to grasp the extent of Compose animations capabilities, this article will be helpful to you.
Table of Contents
Understanding the animation APIs in Jetpack Compose
Overview of animation APIs
Jetpack Compose provides multiple animation APIs and each of them is custom designed for its specific need. The following are the main Animation APIs offered in Jetpack Compose:
animate*AsState
: These functions animate a single state in response to the change; essentially this will give your screenplay an animated pulsing color. These are very easy to use for simple animations, such as animating a property when the state changes.//Example of animateColorAsState val color by animateColorAsState(if (isRed) Color.Red else Color.Blue)
Animatable
: This API is great when creating more complex animations, or if you need programmatic control over an animation. TheAnimatable
protocol can animate any value and allows a very detailed control over the state of an animation.val offsetX = remember { Animatable(0f) } //Example of Animatable
Transition
: This API helps to animate multiple properties at once. It offers a method for describing animated changes with various states, therefore allowing the easier coordination of more elaborate animations.//Example of updateTransition val transition = updateTransition(targetState = isExpanded)
AnimatedVisibility
: Compose provides this API for animating the appearance and disappearance of composables. It includes built-in enter and exit animations for when elements come onto or leave the screen.//Example of AnimatedVisibility AnimatedVisibility(visible = isVisible) { Box(modifier = Modifier.size(100.dp).background(Color.Magenta)) }
How to choose the right API
The choice of animation API is based on your use case.
- Simple state changes: When you want to animate a single property as the state changes, use
animate*AsState
. The API is user-friendly and provides a good solution in the easy-to-manage cases like changing colors, sizes and offsets.//Example of using animateColorAsState val color by animateColorAsState(if (isClicked) Color.Green else Color.Gray)
- Programmatic control: Use when you need finer control over an animation – for example, when starting, reversing or updating it in response to user interactions or other app events.
//Example of Animatable val offsetX = remember { Animatable(0f) }
- Coordinated animations: If you want to animate a bunch of properties at the same time, use
Transition
. This helps to identify differences when in transit, making sure all changes animate together with a correct property value.//updateTransition example val transition = updateTransition(targetState = isExpanded)
- Visibility animations:
AnimatedVisibility
is used to animate the visible and invisible state of any composable The API takes care of visibility changes and implements entering-to-exiting screen transitions.//AnimatedVisibility example AnimatedVisibility(visible = showDialog) { // Dialog content here }
Simple animations with animate*AsState
These are simple animations created using animate*AsState
functions in Jetpack Compose and made specifically for state-driven animation. These functions handle the complexity of animating values when state changes, so you don’t need to manage the updates manually. animateColorAsState
, animateDpAsState
, animateFloatAsState
are used for smooth transitions.
//use animateColorAsState to animate the color of a Box
@Composable
fun ColorAnimationDemo() {
var isRed by remember { mutableStateOf(true) }
val color by animateColorAsState(targetValue = if (isRed) Color.Red else Color.Blue)
Box(
modifier = Modifier
.size(100.dp)
.background(color)
.clickable { isRed = !isRed }
)
}
Customizing Animation Parameters
These animate*AsState
functions have parameters that allow you to customize the animations – either for duration, or so that the animation fits better with your design needs. animationSpec
parameter is used to adjust the parameters.
//example of customizing the animation duration
@Composable
fun CustomColorAnimationDemo() {
var isRed by remember { mutableStateOf(true) }
val color by animateColorAsState(
targetValue = if (isRed) Color.Red else Color.Blue,
animationSpec = tween(
durationMillis = 1000, // 1 second duration
easing = LinearOutSlowInEasing, // Easing curve
delayMillis = 300 // 300 ms delay
)
)
Box(
modifier = Modifier
.size(100.dp)
.background(color)
.clickable { isRed = !isRed }
)
}
Using Animatable
for complex animations
The Animatable
class is a powerful tool for creating more complex and programmatic animations in Jetpack Compose. While animate*AsState
functions are fairly simple state-driven animations, Animatable is more granular, allowing you to control animations. You can play, stop, reverse or chain Animations together along with some more advanced use cases.
Animatable
can animate properties like color, size or position.
Using Animatable
to change the position:
//Example of Animating Position
@Composable
fun DraggableBox() {
val offsetX = remember { Animatable(0f) }
val offsetY = remember { Animatable(0f) }
Box(
modifier = Modifier
.size(100.dp)
.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
.background(Color.Green)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offsetX.snapTo(offsetX.value + dragAmount.x)
offsetY.snapTo(offsetY.value + dragAmount.y)
}
}
)
}
Using Animatable
to change the color:
//Example of Animating Color
@Composable
fun ColorChangeBox() {
val color = remember { Animatable(Color.Red) }
LaunchedEffect(key1 = Unit) {
color.animateTo(Color.Blue, animationSpec = tween(durationMillis = 2000))
}
Box(
modifier = Modifier
.size(100.dp)
.background(color.value)
.clickable {
LaunchedEffect(key1 = Unit) {
color.animateTo(if (color.value == Color.Blue) Color.Red else Color.Blue)
}
}
)
}
Leveraging Transition
for state transitions
Jetpack Compose provides Transition
API to ease the native handling of complicated state transitions takes with animating multiple property at once. This API allows transitions between states to be defined in a declarative manner, ensuring animations work without any visual hiccups.
How to create a Simple Transition
The first is using the Transition API to create a transition object with updateTransition
, within which you can then specify animated properties. Here’s a simple example of how that works:
// example where a Box animates its size and color
@Composable
fun SimpleTransitionDemo() {
var isExpanded by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isExpanded, label = "BoxTransition")
val size by transition.animateDp(label = "SizeAnimation") { state ->
if (state) 200.dp else 100.dp
}
val color by transition.animateColor(label = "ColorAnimation") { state ->
if (state) Color.Red else Color.Blue
}
Box(
modifier = Modifier
.size(size)
.background(color)
.clickable { isExpanded = !isExpanded }
)
}
Customizing the Transition
The Transition
API focuses on animation parameters like duration, easing and delay for all those animated values. The animateDp
and animateColor
functions can take an AnimationSpec
to determine how you want the animation performed.
//Example of Custom Animation Parameters
@Composable
fun CustomTransitionDemo() {
var isExpanded by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isExpanded, label = "BoxTransition")
val size by transition.animateDp(
label = "SizeAnimation",
transitionSpec = { tween(durationMillis = 1000, easing = LinearOutSlowInEasing) }
) { state ->
if (state) 200.dp else 100.dp
}
val color by transition.animateColor(
label = "ColorAnimation",
transitionSpec = { tween(durationMillis = 1000, easing = FastOutSlowInEasing) }
) { state ->
if (state) Color.Red else Color.Blue
}
Box(
modifier = Modifier
.size(size)
.background(color)
.clickable { isExpanded = !isExpanded }
)
}
Gesture-Based Animations
Animating Based on Gestures
Animations combined with gestures can help make UI components feel dynamic and highly interactive. Jetpack Compose’s Modifier.pointerInput
lets you detect and respond to other user gestures – like dragging, swiping or tapping.
This lets you tie animations directly to user gestures. By combining Animatable
with gesture detection, you can create smooth, seamless and gesture-driven animations.
//Example of Box dragged around the screen
@Composable
fun DraggableBox() {
val offsetX = remember { Animatable(0f) }
val offsetY = remember { Animatable(0f) }
Box(
modifier = Modifier
.size(100.dp)
.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
.background(Color.Green)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
launch {
offsetX.snapTo(offsetX.value + dragAmount.x)
offsetY.snapTo(offsetY.value + dragAmount.y)
}
}
}
)
}
Swipe to dismiss example:
//example where a Box can be swiped to the left or right to dismiss it
@Composable
fun SwipeToDismissBox() {
val offsetX = remember { Animatable(0f) }
val boxWidth = 100.dp
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
Box(
modifier = Modifier
.size(boxWidth)
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.background(Color.Red)
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
launch {
if (offsetX.value.absoluteValue > screenWidth.value / 4) {
offsetX.animateTo(if (offsetX.value > 0) screenWidth.value.toFloat() else -screenWidth.value.toFloat())
} else {
offsetX.animateTo(0f)
}
}
}
) { change, dragAmount ->
change.consume()
launch {
offsetX.snapTo(offsetX.value + dragAmount.x)
}
}
}
)
}
Performance optimization for animations
Reducing Recomposition
In Jetpack Compose, recomposition is the process of re-executing composable functions when state changes. Although recomposition is an efficient operation in Compose, you must be careful not to rebuild it too frequently as otherwise the performance may suffer.
- Use
remember
to store state that doesn’t need to be – and, indeed, should be not be – recomposed. This ensures you don’t recompose when nothing has changed, and preserves the state over recompositions.val offsetX = remember { Animatable(0f) }
- Use
rememberUpdatedState
inside effects or lambdas to reference the latest value of a state without restarting the effect. This ensures your animations and side effects stay in sync with state changes.
val currentValue by rememberUpdatedState(newValue)
- Avoid heavy computations in your composables and instead just pass results of those computations to the composable
@Composable fun MyComposable() { val computedValue = remember { performHeavyComputation() } // Use computedValue in the UI }
- Launch coroutines in Compose with
LaunchedEffect
– or for non-suspending side effects useSideEffect
. This ensures the side effects will not be executed unnecessarily.
LaunchedEffect(key1 = someKey) {
// Coroutine logic
}
SideEffect {
// Non-suspending side effects
}
Real-World use cases
Case study of E-Commerce application with high-level animations
Below is a real example of advanced animations applied to an e-commerce application. This case study focuses on animating the transition between a product list and a product detail page.
Product detail page transition
In an e-commerce app, providing a smooth transition between page and other UI elements can significantly enhance the user experience. We can use a combination of Transition
and AnimatedVisibility
to achieve this effect.
The result is a seamless transition from the list of products to details of a selected product. For this effect we can use Transition
with AnimatedVisibility
.
@Composable
fun ProductListItem(product: Product, onClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable(onClick = onClick)
) {
Row {
Image(
painter = rememberImagePainter(product.imageUrl),
contentDescription = null,
modifier = Modifier.size(100.dp)
)
Text(
text = product.name,
modifier = Modifier
.padding(16.dp)
.weight(1f)
)
}
}
}
@Composable
fun ProductDetailPage(product: Product, onClose: () -> Unit) {
var isVisible by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = isVisible,
enter = fadeIn() + expandIn(),
exit = fadeOut() + shrinkOut()
) {
Column {
Button(onClick = { isVisible = false; onClose() }) {
Text("Back")
}
Image(
painter = rememberImagePainter(product.imageUrl),
contentDescription = null,
modifier = Modifier.fillMaxWidth().height(200.dp)
)
Text(
text = product.name,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(16.dp)
)
Text(
text = product.description,
modifier = Modifier.padding(16.dp)
)
}
}
}
Best Practices for Smooth Animations
- Use
remember
to store state and avoid unnecessary recompositions. Avoid doing heavy computations in composables. - Use the profiling tools in Android Studio to monitor performance of your app and identify ways to optimize.
- Select the right
AnimationSpec
for a smooth transition and better performance. - Avoid overdraw by minimizing overlapping composables.
- Use coroutines with
Animatable
for complex, sequential animations that don’t block the main thread. - Efficiently manage rapid gesture events by debouncing them, which reduces the number of animations.
To Sum up
In this article, we learned why animations are important for improving the UX, and examined various animation APIs provided by Jetpack Compose. We also:
- Learned about animations powered by state using functions like
animate*AsState
, as well as how to customize parameters like duration, easing and delays. - Went in-depth on the
Animatable
class, supporting richer animations like changing color, size and position via coroutines. - Experimented with the
Transition
API to animate multiple properties which make state transitions complex. - Applied
AnimatedVisibility
to handle the appearance and disappearance of composables. - Integrated animations with gestures using
Modifier.pointerInput
to create responsive, interactive UI elements. - Talked about ways to optimize animations for better performance by reducing the recompositions, in addition to profiling our animations with the tools in Android Studio.
- Walked through a case study of more complicated animations in an e-commerce app and methods to keep things performant smooth. Happy Coding!
Expect The Unexpected!
Debug Faster With Bugfender