Understanding Visibility Animations in Jetpack Compose: A Source‑Code Walkthrough
This article explores Jetpack Compose's visibility animation APIs by dissecting the AnimatedVisibility composable, its EnterTransition and ExitTransition parameters, the underlying Transition system, and related helper functions, while providing concrete Kotlin code examples and explanations of how each component works together to produce smooth UI effects.
From Visibility Animation
The article begins by revisiting the simple visibility animation shown in the previous post and introduces a more detailed example using AnimatedVisibility with a mutable state variable.
val visible = remember { mutableStateOf(true) }
AnimatedVisibility(visible = visible.value) {
Text(text = "天青色等烟雨,而我在等你,炊烟袅袅升起,隔江千万里")
}EnterTransition
The EnterTransition sealed class defines how content appears. Its source shows an immutable data holder TransitionData and an overloaded plus operator that allows combining multiple animations such as fadeIn() and expandIn() .
@Immutable
sealed class EnterTransition {
internal abstract val data: TransitionData
@Stable
operator fun plus(enter: EnterTransition): EnterTransition {
return EnterTransitionImpl(
TransitionData(
fade = data.fade ?: enter.data.fade,
slide = data.slide ?: enter.data.slide,
changeSize = data.changeSize ?: enter.data.changeSize,
scale = data.scale ?: enter.data.scale
)
)
}
companion object { val None: EnterTransition = EnterTransitionImpl(TransitionData()) }
}The accompanying data classes ( Fade , Slide , ChangeSize , Scale ) each hold the parameters needed for the respective animation, all of which share an animationSpec of type FiniteAnimationSpec .
ExitTransition
The ExitTransition mirrors EnterTransition with a similar sealed‑class structure and a plus operator for combining exit animations such as shrinkOut() and fadeOut() .
@Immutable
sealed class ExitTransition {
internal abstract val data: TransitionData
@Stable
operator fun plus(exit: ExitTransition): ExitTransition {
return ExitTransitionImpl(
TransitionData(
fade = data.fade ?: exit.data.fade,
slide = data.slide ?: exit.data.slide,
changeSize = data.changeSize ?: exit.data.changeSize,
scale = data.scale ?: exit.data.scale
)
)
}
companion object { val None: ExitTransition = ExitTransitionImpl(TransitionData()) }
}Transition System
The core of the animation logic is the Transition class, which tracks a mutable state and drives child animations. The helper updateTransition creates a Transition from a target boolean and a label.
@Composable
fun
updateTransition(targetState: T, label: String? = null): Transition
{
val transition = remember { Transition(targetState, label = label) }
transition.animateTo(targetState)
DisposableEffect(transition) {
onDispose { transition.onTransitionEnd() }
}
return transition
}Child transitions are generated via createChildTransition and createChildTransitionInternal , which propagate the parent state to a new Transition instance and manage lifecycle cleanup.
AnimatedEnterExitImpl
The composable AnimatedEnterExitImpl ties everything together. It decides whether the content should be visible, creates a child transition, observes its state with LaunchedEffect and snapshotFlow , and finally composes the content inside a custom Layout that applies the appropriate modifiers.
@Composable
private fun
AnimatedEnterExitImpl(
transition: Transition
, visible: (T) -> Boolean,
modifier: Modifier, enter: EnterTransition,
exit: ExitTransition, content: @Composable AnimatedVisibilityScope.() -> Unit
) {
val isAnimationVisible = remember(transition) { mutableStateOf(visible(transition.currentState)) }
if (visible(transition.targetState) || isAnimationVisible.value || transition.isSeeking) {
val childTransition = transition.createChildTransition(label = "EnterExitTransition") { transition.targetEnterExit(visible, it) }
LaunchedEffect(childTransition) {
snapshotFlow { childTransition.currentState == EnterExitState.Visible || childTransition.targetState == EnterExitState.Visible }
.collect { isAnimationVisible.value = it }
}
AnimatedEnterExitImpl(childTransition, modifier, enter = enter, exit = exit, content = content)
}
}The second overload of AnimatedEnterExitImpl receives a Transition<EnterExitState> and, when the state is visible, builds an AnimatedVisibilityScopeImpl and a Layout that applies the combined enter and exit modifiers.
Other Visibility Animations
Additional convenience overloads are provided for RowScope and ColumnScope>, each supplying sensible default enter/exit animations (horizontal for rows, vertical for columns). There are also variants that accept a MutableTransitionState<Boolean> for more explicit state handling.
@Composable
fun RowScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandHorizontally(),
exit: ExitTransition = fadeOut() + shrinkHorizontally(),
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
) { /* implementation similar to the generic version */ }Conclusion
The article demonstrates how Jetpack Compose implements visibility animations by layering immutable data classes, sealed transition types, and a flexible Transition system, allowing developers to compose complex enter/exit effects with concise Kotlin code while keeping the runtime efficient and declarative.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.