Animaciones basadas en el valor

Anima un solo valor con animate*AsState

Las funciones animate*AsState son las APIs de animación más simples en Compose para animar un solo valor. Solo debes proporcionar el valor objetivo (o valor final), y la API comienza la animación desde el valor actual hasta el especificado.

A continuación, se muestra un ejemplo de animación de alfa con esta API. Con solo unir el valor objetivo en animateFloatAsState, el valor alfa ahora es un valor de animación entre los valores proporcionados (1f o 0.5f en este caso).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Ten en cuenta que no necesitas crear una instancia de ninguna clase de animación ni procesar la interrupción. De forma interna, se creará un objeto de animación (es decir, una instancia de Animatable) y se lo recordará en el sitio que realiza la llamada, con el primer valor objetivo como valor inicial. A partir de ese momento, cada vez que proporciones un valor objetivo diferente a este elemento componible, se iniciará automáticamente una animación con ese valor. Si ya hay una animación en curso, esta comienza desde su valor (y velocidad) actual, y se anima al valor objetivo. Durante la animación, este elemento componible se vuelve a componer y muestra un valor actualizado de la animación en cada fotograma.

Desde el primer momento, Compose brinda funciones animate*AsState para Float, Color, Dp, Size, Offset, Rect, Int, IntOffset y IntSize. Puedes agregar compatibilidad con otros tipos de datos si proporcionas un TwoWayConverter a animateValueAsState que tome un tipo genérico.

Puedes personalizar las especificaciones de la animación si proporcionas un AnimationSpec. Consulta AnimationSpec para obtener más información.

Cómo animar varias propiedades simultáneamente con una transición

Transition administra una o más animaciones como elementos secundarios, y las ejecuta de forma simultánea entre varios estados.

Los estados pueden ser de cualquier tipo de datos. En muchos casos, puedes usar un tipo enum personalizado para garantizar la seguridad del tipo, como en este ejemplo:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition crea y recuerda una instancia de Transition, y actualiza su estado.

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

Luego, puedes usar una de las funciones de extensión animate* para definir una animación secundaria en esta transición. Especifica los valores objetivo para cada uno de los estados. Estas funciones animate* muestran un valor de animación que se actualiza con cada fotograma durante la animación cuando el estado de transición se actualiza con updateTransition.

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

De manera opcional, puedes pasar un parámetro transitionSpec a fin de especificar un AnimationSpec diferente para cada una de las combinaciones de cambios de estado de transición. Consulta AnimationSpec para obtener más información.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

Una vez que haya una transición en el estado objetivo, Transition.currentState será la misma que en Transition.targetState. Esto se puede usar como indicador para comprobar si finalizó la transición.

En ocasiones, queremos tener un estado inicial diferente del primer estado objetivo. Podemos usar updateTransition con MutableTransitionState para lograrlo. Por ejemplo, nos permite iniciar la animación en cuanto el código entra en conflicto.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

En el caso de una transición más compleja que involucra varias funciones que admiten composición, puedes usar createChildTransition para crear una transición secundaria. Esta técnica es útil para separar los problemas entre varios subcomponentes en un elemento complejo que admite composición. La transición superior tendrá en cuenta todos los valores de animación en las transiciones secundarias.

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Usa la transición con AnimatedVisibility y AnimatedContent

AnimatedVisibility y AnimatedContent están disponibles como funciones de extensión de Transition. targetState para Transition.AnimatedVisibility y Transition.AnimatedContent se deriva de Transition y activa la transición de entrada y salida en los casos necesarios cuando cambia el targetState de Transition. Estas funciones de extensión permiten que todas las animaciones de entrada, salida y sizeTransform que, de lo contrario, serían internas para AnimatedVisibility o AnimatedContent se eleven a Transition. Con estas funciones de extensión, el cambio de estado de AnimatedVisibility o AnimatedContent se puede observar desde el exterior. En lugar de un parámetro booleano visible, esta versión de AnimatedVisibility toma una lambda que convierte el estado objetivo de la transición superior en un valor booleano.

Consulta AnimatedVisibility y AnimatedContent para obtener más detalles.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Encapsula una transición y hazla reutilizable

Para casos de uso simples, definir las animaciones de transición en el mismo elemento componible que tu IU es una opción perfectamente válida. Sin embargo, si trabajas en un componente complejo con una serie de valores animados, es posible que quieras separar la implementación de la animación de la IU del elemento componible.

Para hacerlo, crea una clase que contenga todos los valores de animación y una función "update" que muestre una instancia de esa clase. La implementación de la transición se puede extraer en la nueva función separada. Este patrón es útil cuando hay una necesidad de centralizar la lógica de animación o hacer que las animaciones complejas se puedan volver a usar.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Crea una animación que se repita infinitamente con rememberInfiniteTransition

InfiniteTransition contiene una o más animaciones secundarias, como Transition, pero las animaciones comienzan a ejecutarse apenas entran en la composición y no se detienen, a menos que se las quite. Puedes crear una instancia de InfiniteTransition con rememberInfiniteTransition. Se pueden agregar animaciones secundarias con animateColor, animatedFloat o animatedValue. También debes especificar un infiniteRepeatable para indicar las especificaciones de la animación.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

APIs de animación de bajo nivel

Todas las API de Animation de alto nivel mencionadas en la sección anterior se compilan sobre la base de las API de Animation de bajo nivel.

Las funciones animate*AsState son las API más simples que procesan un cambio de valor instantáneo como un valor de animación. Cuenta con el respaldo de Animatable, que es una API basada en corrutinas para animar un valor único. updateTransition crea un objeto de transición que puede administrar múltiples valores de animación y ejecutarlos según un cambio de estado. rememberInfiniteTransition es similar, pero crea una transición infinita que puede administrar varias animaciones que se mantienen en ejecución indefinidamente. Todas estas API son componibles, excepto Animatable, lo que significa que se pueden crear estas animaciones fuera de la composición.

Todas estas API se basan en la API de Animation más básica. Si bien la mayoría de las apps no interactuarán directamente con Animation, algunas de las capacidades de personalización de Animation están disponibles a través de API de nivel superior. Consulta Cómo personalizar animaciones para obtener más información sobre AnimationVector y AnimationSpec.

Diagrama que muestra la relación entre las diversas API de Animation de bajo nivel

Animatable: Animación de un solo valor basada en corrutinas

Animatable es un contenedor de valor que puede animar el valor a medida que se modifica a través de animateTo. Esta es la API que respalda la implementación de animate*AsState. Garantiza una continuación coherente y una exclusividad mutua, lo que significa que el cambio de valor es siempre continuo, y cualquier animación en curso será cancelada.

Muchas funciones de Animatable, como animateTo, se proporcionan como funciones de suspensión. Por lo tanto, deben unirse a un alcance de corrutinas apropiado. Por ejemplo, puedes usar el elemento componible LaunchedEffect para crear un alcance únicamente para la duración del par clave-valor especificado.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

En el ejemplo anterior, creamos y recordamos una instancia de Animatable con el valor inicial de Color.Gray. Según el valor de la marca booleana ok, el color se anima a Color.Green o Color.Red. Cualquier cambio posterior al valor booleano inicia la animación en el otro color. Si hay una animación en curso cuando cambia el valor, la animación se cancela, y la animación nueva comienza desde el valor de instantánea actual con la velocidad actual.

Esta es la implementación de animación que crea una copia de seguridad de la API de animate*AsState mencionada en la sección anterior. En comparación con animate*AsState, el uso directo de Animatable brinda un control más preciso sobre varios aspectos. En primer lugar, Animatable puede tener un valor inicial diferente del primer valor objetivo. Por ejemplo, el código de ejemplo anterior muestra un cuadro gris al principio, que comienza inmediatamente a animarse en verde o rojo. En segundo lugar, Animatable proporciona más operaciones sobre el valor del contenido, es decir, snapTo y animateDecay. snapTo establece el valor actual en el valor objetivo de inmediato. Esto es útil cuando la animación en sí no es la única fuente de confianza y debe sincronizarse con otros estados, como eventos táctiles. animateDecay inicia una animación que se ralentiza a partir de la velocidad determinada. Esto es útil para implementar comportamientos de deslizamiento. Consulta Gestos y animación para obtener más información.

Desde el primer momento, Animatable admite Float y Color, pero cualquier tipo de datos puede usarse si se proporciona un TwoWayConverter. Consulta AnimationVector para obtener más información.

Puedes personalizar las especificaciones de la animación si proporcionas un AnimationSpec. Consulta AnimationSpec para obtener más información.

Animation: Animación controlada de forma manual

Animation es la API de Animation de nivel más bajo disponible. Muchas de las animaciones que vimos hasta ahora se basan en Animation. Hay dos subtipos de Animation: TargetBasedAnimation y DecayAnimation.

Solo se debe usar Animation para controlar manualmente la hora de la animación. Animation no tiene estado y no tiene ningún concepto de ciclo de vida. Funciona como motor de cálculo de animación que usan las API de nivel superior.

TargetBasedAnimation

Otras API abarcan la mayoría de los casos de uso, pero utilizar directamente TargetBasedAnimation te permite controlar por tu cuenta el tiempo de reproducción de la animación. En el siguiente ejemplo, el tiempo de reproducción del objeto TargetAnimation se controla de forma manual en función de la latencia de fotogramas que proporciona withFrameNanos.

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

A diferencia de TargetBasedAnimation, DecayAnimation no requiere que se proporcione un targetValue. En su lugar, calcula su targetValue en función de las condiciones de inicio, establecidas por initialVelocity y initialValue, y el elemento DecayAnimationSpec proporcionado.

Las animaciones de disminución suelen usarse después de un gesto de deslizamiento para ralentizar los elementos hasta que se detengan. La velocidad de animación comienza en el valor establecido por initialVelocityVector y se ralentiza con el tiempo.