Modificadores de animación y elementos componibles

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

Elementos componibles animados integrados

Anima la apariencia y la desaparición con AnimatedVisibility

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. Puedes personalizar la transición si especificas 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 puedes ver 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 referencias para obtener más información.

Ejemplos de EnterTransition y ExitTransition

EnterTransition ExitTransition
fadeIn
animación de fundido de entrada
fadeOut
animación de fundido de salida
slideIn
animación de deslizamiento de entrada
slideOut
animación de deslizamiento de salida
slideInHorizontally
animación de deslizamiento de entrada horizontal
slideOutHorizontally
animación de deslizamiento de salida horizontal
slideInVertically
animación de deslizamiento de entrada vertical
slideOutVertically
animación de deslizamiento de salida vertical
scaleIn
animación de reducción de escala
scaleOut
animación de ampliación de escala
expandIn
animación de expansión
shrinkOut
animación de reducción
expandHorizontally
animación de expansión horizontal
shrinkHorizontally
animación de reducción horizontal
expandVertically
animación de expansión vertical
shrinkVertically
animación de reducción vertical

AnimatedVisibility también ofrece una variante que toma un MutableTransitionState. De esta manera, puedes activar una animación en cuanto se agrega AnimatedVisibility al árbol de la 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)
    )
}

Consulta updateTransition para obtener los detalles sobre Transition.

Crea animaciones basadas en el estado objetivo con AnimatedContent

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

Ten en cuenta que siempre debes usar el parámetro lambda y reflejarlo en el contenido. La API usa este valor como clave para identificar el contenido que se muestra en el momento.

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 ContentTransform si combinas EnterTransition con ExitTransition mediante la función infija with. Puedes aplicar SizeTransform a 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 transiciones secundarias de entrada y salida

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.

Anima entre dos diseños con Crossfade

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

Cómo animar los cambios de tamaño del elemento componible con animateContentSize

Elemento componible verde que anima su cambio de tamaño sin problemas.
Figura 2: Elemento componible que se anima de forma 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.