Compose has many built-in animation mechanisms and it can be overwhelming to know which one to choose. Below is a list of common animation use cases. For more detailed information about the full set of different API options available to you, read the full Compose Animation documentation.
Animate common composable properties
Compose provides convenient APIs that allow you to solve for many common animation use cases. This section demonstrates how you can animate common properties of a composable.
Animate appearing / disappearing
Use AnimatedVisibility
to hide or show a Composable. Children inside
AnimatedVisibility
can use Modifier.animateEnterExit()
for their own enter
or exit transition.
var visible by remember { mutableStateOf(true) } // Animated visibility will eventually remove the item from the composition once the animation has finished. AnimatedVisibility(visible) { // your composable here // ... }
The enter and exit parameters of AnimatedVisibility
allow you to configure how
a composable behaves when it appears and disappears. Read the full
documentation for more information.
Another option for animating the visibility of a composable is to animate the
alpha over time using animateFloatAsState
:
var visible by remember { mutableStateOf(true) } val animatedAlpha by animateFloatAsState( targetValue = if (visible) 1.0f else 0f, label = "alpha" ) Box( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = animatedAlpha } .clip(RoundedCornerShape(8.dp)) .background(colorGreen) .align(Alignment.TopCenter) ) { }
However, changing the alpha comes with the caveat that the composable remains
in the composition and continues to occupy the space it's laid out in. This
could cause screen readers and other accessibility mechanisms to still consider
the item on screen. On the other hand, AnimatedVisibility
eventually removes
the item from the composition.
Animate background color
val animatedColor by animateColorAsState( if (animateBackgroundColor) colorGreen else colorBlue, label = "color" ) Column( modifier = Modifier.drawBehind { drawRect(animatedColor) } ) { // your composable here }
This option is more performant than using Modifier.background()
.
Modifier.background()
is acceptable for a one-shot color setting, but when
animating a color over time, this could cause more recompositions than
necessary.
For infinitely animating the background color, see repeating an animation section.
Animate the size of a composable
Compose lets you animate the size of composables in a few different ways. Use
animateContentSize()
for animations between composable size changes.
For example, if you have a box that contains text which can expand from one to
multiple lines you can use Modifier.animateContentSize()
to achieve a smoother
transition:
var expanded by remember { mutableStateOf(false) } Box( modifier = Modifier .background(colorBlue) .animateContentSize() .height(if (expanded) 400.dp else 200.dp) .fillMaxWidth() .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { expanded = !expanded } ) { }
You can also use AnimatedContent
, with a SizeTransform
to describe
how size changes should take place.
Animate position of composable
To animate the position of a composable, use Modifier.offset{ }
combined with
animateIntOffsetAsState()
.
var moved by remember { mutableStateOf(false) } val pxToMove = with(LocalDensity.current) { 100.dp.toPx().roundToInt() } val offset by animateIntOffsetAsState( targetValue = if (moved) { IntOffset(pxToMove, pxToMove) } else { IntOffset.Zero }, label = "offset" ) Box( modifier = Modifier .offset { offset } .background(colorBlue) .size(100.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { moved = !moved } )
If you want to ensure that composables are not drawn over or under other
composables when animating position or size, use Modifier.layout{ }
. This
modifier propagates size and position changes to the parent, which then affects
other children.
For example, if you are moving a Box
within a Column
and the other children
need to move when the Box
moves, include the offset information with
Modifier.layout{ }
as follows:
var toggled by remember { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } Column( modifier = Modifier .padding(16.dp) .fillMaxSize() .clickable(indication = null, interactionSource = interactionSource) { toggled = !toggled } ) { val offsetTarget = if (toggled) { IntOffset(150, 150) } else { IntOffset.Zero } val offset = animateIntOffsetAsState( targetValue = offsetTarget, label = "offset" ) Box( modifier = Modifier .size(100.dp) .background(colorBlue) ) Box( modifier = Modifier .layout { measurable, constraints -> val offsetValue = if (isLookingAhead) offsetTarget else offset.value val placeable = measurable.measure(constraints) layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) { placeable.placeRelative(offsetValue) } } .size(100.dp) .background(colorGreen) ) Box( modifier = Modifier .size(100.dp) .background(colorBlue) ) }
Animate padding of a composable
To animate the padding of a composable, use animateDpAsState
combined with
Modifier.padding()
:
var toggled by remember { mutableStateOf(false) } val animatedPadding by animateDpAsState( if (toggled) { 0.dp } else { 20.dp }, label = "padding" ) Box( modifier = Modifier .aspectRatio(1f) .fillMaxSize() .padding(animatedPadding) .background(Color(0xff53D9A1)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { toggled = !toggled } )
Animate elevation of a composable
To animate the elevation of a composable, use animateDpAsState
combined with
Modifier.graphicsLayer{ }
. For once-off elevation changes, use
Modifier.shadow()
. If you are animating the shadow, using
Modifier.graphicsLayer{ }
modifier is the more performant option.
val mutableInteractionSource = remember { MutableInteractionSource() } val pressed = mutableInteractionSource.collectIsPressedAsState() val elevation = animateDpAsState( targetValue = if (pressed.value) { 32.dp } else { 8.dp }, label = "elevation" ) Box( modifier = Modifier .size(100.dp) .align(Alignment.Center) .graphicsLayer { this.shadowElevation = elevation.value.toPx() } .clickable(interactionSource = mutableInteractionSource, indication = null) { } .background(colorGreen) ) { }
Alternatively, use the Card
composable, and set the elevation property to
different values per state.
Animate text scale, translation or rotation
When animating scale, translation, or rotation of text, set the textMotion
parameter on TextStyle
to TextMotion.Animated
. This ensures smoother
transitions between text animations. Use Modifier.graphicsLayer{ }
to
translate, rotate or scale the text.
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") val scale by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 8f, animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), label = "scale" ) Box(modifier = Modifier.fillMaxSize()) { Text( text = "Hello", modifier = Modifier .graphicsLayer { scaleX = scale scaleY = scale transformOrigin = TransformOrigin.Center } .align(Alignment.Center), // Text composable does not take TextMotion as a parameter. // Provide it via style argument but make sure that we are copying from current theme style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated) ) }
Animate text color
To animate text color, use the color
lambda on the BasicText
composable:
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition") val animatedColor by infiniteTransition.animateColor( initialValue = Color(0xFF60DDAD), targetValue = Color(0xFF4285F4), animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse), label = "color" ) BasicText( text = "Hello Compose", color = { animatedColor }, // ... )
Switch between different types of content
Use AnimatedContent
to animate between different composables, if you
just want a standard fade between composables, use Crossfade
.
var state by remember { mutableStateOf(UiState.Loading) } AnimatedContent( state, transitionSpec = { fadeIn( animationSpec = tween(3000) ) togetherWith fadeOut(animationSpec = tween(3000)) }, modifier = Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { state = when (state) { UiState.Loading -> UiState.Loaded UiState.Loaded -> UiState.Error UiState.Error -> UiState.Loading } }, label = "Animated Content" ) { targetState -> when (targetState) { UiState.Loading -> { LoadingScreen() } UiState.Loaded -> { LoadedScreen() } UiState.Error -> { ErrorScreen() } } }
AnimatedContent
can be customized to show many different kinds of enter and
exit transitions. For more information, read the documentation on
AnimatedContent
or read this blog post on
AnimatedContent
.
Animate whilst navigating to different destinations
To animate transitions between composables when using the
navigation-compose artifact, specify the enterTransition
and
exitTransition
on a composable. You can also set the default animation to be
used for all destinations at the top level NavHost
:
val navController = rememberNavController() NavHost( navController = navController, startDestination = "landing", enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None } ) { composable("landing") { ScreenLanding( // ... ) } composable( "detail/{photoUrl}", arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }), enterTransition = { fadeIn( animationSpec = tween( 300, easing = LinearEasing ) ) + slideIntoContainer( animationSpec = tween(300, easing = EaseIn), towards = AnimatedContentTransitionScope.SlideDirection.Start ) }, exitTransition = { fadeOut( animationSpec = tween( 300, easing = LinearEasing ) ) + slideOutOfContainer( animationSpec = tween(300, easing = EaseOut), towards = AnimatedContentTransitionScope.SlideDirection.End ) } ) { backStackEntry -> ScreenDetails( // ... ) } }
There are many different kinds of enter and exit transitions that apply different effects to the incoming and outgoing content, see the documentation for more.
Repeat an animation
Use rememberInfiniteTransition
with an infiniteRepeatable
animationSpec
to continuously repeat your animation. Change RepeatModes
to
specify how it should go back and forth.
Use finiteRepeatable
to repeat a set number of times.
val infiniteTransition = rememberInfiniteTransition(label = "infinite") val color by infiniteTransition.animateColor( initialValue = Color.Green, targetValue = Color.Blue, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ), label = "color" ) Column( modifier = Modifier.drawBehind { drawRect(color) } ) { // your composable here }
Start an animation on launch of a composable
LaunchedEffect
runs when a composable enters the composition. It starts
an animation on launch of a composable, you can use this to drive the animation
state change. Using Animatable
with the animateTo
method to start the
animation on launch:
val alphaAnimation = remember { Animatable(0f) } LaunchedEffect(Unit) { alphaAnimation.animateTo(1f) } Box( modifier = Modifier.graphicsLayer { alpha = alphaAnimation.value } )
Create sequential animations
Use the Animatable
coroutine APIs to perform sequential or concurrent
animations. Calling animateTo
on the Animatable
one after the other causes
each animation to wait for the previous animations to finish before proceeding .
This is because it is a suspend function.
val alphaAnimation = remember { Animatable(0f) } val yAnimation = remember { Animatable(0f) } LaunchedEffect("animationKey") { alphaAnimation.animateTo(1f) yAnimation.animateTo(100f) yAnimation.animateTo(500f, animationSpec = tween(100)) }
Create concurrent animations
Use the coroutine APIs (Animatable#animateTo()
or animate
), or
the Transition
API to achieve concurrent animations. If you use multiple
launch functions in a coroutine context, it launches the animations at the same
time:
val alphaAnimation = remember { Animatable(0f) } val yAnimation = remember { Animatable(0f) } LaunchedEffect("animationKey") { launch { alphaAnimation.animateTo(1f) } launch { yAnimation.animateTo(100f) } }
You could use the updateTransition
API to use the same state to drive
many different property animations at the same time. The example below animates
two properties controlled by a state change, rect
and borderWidth
:
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "transition") val rect by transition.animateRect(label = "rect") { state -> when (state) { BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) } } val borderWidth by transition.animateDp(label = "borderWidth") { state -> when (state) { BoxState.Collapsed -> 1.dp BoxState.Expanded -> 0.dp } }
Optimize animation performance
Animations in Compose can cause performance problems. This is due to the nature of what an animation is: moving or changing pixels on screen quickly, frame-by-frame to create the illusion of movement.
Consider the different phases of Compose: composition, layout and draw. If your animation changes the layout phase, it requires all affected composables to relayout and redraw. If your animation occurs in the draw phase, it is by default be more performant than if you were to run the animation in the layout phase, as it would have less work to do overall.
To ensure your app does as little as possible while animating, choose the lambda
version of a Modifier
where possible. This skips recomposition and performs
the animation outside of the composition phase, otherwise use
Modifier.graphicsLayer{ }
, as this modifier always runs in the draw
phase. For more information on this, see the deferring reads section in
the performance documentation.
Change animation timing
Compose by default uses spring animations for most animations. Springs, or
physics-based animations, feel more natural. They are also interruptible as
they take into account the object's current velocity, instead of a fixed time.
If you want to override the default, all the animation APIs demonstrated above
have the ability to set an animationSpec
to customize how an animation runs,
whether you'd like it to execute over a certain duration or be more bouncy.
The following is a summary of the different animationSpec
options:
spring
: Physics-based animation, the default for all animations. You can change the stiffness or dampingRatio to achieve a different animation look and feel.tween
(short for between): Duration-based animation, animates between two values with anEasing
function.keyframes
: Spec for specifying values at certain key points in an animation.repeatable
: Duration-based spec that runs a certain number of times, specified byRepeatMode
.infiniteRepeatable
: Duration-based spec that runs forever.snap
: Instantly snaps to the end value without any animation.
Read the full documentation for more information about animationSpecs.
Additional resources
For more examples of fun animations in Compose, take a look at the following:
- 5 quick animations in Compose
- Making Jellyfish move in Compose
- Customizing
AnimatedContent
in Compose - Easing into Easing functions in Compose