Modificadores de animación y elementos componibles

Compose incluye elementos componibles y modificadores integrados para controlar casos de uso comunes de animación.

Elementos componibles animados integrados

Compose proporciona varios elementos componibles que animan la aparición, la desaparición y los cambios de diseño del contenido.

Cómo animar la aparición y la desaparición

Elemento componible verde que se muestra y se oculta
Figura 1: Animar la aparición y la desaparición de un elemento en una columna.

El elemento componible AnimatedVisibility anima la aparición y la desaparición de su contenido.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

De forma predeterminada, el contenido aparece atenuado y expandido, y desaparece desvaneciéndose y achicándose. Para personalizar esta transición, especifica objetos EnterTransition y 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)
    )
}

Como se muestra en el ejemplo anterior, puedes combinar varios objetos EnterTransition o ExitTransition con un operador +, y cada uno acepta parámetros opcionales para personalizar su comportamiento. Consulta las páginas de referencia para obtener más información.

Ejemplos de transiciones de entrada y salida

EnterTransition ExitTransition
fadeIn
Un elemento de la IU se desvanece gradualmente hasta que se ve.
fadeOut
Un elemento de la IU se desvanece gradualmente de la vista.
slideIn
Un elemento de la IU se desliza para aparecer en la vista desde fuera de la pantalla.
slideOut
Un elemento de la IU se desliza fuera de la vista de la pantalla.
slideInHorizontally
Un elemento de la IU se desliza horizontalmente para que se vea.
slideOutHorizontally
Un elemento de la IU se desliza horizontalmente fuera de la vista.
slideInVertically
Un elemento de la IU se desliza verticalmente hacia la vista.
slideOutVertically
Un elemento de la IU se desliza verticalmente fuera de la vista.
scaleIn
Un elemento de la IU se agranda y se muestra.
scaleOut
Un elemento de la IU se reduce y desaparece de la vista.
expandIn
Un elemento de la IU se expande en la vista desde un punto central.
shrinkOut
Un elemento de la IU se reduce hasta desaparecer de la vista en un punto central.
expandHorizontally
Un elemento de la IU se expande horizontalmente hasta que se ve.
shrinkHorizontally
Un elemento de la IU se contrae horizontalmente fuera de la vista.
expandVertically
Un elemento de la IU se expande verticalmente hasta que se ve.
shrinkVertically
Un elemento de la IU se contrae verticalmente fuera de la vista.

AnimatedVisibility también ofrece una variante que toma un argumento de MutableTransitionState. De esta manera, puedes activar una animación en cuanto se agrega el elemento AnimatedVisibility componible al árbol de composición. También es útil para observar el estado de la animación.

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

Cómo animar la entrada y la salida de elementos secundarios

El contenido dentro de AnimatedVisibility (elementos secundarios directos o indirectos) puede usar el modificador animateEnterExit a fin de especificar un comportamiento de animación diferente para cada uno de ellos. El efecto visual para cada uno de estos elementos secundarios es una combinación de las animaciones que se especifican en el elemento AnimatedVisibility que admite composición y las animaciones de entrada y salida propias del elemento secundario.

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…
        }
    }
}

En algunos casos, es posible que quieras que AnimatedVisibility no aplique ninguna animación de modo que cada elemento secundario pueda tener sus propias animaciones distintas mediante animateEnterExit. Para lograrlo, especifica EnterTransition.None y ExitTransition.None en el elemento AnimatedVisibility que admite composición.

Cómo agregar animación personalizada

Si quieres agregar efectos de animación personalizada además de las animaciones de entrada y salida integradas, accede a la instancia Transition subyacente a través de la propiedad transition dentro de la lambda de contenido para AnimatedVisibility. Todos los estados de animación que se agreguen a la instancia Transition se ejecutarán de manera simultánea con las animaciones de entrada y salida de AnimatedVisibility. AnimatedVisibility espera hasta que terminen todas las animaciones en Transition antes de quitar su contenido. En el caso de las animaciones de salida que se crean independientemente de Transition (por ejemplo, mediante animate*AsState), AnimatedVisibility no podría responder por ellas y, por lo tanto, es posible que quite el contenido que admite composición antes de finalizar.

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

Para obtener más información sobre el uso de Transition para administrar animaciones, consulta Cómo animar varias propiedades de forma simultánea con una transición.

Animar en función del estado objetivo

El elemento AnimatedContent que admite composición anima su contenido a medida que cambia en función de un estado objetivo.

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

De forma predeterminada, el contenido inicial aplica un fundido de salida y, luego, el contenido objetivo aplica un fundido de entrada (este comportamiento se denomina atenuación). Puedes personalizar este comportamiento de animación si especificas un objeto ContentTransform en el parámetro transitionSpec. Puedes crear una instancia de ContentTransform combinando un objeto EnterTransition con un objeto ExitTransition a través de la función infija with. Puedes aplicar SizeTransform al objeto ContentTransform si lo adjuntas con la función infija using.

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() togetherWith
                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() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition define cómo debe aparecer el contenido objetivo, y ExitTransition define cómo debe desaparecer el contenido inicial. Además de todas las funciones EnterTransition y ExitTransition disponibles para AnimatedVisibility, AnimatedContent ofrece slideIntoContainer y slideOutOfContainer. Estas son alternativas convenientes para slideInHorizontally/Vertically y slideOutHorizontally/Vertically que calculan la distancia de la diapositiva según los tamaños del contenido inicial y el contenido objetivo del contenido AnimatedContent.

SizeTransform define cómo se debe animar el tamaño entre el contenido inicial y el objetivo. Cuando creas la animación, tienes acceso al tamaño inicial y al tamaño objetivo. SizeTransform también controla si el contenido debe recortarse según el tamaño del componente durante las animaciones.

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                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
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

Cómo animar las transiciones de entrada y salida de elementos secundarios

Al igual que AnimatedVisibility, el modificador animateEnterExit está disponible dentro de la lambda de contenido de AnimatedContent. Úsalo para aplicar EnterAnimation y ExitAnimation a cada uno de los elementos secundarios directos o indirectos por separado.

Cómo agregar animación personalizada

Al igual que AnimatedVisibility, el campo transition está disponible dentro de la lambda de contenido de AnimatedContent. Úsalo para crear un efecto de animación personalizado que se ejecuta de manera simultánea con la transición AnimatedContent. Consulta updateTransition para obtener más detalles.

Cómo animar entre dos diseños

Crossfade anima entre dos diseños con una animación de encadenado. Si alternas el valor que se pasa al parámetro current, el contenido se cambia con una animación de encadenado.

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

Modificadores de animación integrados

Compose proporciona modificadores para animar cambios específicos directamente en elementos componibles.

Cómo animar los cambios de tamaño de los elementos componibles

Elemento componible verde que anima su cambio de tamaño de forma fluida.
Figura 2: Elemento componible que muestra una animación fluida entre un tamaño pequeño y uno más grande

El modificador animateContentSize anima un cambio de tamaño.

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

Animaciones de elementos de lista

Si deseas animar los reordenamientos de elementos dentro de una lista o cuadrícula diferida, consulta la documentación de animación de elementos de diseño diferido.