Compose

Animations in Jetpack Compose: From Basics to Shared Element Transitions

Ahmad Najar Ahmad Najar 1 min read

Compose Makes Good Animation the Path of Least Resistance

Animation in the old View system usually meant ObjectAnimator, XML animator resources, or reaching for a library. Compose bakes animation primitives directly into the state model, so "animate this value changing" is often a one-line change.

Level 1: animate a single value

val scale by animateFloatAsState(
    targetValue = if (pressed) 1.2f else 1f,
    label = "scale"
)
Box(modifier = Modifier.scale(scale))

Whenever pressed changes, Compose automatically animates scale toward the new target — no manual animator setup, no cleanup to forget.

Level 2: animate visibility

AnimatedVisibility(
    visible = showDetails,
    enter = fadeIn() + expandVertically(),
    exit = fadeOut() + shrinkVertically()
) {
    DetailsCard()
}

This replaces a surprising amount of manual height/alpha animator code you'd otherwise write by hand in the View system.

Level 3: animate between different content

AnimatedContent(targetState = selectedTab, label = "tab") { tab ->
    when (tab) {
        Tab.Home -> HomeScreen()
        Tab.Profile -> ProfileScreen()
    }
}

AnimatedContent handles the transition between two entirely different composables, with customizable enter/exit transforms per direction — useful for tab switches or wizard-style flows.

Level 4: shared element transitions

As of recent Compose versions, shared element transitions (an item in a list morphing into its detail screen) are supported natively via SharedTransitionLayout:

SharedTransitionLayout {
    AnimatedContent(targetState = selected) { item ->
        if (item == null) {
            LazyColumn {
                items(list) { i ->
                    ItemRow(
                        i,
                        Modifier.sharedElement(
                            rememberSharedContentState(key = i.id),
                            animatedVisibilityScope = this@AnimatedContent
                        )
                    )
                }
            }
        } else {
            DetailScreen(
                item = selected,
                modifier = Modifier.sharedElement(
                    rememberSharedContentState(key = selected.id),
                    animatedVisibilityScope = this@AnimatedContent
                )
            )
        }
    }
}

This is the same visual effect as Flutter's Hero widget — an element visually "flies" between two screens — but expressed through Compose's state-driven model rather than an explicit widget wrapper.

Practical guidance

  • Start with animateXAsState for anything single-value — it covers 80% of real use cases.
  • Reach for AnimatedVisibility/AnimatedContent once you're animating whole composables appearing, disappearing, or swapping.
  • Save SharedTransitionLayout for genuine navigation transitions where continuity between screens meaningfully improves the UX — it's more setup than it looks and easy to overuse.
  • Always give animations a label — it makes debugging in Android Studio's animation inspector far less painful.
Compose