New Compose Multiplatform components arrived on Composables UICheck it out →

Component in Compose Animation

AnimatedVisibility

Common

[AnimatedVisibility] composable animates the appearance and disappearance of its content, as [visibleState]'s [targetState][MutableTransitionState.targetState] changes. The [visibleState] can also be used to observe the state of [AnimatedVisibility]. For example: visibleState.isIdle indicates whether all the animations have finished in [AnimatedVisibility], and visibleState.currentState returns the initial state of the current animations.

Different [EnterTransition]s and [ExitTransition]s can be defined in [enter] and [exit] for the appearance and disappearance animation. There are 4 types of [EnterTransition] and [ExitTransition]: Fade, Expand/Shrink, Scale and Slide. The enter transitions can be combined using +. Same for exit transitions. The order of the combination does not matter, as the transition animations will start simultaneously. See [EnterTransition] and [ExitTransition] for details on the three types of transition.

Aside from these three types of [EnterTransition] and [ExitTransition], [AnimatedVisibility] also supports custom enter/exit animations. Some use cases may benefit from custom enter/exit animations on shape, scale, color, etc. Custom enter/exit animations can be created using the Transition<EnterExitState> object from the [AnimatedVisibilityScope] (i.e. [AnimatedVisibilityScope.transition]). See [EnterExitState] for an example of custom animations. These custom animations will be running along side of the built-in animations specified in [enter] and [exit]. In cases where the enter/exit animation needs to be completely customized, [enter] and/or [exit] can be specified as [EnterTransition.None] and/or [ExitTransition.None] as needed. [AnimatedVisibility] will wait until all of enter/exit animations to finish before it considers itself idle. [content] will only be removed after all the (built-in and custom) exit animations have finished.

[AnimatedVisibility] creates a custom [Layout] for its content. The size of the custom layout is determined by the largest width and largest height of the children. All children will be aligned to the top start of the [Layout].

Note: Once the exit transition is finished, the [content] composable will be removed from the tree, and disposed. Both currentState and targetState will be false for [visibleState].

By default, the enter transition will be a combination of [fadeIn] and [expandIn] of the content from the bottom end. And the exit transition will be shrinking the content towards the bottom end while fading out (i.e. [fadeOut] + [shrinkOut]). The expanding and shrinking will likely also animate the parent and siblings if they rely on the size of appearing/disappearing content. When the [AnimatedVisibility] composable is put in a [Row] or a [Column], the default enter and exit transitions are tailored to that particular container. See [RowScope.AnimatedVisibility] and [ColumnScope.AnimatedVisibility] for details.

Last updated:

Installation

dependencies {
   implementation("androidx.compose.animation:animation:1.7.0-beta04")
}

Overloads

@Composable
fun AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = fadeOut() + shrinkOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

Parameters

namedescription
visibleStatedefines whether the content should be visible
modifiermodifier for the [Layout] created to contain the [content]
enterEnterTransition(s) used for the appearing animation, fading in while expanding by default
exitExitTransition(s) used for the disappearing animation, fading out while shrinking by default
contentContent to appear or disappear based on the value of [visibleState]
@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

Parameters

namedescription
visibledefines whether the content should be visible
modifiermodifier for the [Layout] created to contain the [content]
enterEnterTransition(s) used for the appearing animation, fading in while expanding by default
exitExitTransition(s) used for the disappearing animation, fading out while shrinking by default
contentContent to appear or disappear based on the value of [visible]

Code Examples

AnimatedVisibilityLazyColumnSample

@Composable
@Sampled
fun AnimatedVisibilityLazyColumnSample() {
    val turquoiseColors = listOf(
        Color(0xff07688C),
        Color(0xff1986AF),
        Color(0xff50B6CD),
        Color(0xffBCF8FF),
        Color(0xff8AEAE9),
        Color(0xff46CECA)
    )

    // MyModel class handles the data change of the items that are displayed in LazyColumn.
    class MyModel {
        private val _items: MutableList<ColoredItem> = mutableStateListOf()
        private var lastItemId = 0
        val items: List<ColoredItem> = _items

        // Each item has a MutableTransitionState field to track as well as to mutate the item's
        // visibility. When the MutableTransitionState's targetState changes, corresponding
        // transition will be fired. MutableTransitionState allows animation lifecycle to be
        // observed through it's [currentState] and [isIdle]. See below for details.
        inner class ColoredItem(
            val visible: MutableTransitionState<Boolean>,
            val itemId: Int
        ) {
            val color: Color
                get() = turquoiseColors.let {
                    it[itemId % it.size]
                }
        }

        fun addNewItem() {
            lastItemId++
            _items.add(
                ColoredItem(
                    // Here the initial state of the MutableTransitionState is set to false, and
                    // target state is set to true. This will result in an enter transition for
                    // the newly added item.
                    MutableTransitionState(false).apply { targetState = true },
                    lastItemId
                )
            )
        }

        fun removeItem(item: ColoredItem) {
            // By setting the targetState to false, this will effectively trigger an exit
            // animation in AnimatedVisibility.
            item.visible.targetState = false
        }

        fun pruneItems() {
            // Inspect the animation status through MutableTransitionState. If isIdle == true,
            // all animations have finished for the transition.
            _items.removeAll(
                items.filter {
                    // This checks that the animations have finished && the animations are exit
                    // transitions. In other words, the item has finished animating out.
                    it.visible.isIdle && !it.visible.targetState
                }
            )
        }

        fun removeAll() {
            _items.forEach {
                it.visible.targetState = false
            }
        }
    }

    @Composable
    fun AnimatedVisibilityInLazyColumn() {
        Column {
            val model = remember { MyModel() }
            Row(Modifier.fillMaxWidth()) {
                Button(
                    { model.addNewItem() },
                    modifier = Modifier.padding(15.dp).weight(1f)
                ) {
                    Text("Add")
                }
            }

            // This sets up a flow to check whether any item has finished animating out. If yes,
            // notify the model to prune the list.
            LaunchedEffect(model) {
                snapshotFlow {
                    model.items.firstOrNull { it.visible.isIdle && !it.visible.targetState }
                }.collect {
                    if (it != null) {
                        model.pruneItems()
                    }
                }
            }
            LazyColumn {
                items(model.items, key = { it.itemId }) { item ->
                    AnimatedVisibility(
                        item.visible,
                        enter = expandVertically(),
                        exit = shrinkVertically()
                    ) {
                        Box(
                            Modifier.fillMaxWidth().requiredHeight(90.dp)
                                .background(item.color)
                        ) {
                            Button(
                                { model.removeItem(item) },
                                modifier = Modifier.align(Alignment.CenterEnd)
                                    .padding(15.dp)
                            ) {
                                Text("Remove")
                            }
                        }
                    }
                }
            }

            Button(
                { model.removeAll() },
                modifier = Modifier.align(Alignment.End).padding(15.dp)
            ) {
                Text("Clear All")
            }
        }
    }
}

FullyLoadedTransition

@Composable
@Sampled
@OptIn(ExperimentalAnimationApi::class
fun FullyLoadedTransition() {
    var visible by remember { mutableStateOf(true) }
    AnimatedVisibility(
        visible = visible,
        enter = slideInVertically(
            // Start the slide from 40 (pixels) above where the content is supposed to go, to
            // produce a parallax effect
            initialOffsetY = { -40 }
        ) + expandVertically(
            expandFrom = Alignment.Top
        ) + scaleIn(
            // Animate scale from 0f to 1f using the top center as the pivot point.
            transformOrigin = TransformOrigin(0.5f, 0f)
        ) + fadeIn(initialAlpha = 0.3f),
        exit = slideOutVertically() + shrinkVertically() + fadeOut() + scaleOut(targetScale = 1.2f)
    ) {
        // Content that needs to appear/disappear goes here:
        Text("Content to appear/disappear", Modifier.fillMaxWidth().requiredHeight(200.dp))
    }
}

AnimatedVisibilityWithBooleanVisibleParamNoReceiver

@Composable
@Sampled
@OptIn(ExperimentalAnimationApi::class
fun AnimatedVisibilityWithBooleanVisibleParamNoReceiver() {
    var visible by remember { mutableStateOf(true) }
    Box(modifier = Modifier.clickable { visible = !visible }) {
        AnimatedVisibility(
            visible = visible,
            modifier = Modifier.align(Alignment.Center),
            enter = fadeIn(),
            exit = fadeOut(animationSpec = tween(200)) + scaleOut()
        ) { // Content that needs to appear/disappear goes here:
            // Here we can optionally define a custom enter/exit animation by creating an animation
            // using the Transition<EnterExitState> object from AnimatedVisibilityScope:

            // As a part of the enter transition, the corner radius will be animated from 0.dp to 50.dp.
            val cornerRadius by transition.animateDp {
                when (it) {
                    EnterExitState.PreEnter -> 0.dp
                    EnterExitState.Visible -> 50.dp
                    // No corner radius change when exiting.
                    EnterExitState.PostExit -> 50.dp
                }
            }
            Box(
                Modifier.background(Color.Red, shape = RoundedCornerShape(cornerRadius))
                    .height(100.dp).fillMaxWidth()
            )
        }
    }
}