Compete in the Jetpack Compose #AndroidDevChallenge for a chance to win one of over 1,000 prizes, including a Google Pixel 5. Learn more

Animation

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.

This chart below helps you decide what API to use to implement your animation.

  • If you are animating content change in layout:
    • If you are animating enter/exit transition:
      • Use AnimationVisibility.
    • If you are animating changes to content size:
      • Use Modifier.contentSize.
    • Use Crossfade.
  • If the animation is state-based:
    • If the animation happens during composition:
      • If the animation is infinite:
        • Use rememberInfiniteTransition.
      • If you are animation multiple values simultaneously:
        • Use updateTransition.
      • Otherwise, use animate*AsState.
  • If you want to have fine control over animation time:
    • Use Animation.
  • If the animation is the only source of truth
    • Use Animatable.
  • Otherwise, use AnimationState or animate.

Flowchart describing the decision tree for choosing the appropriate animation API

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 (experimental)

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) }
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically(
        initialOffsetY = { -40 }
    ) + expandVertically(
        expandFrom = Alignment.Top
    ) + fadeIn(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.

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")
    }
}

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.

Diagram showing the relationship between the various low-level animation APIs

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).

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, Bounds, 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.

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.

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:

private 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)

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 { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp { 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)
        }
    }
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colors.primary
        BoxState.Expanded -> MaterialTheme.colors.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)
// ...

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)
    val color = transition.animateColor { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Tooling support

Android Studio supports inspection of Transition in Compose Preview.

  • Frame-by-frame preview of the transition
  • Value inspection for all animations in the transition
  • Preview of transition between any initial and target state

When you start the animation inspector, you see below the interactive preview the "Animations" pane where you can run any transition included in the preview. The transition as well as each of the animation values of it is labeled with a default name. You can customize the label by specifying the label parameter in updateTransition and the animate* functions. To learn more about Compose Preview, see Layout preview.

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

TargetBasedAnimation

TargetBasedAnimation is the lowest-level Animation API we've seen so far. 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 withFrameMillis.

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())
}

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.

Animated graphic showing the behavior of different damping ratios

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

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.

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<MySize, AnimationVector2D>(
        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)
            }
        )
    )
}

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.
                        val position = awaitPointerEventScope {
                            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) {
        val decay = splineBasedDecay<Float>(this)
        coroutineScope {
            while (true) {
                // Detect a touch down event.
                val pointerId = awaitPointerEventScope { awaitFirstDown().id }
                val velocityTracker = VelocityTracker()
                // Intercept an ongoing animation (if there's one).
                offsetX.stop()
                awaitPointerEventScope {
                    horizontalDrag(pointerId) { change ->
                        // Update the animation value with touch events.
                        launch {
                            offsetX.snapTo(
                                offsetX.value + change.positionChange().x
                            )
                        }
                        velocityTracker.addPosition(
                            change.uptimeMillis,
                            change.position
                        )
                    }
                }
                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.
    rule.onRoot().captureToImage().assertAgainstGolden()
}

Learn more

To learn more, try the Jetpack Compose Animation codelab.