Guía rápida sobre las animaciones en Compose

Compose tiene muchos mecanismos de animación integrados, y puede ser abrumador saber cuál elegir. A continuación, se incluye una lista de casos de uso comunes de las animaciones. Para obtener información más detallada sobre el conjunto completo de las diferentes opciones de API disponibles, lee la documentación de Compose Animation completa.

Cómo animar propiedades de funciones de componibilidad comunes

Compose proporciona APIs convenientes que te permiten resolver muchos casos de uso de animación comunes. En esta sección, se muestra cómo puedes animar propiedades comunes de una función de componibilidad.

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

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

Usa AnimatedVisibility para ocultar o mostrar un elemento componible. Los elementos secundarios dentro de AnimatedVisibility pueden usar Modifier.animateEnterExit() para su propia transición de entrada o salida.

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
    // ...
}

Los parámetros de entrada y salida de AnimatedVisibility te permiten configurar cómo se comporta un elemento componible cuando aparece y desaparece. Lee la documentación completa para obtener más información.

Otra opción para animar la visibilidad de una función de componibilidad es animar el valor alfa con el tiempo usando animateFloatAsState:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

Sin embargo, cambiar el valor alfa tiene la advertencia de que el elemento de componibilidad permanece en la composición y sigue ocupando el espacio en el que se encuentra. Esto podría hacer que los lectores de pantalla y otros mecanismos de accesibilidad sigan considerando el elemento en pantalla. Por otro lado, AnimatedVisibility quita el elemento de la composición.

Cómo animar el valor alfa de un elemento componible
Figura 2: Cómo animar el valor alfa de un elemento componible

Cómo animar el color de fondo

Elemento componible con un color de fondo que cambia con el tiempo como una animación, en la que los colores se desvanecen entre sí.
Figura 3: Animación del color de fondo de la función de componibilidad

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

Esta opción tiene un mejor rendimiento que el uso de Modifier.background(). Modifier.background() es aceptable para un parámetro de configuración de color de un solo uso, pero, cuando se anima un color con el tiempo, esto podría causar más recomposiciones de las necesarias.

Para animar el color de fondo de forma infinita, consulta la sección sobre cómo repetir una animación.

Cómo animar el tamaño de un elemento componible

Elemento de componibilidad verde que anima su cambio de tamaño de forma fluida.
Figura 4: Elemento componible que se anima de forma fluida entre un tamaño pequeño y uno más grande

Compose te permite animar el tamaño de los elementos que admiten composición de varias maneras. Usa animateContentSize() para las animaciones entre los cambios de tamaño de los elementos componibles.

Por ejemplo, si tienes un cuadro que contiene texto que se puede expandir de una a varias líneas, puedes usar Modifier.animateContentSize() para lograr una transición más fluida:

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
        }

) {
}

También puedes usar AnimatedContent con un SizeTransform para describir cómo deben realizarse los cambios de tamaño.

Cómo animar la posición de un elemento componible

Elemento componible verde que se anima de forma fluida hacia abajo y hacia la derecha
Figura 5: Composable que se mueve con un desplazamiento

Para animar la posición de una función de componibilidad, usa Modifier.offset{ } combinado con animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

Si quieres asegurarte de que los elementos componibles no se dibujen sobre otros elementos componibles ni debajo de ellos cuando se anime la posición o el tamaño, usa Modifier.layout{ }. Este modificador propaga los cambios de tamaño y posición al elemento superior, lo que, a su vez, afecta a otros elementos secundarios.

Por ejemplo, si mueves un Box dentro de un Column y los otros elementos secundarios deben moverse cuando se mueve el Box, incluye la información de desplazamiento con Modifier.layout{ } de la siguiente manera:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

2 cuadros,en los que el segundo anima su posición X, Y y el tercero responde moviéndose también en la cantidad Y.
Figura 6: Cómo animar con Modifier.layout{ }

Cómo animar el padding de un elemento componible

Elemento componible verde que se hace más pequeño y más grande al hacer clic, con padding animado
Figura 7: Elemento componible con su padding animado

Para animar el padding de una función de componibilidad, usa animateDpAsState combinado con Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

Cómo animar la elevación de un elemento componible

Figura 8. Animación de la elevación de la función de componibilidad cuando se hace clic en ella

Para animar la elevación de un elemento que admite composición, usa animateDpAsState combinado con Modifier.graphicsLayer{ }. Para los cambios de elevación únicos, usa Modifier.shadow(). Si animas la sombra, usar el modificador Modifier.graphicsLayer{ } es la opción con mejor rendimiento.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

Como alternativa, usa el elemento de componibilidad Card y establece la propiedad de elevación en diferentes valores por estado.

Cómo animar la escala, la traducción o la rotación del texto

Un elemento de texto componible que dice Hello y se anima entre un tamaño pequeño y uno más grande.
Figura 9: Animación fluida del texto entre dos tamaños

Cuando animes la escala, la traslación o la rotación del texto, establece el parámetro textMotion en TextStyle como TextMotion.Animated. Esto garantiza transiciones más fluidas entre las animaciones de texto. Usa Modifier.graphicsLayer{ } para traducir, rotar o ajustar el texto.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Anima el color del texto

Las palabras Hello Compose animando su color entre verde y azul
Figura 10: Ejemplo que muestra la animación del color del texto

Para animar el color del texto, usa la expresión lambda color en el elemento BasicText componible:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

Cómo cambiar entre diferentes tipos de contenido

Pantalla verde que dice Loading, pantalla azul que dice Loaded y pantalla blanca que dice Error. Se iteran los diferentes elementos componibles con una animación simple.
Figura 11: Uso de AnimatedContent para animar cambios entre diferentes elementos componibles (en cámara lenta)

Usa AnimatedContent para animar entre diferentes elementos componibles. Si solo quieres un fundido estándar entre elementos componibles, usa Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

AnimatedContent se puede personalizar para mostrar muchos tipos diferentes de transiciones de entrada y salida. Para obtener más información, consulta la documentación sobre AnimatedContent o lee esta entrada de blog sobre AnimatedContent.

Cómo agregar animaciones mientras se navega a diferentes destinos

Dos elementos componibles, uno verde que dice Landing y uno azul que dice Detail, que se animan deslizando el elemento componible de detalles sobre el de destino.
Figura 12. Cómo animar entre funciones de componibilidad con navigation-compose

Para animar las transiciones entre elementos componibles cuando se usa el artefacto navigation-compose, especifica enterTransition y exitTransition en un elemento componible. También puedes establecer la animación predeterminada que se usará para todos los destinos en el NavHost de nivel superior:

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

Existen muchos tipos diferentes de transiciones de entrada y salida que aplican diferentes efectos al contenido entrante y saliente. Consulta la documentación para obtener más información.

Cómo repetir una animación

Un fondo verde que se transforma en un fondo azul de forma infinita con una animación entre los dos colores.
Figura 13. Animación del color de fondo entre dos valores, de forma infinita

Usa rememberInfiniteTransition con un infiniteRepeatable animationSpec para repetir la animación de forma continua. Cambia RepeatModes para especificar cómo debe ir y volver.

Usa repeatable para repetir una cantidad de veces establecida.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

Cómo iniciar una animación cuando se inicia un elemento componible

LaunchedEffect se ejecuta cuando un elemento componible ingresa a la composición. Inicia una animación cuando se inicia un elemento componible. Puedes usarlo para controlar el cambio de estado de la animación. Usa Animatable con el método animateTo para iniciar la animación en el lanzamiento:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Cómo crear animaciones secuenciales

Cuatro círculos con flechas verdes que se animan entre sí, una tras otra.
Figura 14: Diagrama que indica cómo progresa una animación secuencial, una por una.

Usa las APIs de corrutinas de Animatable para realizar animaciones secuenciales o simultáneas. Llamar a animateTo en Animatable una tras otra hace que cada animación espere a que finalicen las animaciones anteriores antes de continuar. Esto se debe a que es una función de suspensión.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

Cómo crear animaciones simultáneas

Tres círculos con flechas verdes que se animan hacia cada uno de ellos, todos al mismo tiempo.
Figura 15. Diagrama que indica cómo progresan las animaciones simultáneas, todas al mismo tiempo.

Usa las APIs de corrutinas (Animatable#animateTo() o animate) o la API de Transition para lograr animaciones simultáneas. Si usas varias funciones de inicio en un contexto de corrutina, se iniciarán las animaciones al mismo tiempo:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

Puedes usar la API de updateTransition para usar el mismo estado y controlar muchas animaciones de propiedades diferentes al mismo tiempo. En el siguiente ejemplo, se animan dos propiedades controladas por un cambio de estado, rect y borderWidth:

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "transition")

val rect by transition.animateRect(label = "rect") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "borderWidth") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Optimiza el rendimiento de las animaciones

Las animaciones en Compose pueden causar problemas de rendimiento. Esto se debe a la naturaleza de lo que es una animación: mover o cambiar píxeles en la pantalla rápidamente, fotograma por fotograma, para crear la ilusión de movimiento.

Considera las diferentes fases de Compose: composición, diseño y dibujo. Si tu animación cambia la fase de diseño, requiere que todos los elementos componibles afectados se vuelvan a diseñar y dibujar. Si la animación se produce en la fase de dibujo, de forma predeterminada, tiene un mejor rendimiento que si la ejecutaras en la fase de diseño, ya que tendría menos trabajo que hacer en general.

Para asegurarte de que tu app haga lo menos posible durante la animación, elige la versión lambda de un Modifier siempre que sea posible. Esto omite la recomposición y realiza la animación fuera de la fase de composición. De lo contrario, usa Modifier.graphicsLayer{ }, ya que este modificador siempre se ejecuta en la fase de dibujo. Para obtener más información, consulta la sección Cómo aplazar las lecturas en la documentación de rendimiento.

Cómo cambiar el tiempo de la animación

De forma predeterminada, Compose usa animaciones de resorte para la mayoría de las animaciones. Los resortes, o las animaciones basadas en la física, se sienten más naturales. También se pueden interrumpir, ya que tienen en cuenta la velocidad actual del objeto, en lugar de un tiempo fijo. Si quieres anular el valor predeterminado, todas las APIs de Animation que se mostraron anteriormente tienen la capacidad de establecer un animationSpec para personalizar cómo se ejecuta una animación, ya sea que quieras que se ejecute durante un período determinado o que sea más elástica.

A continuación, se incluye un resumen de las diferentes opciones de animationSpec:

  • spring: Animación basada en la física, la opción predeterminada para todas las animaciones. Puedes cambiar la rigidez o la relación de amortiguación para lograr un aspecto diferente de la animación.
  • tween (abreviatura de between): Es una animación basada en la duración que se anima entre dos valores con una función Easing.
  • keyframes: Es la especificación para indicar valores en ciertos puntos clave de una animación.
  • repeatable: Es una especificación basada en la duración que se ejecuta una cantidad determinada de veces, especificada por RepeatMode.
  • infiniteRepeatable: Es una especificación basada en la duración que se ejecuta de forma indefinida.
  • snap: Se ajusta de forma instantánea al valor final sin ninguna animación.
Dos animaciones que muestran un conjunto de especificaciones no establecido en comparación con un conjunto de especificaciones de Spring personalizado.
Figura 16. No spec set versus Custom Spring spec set

Lee la documentación completa para obtener más información sobre animationSpecs.

Recursos adicionales

Para ver más ejemplos de animaciones divertidas en Compose, consulta los siguientes recursos: