Guía rápida sobre las animaciones en Compose

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

Cómo animar propiedades comunes de componibilidad

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

Mostrar animaciones que aparezcan o desaparezcan

Elemento componible verde que se muestra y se oculta
Figura 1: Cómo animar la apariencia 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 el comportamiento de 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 un elemento componible es animar la versión 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, el cambio de alfa incluye la salvedad de que el elemento componible permanece en la composición y continúa ocupando el espacio en el que está dispuesto. Esto podría hacer que los lectores de pantalla y otros mecanismos de accesibilidad aún consideren el elemento en pantalla. Por otro lado, AnimatedVisibility quita el elemento de la composición con el tiempo.

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

Color de fondo animado

Elemento componible con el color de fondo que cambia con el tiempo como una animación, donde los colores se desvanecen entre sí
Figura 3: Cómo animar el color de fondo de un elemento componible

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 usar Modifier.background(). Modifier.background() es aceptable para una configuración de color con un solo ejemplo, pero, cuando se anima un color con el tiempo, esto podría provocar más recomposiciones de las necesarias.

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

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

Elemento componible verde que anima su cambio de tamaño sin problemas
Figura 4: Elemento componible que anima suavemente entre un tamaño pequeño y uno más grande

Compose te permite animar el tamaño de los elementos componibles de diferentes maneras. Usa animateContentSize() para animaciones entre cambios de tamaño 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 se deben realizar los cambios de tamaño.

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

El elemento verde componible se anima suavemente hacia abajo y hacia la derecha
Figura 5: Elemento componible que se mueve por un desplazamiento

Para animar la posición de un elemento componible, 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 deseas asegurarte de que los elementos componibles no se dibujen encima ni debajo de otros elementos componibles cuando animes 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 luego afecta a otros elementos secundarios.

Por ejemplo, si mueves un Box dentro de una 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 con el segundo cuadro que anima su posición X e Y, y el tercer cuadro responde moviéndose a sí mismo una cantidad Y.
Figura 6: Cómo animar con Modifier.layout{ }

Cómo animar el padding de un elemento componible

El elemento verde que admite composición se vuelve cada vez más pequeño cuando se hace clic en él, y el padding se anima
Figura 7: Se puede componer con su animación de padding

Para animar el padding de un elemento componible, 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 elevación del elemento componible cuando se hace clic

Para animar la elevación de un elemento componible, usa animateDpAsState combinado con Modifier.graphicsLayer{ }. Para cambios de elevación únicos, usa Modifier.shadow(). Si animas la sombra, el uso del 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 componible Card y establece la propiedad de elevación en diferentes valores por estado.

Cómo animar la escala, la traslación o la rotación de texto

Elemento componible de texto que dice
Figura 9: Animación de texto fluida entre dos tamaños

Cuando animes la escala, la traslación o la rotación del texto, establece el parámetro textMotion de TextStyle en TextMotion.Animated. De esta manera, se 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)
    )
}

Color del texto animado

Aparece la frase
Figura 10: Ejemplo en el que se muestra la animación del color de 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
    },
    // ...
)

Alterna entre diferentes tipos de contenido

Pantalla verde que dice
Figura 11: Cómo usar AnimatedContent para animar cambios entre diferentes elementos componibles (demorado)

Usa AnimatedContent para animar entre diferentes elementos componibles. Si solo quieres una atenuación 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, lee la documentación sobre AnimatedContent o lee esta entrada de blog sobre AnimatedContent.

Haz animaciones mientras navegas a diferentes destinos

Dos elementos componibles, uno verde que dice Landing y uno azul que dice Detalle, que se anima deslizando el elemento de detalle componible sobre el elemento de destino componible.
Figura 12: Cómo animar entre elementos componibles 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 configurar la animación predeterminada para que se use en todos los destinos en el nivel superior NavHost:

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 distintos 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 infinitamente a través de una animación entre los dos colores.
Figura 13. Color de fondo de una animación entre dos valores, infinitamente

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 finiteRepeatable para repetir una cantidad determinada de veces.

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 usarla para impulsar el cambio de estado de la animación. Usa Animatable con el método animateTo para iniciar la animación durante el inicio:

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 cada uno de ellos, uno por uno tras otro.
Figura 14: Diagrama que indica cómo progresa una animación secuencial, uno por uno.

Usa las APIs de corrutinas Animatable para realizar animaciones secuenciales o simultáneas. Llamar a animateTo en el Animatable uno tras otro 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 a cada uno y se animan todos al mismo tiempo.
Figura 15: Diagrama en el que se indica cómo progresan las animaciones simultáneas, todo 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 corrutinas, 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)
    }
}

Podrías usar la API de updateTransition para usar el mismo estado y así conducir 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
    }
}

Cómo optimizar el rendimiento de la animación

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 los píxeles en 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, requerirá que se vuelvan a diseñar y dibujar todos los elementos componibles afectados. Si la animación se produce en la fase de dibujo, tendrá un mejor rendimiento de forma predeterminada que si ejecutaras la animación en la fase de diseño, ya que tendría menos trabajo para hacer en general.

Para asegurarte de que tu app realice el menor esfuerzo 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 sobre este tema, consulta la sección Aplaza las lecturas en la documentación de rendimiento.

Cómo cambiar la sincronización de la animación

De forma predeterminada, Compose usa animaciones de spring para la mayoría de las animaciones. Springs, o las animaciones basadas en la física, parecen 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 deseas anular el valor predeterminado, todas las APIs de Animation que se mostraron antes tienen la capacidad de configurar un animationSpec para personalizar la forma en que se ejecuta una animación, ya sea que desees que se ejecute durante una duración determinada o que sea más dinámica.

El siguiente es un resumen de las diferentes opciones de animationSpec:

  • spring: Animación basada en la física, que es la predeterminada para todas las animaciones. Puedes cambiar la rigidez o el dampingRatio para lograr un aspecto diferente de la animación.
  • tween (abreviatura de entre): Animación basada en la duración, animada entre dos valores con una función Easing.
  • keyframes: Especificación para especificar valores en ciertos puntos clave de una animación.
  • repeatable: Especificación basada en la duración que se ejecuta una cierta cantidad de veces, especificada por RepeatMode.
  • infiniteRepeatable: Especificación basada en la duración que se ejecuta de forma permanente.
  • snap: Se ajusta de forma instantánea al valor final sin ninguna animación.
Escribe tu texto alternativo aquí
Figura 16: Sin conjunto de especificaciones en comparación con un conjunto de especificaciones de resorte personalizado

Para obtener más información sobre animationSpecs, lee la documentación completa.

Recursos adicionales

Para ver más ejemplos de animaciones divertidas en Compose, consulta lo siguiente: