En esta página, se describe cómo crear animaciones basadas en valores en Jetpack Compose, con un enfoque en las APIs que animan valores según sus estados actuales y de destino.
Anima un solo valor con animate*AsState
Las funciones animate*AsState
son APIs de animación sencillas 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 valor especificado.
En el siguiente ejemplo, se anima el alfa con esta API. Si unes 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 animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") Box( Modifier .fillMaxSize() .graphicsLayer { alpha = animatedAlpha } .background(Color.Red) )
No necesitas crear una instancia de ninguna clase de animación ni controlar 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.
De forma predeterminada, Compose proporciona 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 de forma simultánea 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 verificar 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
. Puedes usar esto como indicador para comprobar si finalizó la transición.
En ocasiones, es posible que desees tener un estado inicial diferente del primer estado objetivo. Puedes usar updateTransition
con MutableTransitionState
para lograrlo. Por ejemplo, te 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 principal conoce 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
. El targetState
para Transition.AnimatedVisibility
y Transition.AnimatedContent
se deriva de Transition
y activa las animaciones de entrada, salida y sizeTransform
según sea necesario cuando cambia el targetState
de Transition
. Estas funciones de extensión te permiten elevar todas las animaciones de entrada, salida y sizeTransform
que, de lo contrario, serían internas para AnimatedVisibility
o AnimatedContent
en Transition
. Con estas funciones de extensión, puedes observar el cambio de estado de AnimatedVisibility
o AnimatedContent
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 información.
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") } } } }
Cómo encapsular una transición y volver a usarla
Para casos de uso sencillos, definir las animaciones de transición en el mismo elemento componible que tu IU es una opción 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. Puedes extraer la implementación de la transición en la nueva función separada. Este patrón es útil cuando necesitas centralizar la lógica de animación o hacer que 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) } }
Cómo crear una animación que se repite 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
y 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 Animation detalladas
Todas las APIs de Animation de alto nivel mencionadas en la sección anterior se compilan sobre las APIs de Animation de bajo nivel.
Las funciones animate*AsState
son APIs sencillas que renderizan un cambio de valor instantáneo como un valor de animación. Esta funcionalidad cuenta con el respaldo de Animatable
, una API basada en corrutinas para animar un solo valor.
updateTransition
crea un objeto de transición que puede administrar múltiples valores de animación y ejecutarlos cuando cambia un estado. rememberInfiniteTransition
es similar, pero crea una transición infinita que puede administrar varias animaciones que continúan indefinidamente. Todas estas APIs son componibles, excepto Animatable
, lo que significa que puedes crear estas animaciones fuera de la composición.
Todas estas APIs se basan en la API de Animation
más fundamental. Si bien la mayoría de las apps no interactuarán directamente con Animation
, puedes acceder a algunas de sus capacidades de personalización a través de APIs de nivel superior. Consulta Cómo personalizar animaciones para obtener más información sobre AnimationVector
y AnimationSpec
.
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 con 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 Compose cancela cualquier animación en curso.
Muchas funciones de Animatable
, como animateTo
, son funciones de suspensión.
Esto significa que debes unirlos 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, creas y recuerdas 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 una animación en el otro color.
Si hay una animación en curso cuando cambia el valor, Compose cancela la animación, y la animación nueva comienza desde el valor de instantánea actual con la velocidad actual.
Esta API de Animatable
es la implementación subyacente de animate*AsState
mencionada en la sección anterior. El uso directo de Animatable
ofrece un control más preciso de varias maneras:
- 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, específicamentesnapTo
yanimateDecay
.snapTo
establece el valor actual en el valor objetivo de inmediato. Esto es útil cuando la animación 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.
De forma predeterminada, Animatable
admite Float
y Color
, pero puedes usar cualquier tipo de datos si proporcionas 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 manualmente
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
.
Usa Animation
solo para controlar manualmente el tiempo 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 para las APIs de nivel superior.
TargetBasedAnimation
Otras APIs abarcan la mayoría de los casos de uso, pero utilizar directamente TargetBasedAnimation
te permite controlar el tiempo de reproducción de la animación. En el siguiente ejemplo, controlas manualmente el tiempo de reproducción de TargetAnimation
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 que establece initialVelocityVector
y se ralentiza con el tiempo.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo personalizar animaciones
- Animaciones en Compose
- Modificadores de animación y elementos componibles