Jetpack Compose provides powerful and extensible APIs that make it easy to implement various animations in your app's UI. This document describes how to use these APIs as well as which API to use depending on your animation scenario.
Overview
Animations are essential in a modern mobile app in order to realize a smooth and understandable user experience. Many Jetpack Compose Animation APIs are available as composable functions just like layouts and other UI elements, and they are backed by lower-level APIs built with Kotlin coroutine suspend functions. This guide starts with the high-level APIs that are useful in many practical scenarios, and moves on to explain the low-level APIs that give you further control and customization.
The diagram below helps you decide what API to use to implement your animation.
- If you are animating content change in layout:
- If you are animating appearance and disappearance:
- Use
AnimatedVisibility
.
- Use
- Swapping content based on state:
- If you are crossfading content:
- Use
Crossfade
.
- Use
- Otherwise, use
AnimatedContent
.
- If you are crossfading content:
- Otherwise, use
Modifier.animateContentSize
.
- If you are animating appearance and disappearance:
- If the animation is state-based:
- If the animation happens during composition:
- If the animation is infinite:
- If you are animating multiple values simultaneously:
- Use
updateTransition
.
- Use
- Otherwise, use
animate*AsState
.
- If the animation happens during composition:
- If you want to have fine control over animation time:
- Use
Animation
, such asTargetBasedAnimation
orDecayAnimation
.
- Use
- If the animation is the only source of truth
- Use
Animatable
.
- Use
- Otherwise, use
AnimationState
oranimate
.
High-level animation APIs
Compose offers high-level animation APIs for several common animation patterns used in many apps. These APIs are tailored to align with the best practices of Material Design Motion.
AnimatedVisibility
The
AnimatedVisibility
composable animates the appearance and disappearance of its content.
var editable by remember { mutableStateOf(true) } AnimatedVisibility(visible = editable) { Text(text = "Edit") }
By default, the content appears by fading in and expanding, and it disappears by
fading out and shrinking. The transition can be customized by specifying
EnterTransition
and
ExitTransition
.
var visible by remember { mutableStateOf(true) } val density = LocalDensity.current AnimatedVisibility( visible = visible, enter = slideInVertically { // Slide in from 40 dp from the top. with(density) { -40.dp.roundToPx() } } + expandVertically( // Expand from the top. expandFrom = Alignment.Top ) + fadeIn( // Fade in with the initial alpha of 0.3f. initialAlpha = 0.3f ), exit = slideOutVertically() + shrinkVertically() + fadeOut() ) { Text("Hello", Modifier.fillMaxWidth().height(200.dp)) }
As you can see in the example above, you can combine multiple EnterTransition
or ExitTransition
objects with a +
operator, and each accepts optional
parameters to customize its behavior. See the references for more information.
EnterTransition
and ExitTransition
examples
AnimatedVisibility
also offers a variant that takes a
MutableTransitionState
. This allows you to trigger an animation as soon as the
AnimatedVisibility
is added to the composition tree. It is also useful for
observing the animation state.
// Create a MutableTransitionState<Boolean> for the AnimatedVisibility. val state = remember { MutableTransitionState(false).apply { // Start the animation immediately. targetState = true } } Column { AnimatedVisibility(visibleState = state) { Text(text = "Hello, world!") } // Use the MutableTransitionState to know the current animation state // of the AnimatedVisibility. Text( text = when { state.isIdle && state.currentState -> "Visible" !state.isIdle && state.currentState -> "Disappearing" state.isIdle && !state.currentState -> "Invisible" else -> "Appearing" } ) }
Animate enter and exit for children
Content within AnimatedVisibility
(direct or indirect children) can use the
animateEnterExit
modifier to specify different animation behavior for each of them. The visual
effect for each of these children is a combination of the animations specified
at the AnimatedVisibility
composable and the child's own enter and
exit animations.
var visible by remember { mutableStateOf(true) } AnimatedVisibility( visible = visible, enter = fadeIn(), exit = fadeOut() ) { // Fade in/out the background and the foreground. Box(Modifier.fillMaxSize().background(Color.DarkGray)) { Box( Modifier .align(Alignment.Center) .animateEnterExit( // Slide in/out the inner box. enter = slideInVertically(), exit = slideOutVertically() ) .sizeIn(minWidth = 256.dp, minHeight = 64.dp) .background(Color.Red) ) { // Content of the notification… } } }
In some cases, you may want to have AnimatedVisibility
apply no animations at
all so that children can each have their own distinct animations by
animateEnterExit
. To achieve this, specify EnterTransition.None
and
ExitTransition.None
at the AnimatedVisibility
composable.
Add custom animation
If you want to add custom animation effects beyond the built-in enter and exit
animations, access the underlying Transition
instance via the transition
property inside the content lambda for AnimatedVisibility
. Any animation
states added to the Transition instance will run simultaneously with the enter
and exit animations of AnimatedVisibility
. AnimatedVisibility
waits until
all animations in the Transition
have finished before removing its content.
For exit animations created independent of Transition
(such as using
animate*AsState
), AnimatedVisibility
would not be able to account for them,
and therefore may remove the content composable before they finish.
var visible by remember { mutableStateOf(true) } AnimatedVisibility( visible = visible, enter = fadeIn(), exit = fadeOut() ) { // this: AnimatedVisibilityScope // Use AnimatedVisibilityScope#transition to add a custom animation // to the AnimatedVisibility. val background by transition.animateColor(label = "color") { state -> if (state == EnterExitState.Visible) Color.Blue else Color.Gray } Box(modifier = Modifier.size(128.dp).background(background)) }
See updateTransition for the details about Transition
.
animate*AsState
The animate*AsState
functions are the simplest animation APIs in Compose for
animating a single value. You only provide the end value (or target value), and
the API starts animation from the current value to the specified value.
Below is an example of animating alpha using this API. By simply wrapping the
target value in animateFloatAsState
, the alpha value is now an animation value
between the provided values (1f
or 0.5f
in this case).
var enabled by remember { mutableStateOf(true) } val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f) Box( Modifier.fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) )
Note that you don't need to create an instance of any animation class, or handle
interruption. Under the hood, an animation object (namely, an Animatable
instance) will be created and remembered at the call site, with the first target
value as its initial value. From there on, any time you supply this composable a
different target value, an animation is automatically started towards that
value. If there's already an animation in flight, the animation starts from its
current value (and velocity) and animates toward the target value. During the
animation, this composable gets recomposed and returns an updated animation
value every frame.
Out of the box, Compose provides animate*AsState
functions for Float
,
Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
, and
IntSize
. You can easily add support for other data types by providing a
TwoWayConverter
to animateValueAsState
that takes a generic type.
You can customize the animation specifications by providing an AnimationSpec
.
See AnimationSpec for more information.
AnimatedContent (experimental)
The AnimatedContent
composable animates its content as it changes based on a
target state.
Row { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }) { Text("Add") } AnimatedContent(targetState = count) { targetCount -> // Make sure to use `targetCount`, not `count`. Text(text = "Count: $targetCount") } }
Note that you should always use the lambda parameter and reflect it to the content. The API uses this value as the key to identify the content that's currently shown.
By default, the initial content fades out and then the target content fades in
(this behavior is called fade through). You
can customize this animation behavior by specifying a ContentTransform
object to the
transitionSpec
parameter. You can create ContentTransform
by combining an
EnterTransition
with an ExitTransition
using the with
infix function. You can apply SizeTransform
to the ContentTransform
by attaching it with the
using
infix function.
AnimatedContent( targetState = count, transitionSpec = { // Compare the incoming number with the previous number. if (targetState > initialState) { // If the target number is larger, it slides up and fades in // while the initial (smaller) number slides up and fades out. slideInVertically { height -> height } + fadeIn() with slideOutVertically { height -> -height } + fadeOut() } else { // If the target number is smaller, it slides down and fades in // while the initial number slides down and fades out. slideInVertically { height -> -height } + fadeIn() with slideOutVertically { height -> height } + fadeOut() }.using( // Disable clipping since the faded slide-in/out should // be displayed out of bounds. SizeTransform(clip = false) ) } ) { targetCount -> Text(text = "$targetCount") }
EnterTransition
defines how the target content should appear, and
ExitTransition
defines how the initial content should disappear. In addition
to all of the EnterTransition
and ExitTransition
functions available for
AnimatedVisibility
, AnimatedContent
offers slideIntoContainer
and slideOutOfContainer
.
These are convenient alternatives to slideInHorizontally/Vertically
and
slideOutHorizontally/Vertically
that calculate the slide distance based on
the sizes of the initial content and the target content of the
AnimatedContent
content.
SizeTransform
defines how the
size should animate between the initial and the target contents. You have
access to both the initial size and the target size when you are creating the
animation. SizeTransform
also controls whether the content should be clipped
to the component size during animations.
var expanded by remember { mutableStateOf(false) } Surface( color = MaterialTheme.colorScheme.primary, onClick = { expanded = !expanded } ) { AnimatedContent( targetState = expanded, transitionSpec = { fadeIn(animationSpec = tween(150, 150)) with fadeOut(animationSpec = tween(150)) using SizeTransform { initialSize, targetSize -> if (targetState) { keyframes { // Expand horizontally first. IntSize(targetSize.width, initialSize.height) at 150 durationMillis = 300 } } else { keyframes { // Shrink vertically first. IntSize(initialSize.width, targetSize.height) at 150 durationMillis = 300 } } } } ) { targetExpanded -> if (targetExpanded) { Expanded() } else { ContentIcon() } } }
Animate enter/exit for children
Just like AnimatedVisibility
, the animateEnterExit
modifier is available inside the content lambda of AnimatedContent
. Use this
to apply EnterAnimation
and ExitAnimation
to each of the direct or indirect
children separately.
Add custom animation
Just like AnimatedVisibility
, the transition
field is available inside the
content lambda of AnimatedContent
. Use this to create a custom animation
effect that runs simultaneously with the AnimatedContent
transition. See
updateTransition for the details.
animateContentSize
The animateContentSize
modifier animates a size change.
var message by remember { mutableStateOf("Hello") } Box( modifier = Modifier.background(Color.Blue).animateContentSize() ) { Text(text = message) }
Crossfade
Crossfade
animates between two layouts with a crossfade animation. By toggling
the value passed to the current
parameter, the content is switched with a
crossfade animation.
var currentPage by remember { mutableStateOf("A") } Crossfade(targetState = currentPage) { screen -> when (screen) { "A" -> Text("Page A") "B" -> Text("Page B") } }
updateTransition
Transition
manages one or more animations as its children and runs them
simultaneously between multiple states.
The states can be of any data type. In many cases, you can use a custom enum
type to ensure type safety, as in this example:
enum class BoxState { Collapsed, Expanded }
updateTransition
creates and remembers an instance of Transition
and updates
its state.
var currentState by remember { mutableStateOf(BoxState.Collapsed) } val transition = updateTransition(currentState, label = "box state")
You can then use one of animate*
extension functions to define a child
animation in this transition. Specify the target values for each of the states.
These animate*
functions return an animation value that is updated every frame
during the animation when the transition state is updated with
updateTransition
.
val rect by transition.animateRect(label = "rectangle") { state -> when (state) { BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f) BoxState.Expanded -> Rect(100f, 100f, 300f, 300f) } } val borderWidth by transition.animateDp(label = "border width") { state -> when (state) { BoxState.Collapsed -> 1.dp BoxState.Expanded -> 0.dp } }
Optionally, you can pass a transitionSpec
parameter to specify a different
AnimationSpec
for each of the combinations of transition state changes. See
AnimationSpec for more information.
val color by transition.animateColor( transitionSpec = { when { BoxState.Expanded isTransitioningTo BoxState.Collapsed -> spring(stiffness = 50f) else -> tween(durationMillis = 500) } }, label = "color" ) { state -> when (state) { BoxState.Collapsed -> MaterialTheme.colorScheme.primary BoxState.Expanded -> MaterialTheme.colorScheme.background } }
Once a transition has arrived at the target state, Transition.currentState
will be the same as Transition.targetState
. This can be used as a signal for
whether the transition has finished.
We sometimes want to have an initial state different from the first target
state. We can use updateTransition
with MutableTransitionState
to achieve
this. For example, it allows us to start animation as soon as the code enters
composition.
// Start in collapsed state and immediately animate to expanded var currentState = remember { MutableTransitionState(BoxState.Collapsed) } currentState.targetState = BoxState.Expanded val transition = updateTransition(currentState, label = "box state") // ……
For a more complex transition involving multiple composable functions, you can
use createChildTransition
to create a child transition. This technique is useful for separating concerns
among multiple subcomponents in a complex composable. The parent transition will
be aware of all the animation values in the child transitions.
enum class DialerState { DialerMinimized, NumberPad } @Composable fun DialerButton(isVisibleTransition: Transition<Boolean>) { // `isVisibleTransition` spares the need for the content to know // about other DialerStates. Instead, the content can focus on // animating the state change between visible and not visible. } @Composable fun NumberPad(isVisibleTransition: Transition<Boolean>) { // `isVisibleTransition` spares the need for the content to know // about other DialerStates. Instead, the content can focus on // animating the state change between visible and not visible. } @Composable fun Dialer(dialerState: DialerState) { val transition = updateTransition(dialerState, label = "dialer state") Box { // Creates separate child transitions of Boolean type for NumberPad // and DialerButton for any content animation between visible and // not visible NumberPad( transition.createChildTransition { it == DialerState.NumberPad } ) DialerButton( transition.createChildTransition { it == DialerState.DialerMinimized } ) } }
Use transition with AnimatedVisibility and AnimatedContent
AnimatedVisibility
and AnimatedContent
are available as extension functions of Transition
. The targetState
for
Transition.AnimatedVisibility
and Transition.AnimatedContent
is derived
from the Transition
, and triggering enter/exit transitions as needed when the
Transition
's targetState
has changed. These extension functions allow all
the enter/exit/sizeTransform animations that would otherwise be internal to
AnimatedVisibility
/AnimatedContent
to be hoisted into the Transition
.
With these extension functions, AnimatedVisibility
/AnimatedContent
's state
change can be observed from outside. Instead of a boolean visible
parameter,
this version of AnimatedVisibility
takes a lambda that converts the parent
transition's target state into a boolean.
See AnimatedVisibility and AnimatedContent for the details.
var selected by remember { mutableStateOf(false) } // Animates changes when `selected` is changed. val transition = updateTransition(selected, label = "selected state") val borderColor by transition.animateColor(label = "border color") { isSelected -> if (isSelected) Color.Magenta else Color.White } val elevation by transition.animateDp(label = "elevation") { isSelected -> if (isSelected) 10.dp else 2.dp } Surface( onClick = { selected = !selected }, shape = RoundedCornerShape(8.dp), border = BorderStroke(2.dp, borderColor), elevation = elevation ) { Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Text(text = "Hello, world!") // AnimatedVisibility as a part of the transition. transition.AnimatedVisibility( visible = { targetSelected -> targetSelected }, enter = expandVertically(), exit = shrinkVertically() ) { Text(text = "It is fine today.") } // AnimatedContent as a part of the transition. transition.AnimatedContent { targetState -> if (targetState) { Text(text = "Selected") } else { Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone") } } } }
Encapsulate a Transition and make it reusable
For simple use cases, defining transition animations in the same composable as your UI is a perfectly valid option. When you are working on a complex component with a number of animated values, however, you might want to separate the animation implementation from the composable UI.
You can do so by creating a class that holds all the animation values and an ‘update’ function that returns an instance of that class. The transition implementation can be extracted into the new separate function. This pattern is useful when there is a need to centralize the animation logic, or make complex animations reusable.
enum class BoxState { Collapsed, Expanded } @Composable fun AnimatingBox(boxState: BoxState) { val transitionData = updateTransitionData(boxState) // UI tree Box( modifier = Modifier .background(transitionData.color) .size(transitionData.size) ) } // Holds the animation values. private class TransitionData( color: State<Color>, size: State<Dp> ) { val color by color val size by size } // Create a Transition and return its animation values. @Composable private fun updateTransitionData(boxState: BoxState): TransitionData { val transition = updateTransition(boxState, label = "box state") val color = transition.animateColor(label = "color") { state -> when (state) { BoxState.Collapsed -> Color.Gray BoxState.Expanded -> Color.Red } } val size = transition.animateDp(label = "size") { state -> when (state) { BoxState.Collapsed -> 64.dp BoxState.Expanded -> 128.dp } } return remember(transition) { TransitionData(color, size) } }
rememberInfiniteTransition
InfiniteTransition
holds one or more child animations like Transition
, but
the animations start running as soon as they enter the composition and do not
stop unless they are removed. You can create an instance of InfiniteTransition
with rememberInfiniteTransition
. Child animations can be added with
animateColor
, animatedFloat
, or animatedValue
. You also need to specify an
infiniteRepeatable to specify the animation
specifications.
val infiniteTransition = rememberInfiniteTransition() val color by infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) Box(Modifier.fillMaxSize().background(color))
Low-level Animation APIs
All the high-level animation APIs mentioned in the previous section are built on top of the foundation of the low-level animation APIs.
The animate*AsState
functions are the simplest APIs, that render an instant
value change as an animation value. It is backed by Animatable
, which is a
coroutine-based API for animating a single value. updateTransition
creates a
transition object that can manage multiple animating values and run them based
on a state change. rememberInfiniteTransition
is similar, but it creates an
infinite transition that can manage multiple animations that keep on running
indefinitely. All of these APIs are composables except for Animatable
, which
means these animations can be created outside of composition.
All of these APIs are based on the more fundamental Animation
API. Though most
apps will not interact directly with Animation
, some of the customization
capabilities for Animation
are available through higher-level APIs. See
Customize animations for more information on
AnimationVector
and AnimationSpec
.
Animatable
Animatable
is a value holder that can animate the value as it is changed via
animateTo
. This is the API backing up the implementation of animate*AsState
.
It ensures consistent continuation and mutual exclusiveness, meaning that the
value change is always continuous and any ongoing animation will be canceled.
Many features of Animatable
, including animateTo
, are provided as suspend
functions. This means that they need to be wrapped in an appropriate coroutine
scope. For example, you can use the LaunchedEffect
composable to create a
scope just for the duration of the specified key value.
// Start out gray and animate to green/red based on `ok` val color = remember { Animatable(Color.Gray) } LaunchedEffect(ok) { color.animateTo(if (ok) Color.Green else Color.Red) } Box(Modifier.fillMaxSize().background(color.value))
In the example above, we create and remember an instance of Animatable
with
the initial value of Color.Gray
. Depending on the value of the boolean flag
ok
, the color animates to either Color.Green
or Color.Red
. Any subsequent
change to the boolean value starts animation to the other color. If there's an
ongoing animation when the value is changed, the animation is canceled, and the
new animation starts from the current snapshot value with the current velocity.
This is the animation implementation that backs up the animate*AsState
API
mentioned in the previous section. Compared to animate*AsState
, using
Animatable
directly gives us finer-grained control on several respects. First,
Animatable
can have an initial value different from its first target value.
For example, the code example above shows a gray box at first, which immediately
starts animating to either green or red. Second, Animatable
provides more
operations on the content value, namely snapTo
and animateDecay
. snapTo
sets the current value to the target value immediately. This is useful when the
animation itself is not the only source of truth and has to be synced with other
states, such as touch events. animateDecay
starts an animation that slows down
from the given velocity. This is useful for implementing fling behavior. See
Gesture and animation for more information.
Out of the box, Animatable
supports Float
and Color
, but any data type can
be used by providing a TwoWayConverter
. See
AnimationVector for more information.
You can customize the animation specifications by providing an AnimationSpec
.
See AnimationSpec for more information.
Animation
Animation
is the lowest-level Animation API available. Many of the animations
we've seen so far build ontop of Animation. There are two Animation
subtypes:
TargetBasedAnimation
and DecayAnimation
.
Animation
should only be used to manually control the time of the animation.
Animation
is stateless, and it does not have any concept of lifecycle. It
serves as an animation calculation engine that the higher-level APIs use.
TargetBasedAnimation
Other APIs cover most use cases, but using TargetBasedAnimation
directly
allows you to control the animation play time yourself. In the example below,
the play time of the TargetAnimation
is manually controlled based on the frame
time provided by withFrameNanos
.
val anim = remember { TargetBasedAnimation( animationSpec = tween(200), typeConverter = Float.VectorConverter, initialValue = 200f, targetValue = 1000f ) } var playTime by remember { mutableStateOf(0L) } LaunchedEffect(anim) { val startTime = withFrameNanos { it } do { playTime = withFrameNanos { it } - startTime val animationValue = anim.getValueFromNanos(playTime) } while (someCustomCondition()) }
DecayAnimation
Unlike TargetBasedAnimation
,
DecayAnimation
does not require a targetValue
to be provided. Instead, it calculates its
targetValue
based on the starting conditions, set by initialVelocity
and
initialValue
and the supplied DecayAnimationSpec
.
Decay animations are often used after a fling gesture to slow elements down to a
stop. The animation velocity starts at the value set by initialVelocityVector
and slows down over time.
Customize animations
Many of the Animation APIs commonly accept parameters for customizing their behavior.
AnimationSpec
Most animation APIs allow developers to customize animation specifications by an
optional AnimationSpec
parameter.
val alpha: Float by animateFloatAsState( targetValue = if (enabled) 1f else 0.5f, // Configure the animation duration and easing. animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) )
There are different kinds of AnimationSpec
for creating different types of
animation.
spring
spring
creates a physics-based animation between start and end values. It
takes 2 parameters: dampingRatio
and stiffness
.
dampingRatio
defines how bouncy the spring should be. The default value is
Spring.DampingRatioNoBouncy
.
stiffness
defines how fast the spring should move toward the end value. The
default value is Spring.StiffnessMedium
.
val value by animateFloatAsState( targetValue = 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioHighBouncy, stiffness = Spring.StiffnessMedium ) )
spring
can handle interruptions more smoothly than duration-based
AnimationSpec
types because it guarantees the continuity of velocity when
target value changes amid animations. spring
is used as the default
AnimationSpec by many animation APIs, such as animate*AsState
and
updateTransition
.
tween
tween
animates between start and end values over the specified
durationMillis
using an easing curve. See Easing for more
information. You can also specify delayMillis
to postpone the start of the
animation.
val value by animateFloatAsState( targetValue = 1f, animationSpec = tween( durationMillis = 300, delayMillis = 50, easing = LinearOutSlowInEasing ) )
keyframes
keyframes
animates based on the snapshot values specified at different
timestamps in the duration of the animation. At any given time, the animation
value will be interpolated between two keyframe values. For each of these
keyframes, Easing can be specified to determine the interpolation curve.
It is optional to specify the values at 0 ms and at the duration time. If you do not specify these values, they default to the start and end values of the animation, respectively.
val value by animateFloatAsState( targetValue = 1f, animationSpec = keyframes { durationMillis = 375 0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms 0.2f at 15 with FastOutLinearInEasing // for 15-75 ms 0.4f at 75 // ms 0.4f at 225 // ms } )
repeatable
repeatable
runs a duration-based animation (such as tween
or keyframes
)
repeatedly until it reaches the specified iteration count. You can pass the
repeatMode
parameter to specify whether the animation should repeat by
starting from the beginning (RepeatMode.Restart
) or from the end
(RepeatMode.Reverse
).
val value by animateFloatAsState( targetValue = 1f, animationSpec = repeatable( iterations = 3, animation = tween(durationMillis = 300), repeatMode = RepeatMode.Reverse ) )
infiniteRepeatable
infiniteRepeatable
is like repeatable
, but it repeats for an infinite amount
of iterations.
val value by animateFloatAsState( targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(durationMillis = 300), repeatMode = RepeatMode.Reverse ) )
In tests using
ComposeTestRule
,
animations using infiniteRepeatable
are not run. The component will be
rendered using the initial value of each animated value.
snap
snap
is a special AnimationSpec
that immediately switches the value to the
end value. You can specify delayMillis
in order to delay the start of the
animation.
val value by animateFloatAsState( targetValue = 1f, animationSpec = snap(delayMillis = 50) )
Easing
Duration-based AnimationSpec
operations (such as tween
or keyframes
) use
Easing
to adjust an animation's fraction. This allows the animating value to
speed up and slow down, rather than moving at a constant rate. Fraction is a
value between 0 (start) and 1.0 (end) indicating the current point in the
animation.
Easing is in fact a function that takes a fraction value between 0 and 1.0 and returns a float. The returned value can be outside the boundary to represent overshoot or undershoot. A custom Easing can be created like the code below.
val CustomEasing = Easing { fraction -> fraction * fraction } @Composable fun EasingUsage() { val value by animateFloatAsState( targetValue = 1f, animationSpec = tween( durationMillis = 300, easing = CustomEasing ) ) // …… }
Compose provides several built-in Easing
functions that cover most use cases.
See Speed - Material
Design for more
information about what Easing to use depending on your scenario.
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- See more
AnimationVector
Most Compose animation APIs support Float
, Color
, Dp
, and other basic data
types as animation values out of the box, but you sometimes need to animate
other data types including your custom ones. During animation, any animating
value is represented as an AnimationVector
. The value is converted into an
AnimationVector
and vice versa by a corresponding TwoWayConverter
so that
the core animation system can handle them uniformly. For example, an Int
is
represented as an AnimationVector1D
that holds a single float value.
TwoWayConverter
for Int
looks like this:
val IntToVector: TwoWayConverter<Int, AnimationVector1D> = TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
Color
is essentially a set of 4 values, red, green, blue, and alpha, so
Color
is converted into an AnimationVector4D
that holds 4 float values. In
this manner, every data type used in animations is converted to either
AnimationVector1D
, AnimationVector2D
, AnimationVector3D
, or
AnimationVector4D
depending on its dimensionality. This allows different
components of the object to be animated independently, each with their own
velocity tracking. Built-in converters for basic data types can be accessed
using Color.VectorConverter
, Dp.VectorConverter
, and so on.
When you want to add support for a new data type as an animating value, you can
create your own TwoWayConverter
and provide it to the API. For example, you
can use animateValueAsState
to animate your custom data type like this:
data class MySize(val width: Dp, val height: Dp) @Composable fun MyAnimation(targetSize: MySize) { val animSize: MySize by animateValueAsState( targetSize, TwoWayConverter( convertToVector = { size: MySize -> // Extract a float value from each of the `Dp` fields. AnimationVector2D(size.width.value, size.height.value) }, convertFromVector = { vector: AnimationVector2D -> MySize(vector.v1.dp, vector.v2.dp) } ) ) }
Animated vector resources (experimental)
To use an AnimatedVectorDrawable
resource, load up the drawable file using animatedVectorResource
and pass in a boolean
to switch between the start and end state of your drawable.
@Composable fun AnimatedVectorDrawable() { val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated) var atEnd by remember { mutableStateOf(false) } Image( painter = rememberAnimatedVectorPainter(image, atEnd), contentDescription = "Timer", modifier = Modifier.clickable { atEnd = !atEnd }, contentScale = ContentScale.Crop ) }
For more information about the format of your drawable file, see Animate drawable graphics.
List item animations
If you are looking to animate item reorderings inside a Lazy list or grid, take a look at the Lazy layout item animation documentation.
Gesture and animation (advanced)
There are several things we have to take into consideration when we are working with touch events and animations, compared to when we are working with animations alone. First of all, we might need to interrupt an ongoing animation when touch events begin as user interaction should have the highest priority.
In the example below, we use an Animatable
to represent the offset position of
a circle component. Touch events are processed with the
pointerInput
modifier. When we detect a new tap event, we call animateTo
to animate the
offset value to the tap position. A tap event can happen during the animation
too, and in that case, animateTo
interrupts the ongoing animation and starts
the animation to the new target position while maintaining the velocity of the
interrupted animation.
@Composable fun Gesture() { val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { coroutineScope { while (true) { // Detect a tap event and obtain its position. awaitPointerEventScope { val position = awaitFirstDown().position launch { // Animate to the tap position. offset.animateTo(position) } } } } } ) { Circle(modifier = Modifier.offset { offset.value.toIntOffset() }) } } private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
Another frequent pattern is we need to synchronize animation values with values
coming from touch events, such as drag. In the example below, we see "swipe to
dismiss" implemented as a Modifier
(rather than using the
SwipeToDismiss
composable). The horizontal offset of the element is represented as an
Animatable
. This API has a characteristic useful in gesture animation. Its
value can be changed by touch events as well as the animation. When we receive a
touch down event, we stop the Animatable
by the stop
method so that any
ongoing animation is intercepted.
During a drag event, we use snapTo
to update the Animatable
value with the
value calculated from touch events. For fling, Compose provides
VelocityTracker
to record drag events and calculate velocity. The velocity can
be fed directly to animateDecay
for the fling animation. When we want to slide
the offset value back to the original position, we specify the target offset
value of 0f
with the animateTo
method.
fun Modifier.swipeToDismiss( onDismissed: () -> Unit ): Modifier = composed { val offsetX = remember { Animatable(0f) } pointerInput(Unit) { // Used to calculate fling decay. val decay = splineBasedDecay<Float>(this) // Use suspend functions for touch events and the Animatable. coroutineScope { while (true) { val velocityTracker = VelocityTracker() // Stop any ongoing animation. offsetX.stop() awaitPointerEventScope { // Detect a touch down event. val pointerId = awaitFirstDown().id horizontalDrag(pointerId) { change -> // Update the animation value with touch events. launch { offsetX.snapTo( offsetX.value + change.positionChange().x ) } velocityTracker.addPosition( change.uptimeMillis, change.position ) } } // No longer receiving touch events. Prepare the animation. val velocity = velocityTracker.calculateVelocity().x val targetOffsetX = decay.calculateTargetValue( offsetX.value, velocity ) // The animation stops when it reaches the bounds. offsetX.updateBounds( lowerBound = -size.width.toFloat(), upperBound = size.width.toFloat() ) launch { if (targetOffsetX.absoluteValue <= size.width) { // Not enough velocity; Slide back. offsetX.animateTo( targetValue = 0f, initialVelocity = velocity ) } else { // The element was swiped away. offsetX.animateDecay(velocity, decay) onDismissed() } } } } } .offset { IntOffset(offsetX.value.roundToInt(), 0) } }
Testing
Compose offers ComposeTestRule
that allows you to write tests for animations
in a deterministic manner with full control over the test clock. This allows you
to verify intermediate animation values. In addition, a test can run quicker
than the actual duration of the animation.
ComposeTestRule
exposes its test clock as mainClock
. You can set the
autoAdvance
property to false to control the clock in your test code. After
initiating the animation you want to test, the clock can be moved forward with
advanceTimeBy
.
One thing to note here is that advanceTimeBy
doesn't move the clock exactly by
the specified duration. Rather, it rounds it up to the nearest duration that is
a multiplier of the frame duration.
@get:Rule val rule = createComposeRule() @Test fun testAnimationWithClock() { // Pause animations rule.mainClock.autoAdvance = false var enabled by mutableStateOf(false) rule.setContent { val color by animateColorAsState( targetValue = if (enabled) Color.Red else Color.Green, animationSpec = tween(durationMillis = 250) ) Box(Modifier.size(64.dp).background(color)) } // Initiate the animation. enabled = true // Let the animation proceed. rule.mainClock.advanceTimeBy(50L) // Compare the result with the image showing the expected result. // `assertAgainGolden` needs to be implemented in your code. rule.onRoot().captureToImage().assertAgainstGolden() }
Tooling support
Android Studio supports inspection of
updateTransition
and
animatedVisibility
in
Animation Preview. You can do the
following:
- Preview a transition frame by frame
- Inspect values for all animations in the transition
- Preview a transition between any initial and target state
- Inspect and coordinate multiple animations at once
When you start Animation Preview, you see the "Animations" pane where you can
run any transition included in the preview. The transition as well as each of
its animation values is labeled with a default name. You can customize the label
by specifying the label
parameter in the updateTransition
and the
AnimatedVisibility
functions. For more information, see
Animation Preview.
Learn more
To learn more about animation in Jetpack Compose, consult the following additional resources: