Jetpack Compose proporciona APIs potentes y extensibles que facilitan la implementación de varias animaciones en la IU de tu app. En este documento, se describe cómo usar estas APIs y qué API utilizar según tu situación de animación.
Descripción general
Las animaciones son esenciales en una app para dispositivos móviles moderna con el fin de proporcionar una experiencia del usuario fluida y comprensible. Muchas APIs de Animation de Jetpack Compose están disponibles como funciones de componibilidad, al igual que diseños y otros elementos de IU, y están respaldadas por APIs de nivel inferior compiladas con funciones de suspensión de corrutinas de Kotlin. Esta guía comienza con las APIs generales que son útiles en muchas situaciones prácticas y continúa para explicar las APIs específicas que te otorgan más control y personalización.
El siguiente diagrama te ayuda a decidir qué API utilizar para implementar tu animación.
- Si quieres animar cambios de contenido en el diseño:
- Si quieres animar la aparición y la desaparición:
- Usa
AnimatedVisibility
.
- Usa
- Cambiar contenido según el estado:
- Si quieres encadenar contenido:
- Usa
Crossfade
.
- Usa
- De lo contrario, usa
AnimatedContent
.
- Si quieres encadenar contenido:
- De lo contrario, usa
Modifier.animateContentSize
.
- Si quieres animar la aparición y la desaparición:
- Si la animación se basa en el estado:
- Si la animación ocurre durante la composición:
- Si la animación es infinita:
- Si quieres animar múltiples valores simultáneamente:
- Usa
updateTransition
.
- Usa
- De lo contrario, usa
animate*AsState
.
- Si la animación ocurre durante la composición:
- Si quieres controlar de forma avanzada el tiempo de la animación, haz lo siguiente:
- Usa
Animation
, comoTargetBasedAnimation
oDecayAnimation
.
- Usa
- Si la animación es la única fuente de confianza
- Usa
Animatable
.
- Usa
- De lo contrario, usa
AnimationState
oanimate
.
API de Animation de alto nivel
Compose ofrece APIs de Animation de alto nivel para varios patrones de animación comunes que se utilizan en muchas apps. Estas APIs están diseñadas para alinearse con las prácticas recomendadas de Material Design Motion.
AnimatedVisibility
El elemento de componibilidad AnimatedVisibility
la aparición y la desaparición de su contenido.
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
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
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.
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.
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 { 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
.
animate*AsState
Las funciones animate*AsState
son las APIs de Animation más simples en Compose para crear un valor único. Solo debes proporcionar el valor final (o valor objetivo), 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).
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
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.
AnimatedContent (experimental)
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 { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(targetState = count) { 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() with
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() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { 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.colors.primary,
onClick = { expanded = !expanded }
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
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
}
}
}
}
) { targetExpanded ->
if (targetExpanded) {
Expanded()
} else {
ContentIcon()
}
}
}
Cómo animar la entrada o la salida de elementos secundarios
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.
animateContentSize
El modificador animateContentSize
anima un cambio de tamaño.
var message by remember { mutableStateOf("Hello") }
Box(
modifier = Modifier.background(Color.Blue).animateContentSize()
) {
Text(text = message)
}
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) { screen ->
when (screen) {
"A" -> Text("Page A")
"B" -> Text("Page B")
}
}
updateTransition
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)
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 { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
val borderWidth by transition.animateDp { 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)
}
}
) { state ->
when (state) {
BoxState.Collapsed -> MaterialTheme.colors.primary
BoxState.Expanded -> MaterialTheme.colors.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 = updateTransition(currentState)
// ...
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)
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
}
)
}
}
Cómo usar 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)
val borderColor by transition.animateColor { isSelected ->
if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { isSelected ->
if (isSelected) 10.dp else 2.dp
}
Surface(
onClick = { selected = !selected },
shape = RoundedCornerShape(8.dp),
border = BorderStroke(2.dp, borderColor),
elevation = 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 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 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)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 128.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
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()
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(Modifier.fillMaxSize().background(color))
APIs de Animation detalladas
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
.
Animatable
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
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 { mutableStateOf(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.
Cómo personalizar animaciones
Muchas de las API de Animation suelen aceptar parámetros para personalizar su comportamiento.
AnimationSpec
La mayoría de las API de Animation permiten a los desarrolladores personalizar las especificaciones de animaciones mediante un parámetro AnimationSpec
opcional.
val alpha: Float by animateFloatAsState(
targetValue = if (enabled) 1f else 0.5f,
// Configure the animation duration and easing.
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)
Hay diferentes tipos de AnimationSpec
para crear diferentes tipos de animaciones.
spring
spring
crea una animación basada en la física entre valores iniciales y finales. Toma 2 parámetros: dampingRatio
y stiffness
.
dampingRatio
define el nivel de efectividad que debería tener el resorte. El valor predeterminado es Spring.DampingRatioNoBouncy
.
stiffness
define la velocidad con la que debe moverse el resorte hacia el valor final. El valor predeterminado es Spring.StiffnessMedium
.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
spring
puede controlar las interrupciones de manera más fluida que los tipos AnimationSpec
basados en la duración, ya que garantiza la continuidad de la velocidad cuando cambia el valor objetivo entre las animaciones. spring
se usa como el valor predeterminado de AnimationSpec para muchas API de Animation, como animate*AsState
y updateTransition
.
tween
tween
anima entre los valores inicial y final sobre el durationMillis
especificado mediante una curva de aceleración. Consulta Aceleración para obtener más información. También puedes especificar delayMillis
para posponer el inicio de la animación.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
keyframes
keyframes
anima en función de los valores de instantánea especificados en diferentes marcas de tiempo en la duración de la animación. El valor de la animación se interpolará entre dos valores de fotogramas clave. Para cada uno de esos fotogramas clave, se puede especificar la aceleración a fin de determinar la curva de interpolación.
Es opcional especificar los valores en 0 ms y en el tiempo de duración. Si no especificas esos valores, se establecerán de manera predeterminada en los valores de inicio y finalización de la animación, respectivamente.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
repeatable
repeatable
ejecuta una animación basada en la duración (como tween
o keyframes
) varias veces hasta que alcanza el recuento de iteración especificado. Puedes pasar el parámetro repeatMode
para especificar si la animación se debe repetir comenzando desde el principio (RepeatMode.Restart
) o desde el final (RepeatMode.Reverse
).
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
infiniteRepeatable
infiniteRepeatable
es como repeatable
, pero se repite durante una cantidad infinita de iteraciones.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
En las pruebas que usan ComposeTestRule
, no se ejecutan las animaciones que usan infiniteRepeatable
. El componente se renderizará con el valor inicial de cada valor animado.
snap
snap
es un AnimationSpec
especial que cambia inmediatamente el valor al valor final. Puedes especificar delayMillis
para retrasar el inicio de la animación.
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
Easing
Las operaciones de AnimationSpec
basadas en la duración (como tween
o keyframes
) usan Easing
para ajustar la fracción de una animación. Eso permite que el valor de la animación se acelere y se ralentice, en lugar de moverse a una velocidad constante. La fracción es un valor entre 0 (inicio) y 1.0 (final) que indica el punto actual en la animación.
La aceleración es una función que toma un valor de fracción entre 0 y 1.0, y muestra un número de punto flotante. El valor que se muestra puede estar fuera de los límites para representar una suboscilación o una sobreoscilación. Se puede crear una aceleración personalizada como el siguiente código.
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}
Compose proporciona varias funciones Easing
integradas que abarcan la mayoría de los casos de uso.
Consulta Velocidad: Material Design para obtener más información sobre qué tipo de aceleración debes usar según tu caso.
FastOutSlowInEasing
LinearOutSlowInEasing
FastOutLinearEasing
LinearEasing
CubicBezierEasing
- Ver más
AnimationVector
La mayoría de las API de Animation de Compose admiten Float
, Color
, Dp
y otros tipos de datos básicos como valores de animación listos para usar, pero a veces necesitas animar otros tipos de datos, como los que personalizas. Durante la animación, cualquier valor de animación se representa como un AnimationVector
. El valor se convierte en un AnimationVector
y viceversa por un TwoWayConverter
correspondiente para que el sistema de animación principal pueda controlarlos de manera uniforme. Por ejemplo, un Int
se representa como un AnimationVector1D
que tiene un solo valor de número de punto flotante.
TwoWayConverter
de Int
tiene este aspecto:
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
Color
es, básicamente, un conjunto de 4 valores (rojo, verde, azul y alfa), por lo que Color
se convierte en un AnimationVector4D
que tiene 4 valores de número de punto flotante. De esta manera, cada tipo de datos que se usa en las animaciones se convierte en AnimationVector1D
, AnimationVector2D
, AnimationVector3D
o AnimationVector4D
, según su dimensionalidad. Esto permite que diferentes componentes del objeto se animen de forma independiente, cada uno con su propio seguimiento de velocidad. Se puede acceder a los convertidores integrados para tipos de datos básicos mediante Color.VectorConverter
, Dp.VectorConverter
, etcétera.
Si deseas agregar compatibilidad con un nuevo tipo de datos como un valor de animación, puedes crear tu propio TwoWayConverter
y proporcionarlo a la API. Por ejemplo, puedes usar animateValueAsState
para animar tu tipo de datos personalizados de la siguiente manera:
data class MySize(val width: Dp, val height: Dp)
@Composable
fun MyAnimation(targetSize: MySize) {
val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
targetSize,
TwoWayConverter(
convertToVector = { size: MySize ->
// Extract a float value from each of the `Dp` fields.
AnimationVector2D(size.width.value, size.height.value)
},
convertFromVector = { vector: AnimationVector2D ->
MySize(vector.v1.dp, vector.v2.dp)
}
)
)
}
Recursos vectoriales animados (experimental)
Para usar un recurso AnimatedVectorDrawable
, carga el archivo de elemento de diseño usando animatedVectorResource
y pasa un boolean
para alternar entre el estado inicial y final de tu elemento de diseño.
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
Para obtener más información sobre el formato de tu archivo de elementos de diseño, consulta Cómo animar gráficos de elementos de diseño.
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.
Gesto y animación (avanzado)
Hay varios aspectos que se deben tener en cuenta cuando trabajamos con animaciones y eventos táctiles, en comparación con los casos en que trabajamos solo con animaciones. En primer lugar, es posible que debamos interrumpir una animación en curso cuando comienzan los eventos táctiles, ya que la interacción del usuario debe tener la prioridad más alta.
En el siguiente ejemplo, usamos un Animatable
para representar la posición de desplazamiento de un componente circular. Los eventos táctiles se procesan con el modificador pointerInput
. Cuando detectamos un nuevo evento de toque, llamamos a animateTo
para animar el valor de desplazamiento a la posición del toque. Un evento de toque también puede ocurrir durante la animación y, en ese caso, animateTo
interrumpe la animación en curso y comienza la animación a la nueva posición objetivo, a la vez que se mantiene la velocidad de la animación interrumpida.
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
val position = awaitPointerEventScope {
awaitFirstDown().position
}
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
Otro patrón frecuente es tener que sincronizar valores de animación con valores provenientes de eventos táctiles, como los arrastres. En el siguiente ejemplo, vemos que "deslizar para descartar" se implementa como un Modifier
(en lugar de usar el SwipeToDismiss
componible). El desplazamiento horizontal del elemento se representa como un Animatable
. Esta API tiene una característica útil en la animación de gestos. Su valor puede cambiar mediante eventos táctiles y la animación. Cuando recibimos un evento de toque, detenemos el Animatable
mediante el método stop
para que se intercepte cualquier animación en curso.
Durante un evento de arrastre, usamos snapTo
para actualizar el valor Animatable
con el valor calculado de los eventos táctiles. Para la navegación, Compose proporciona VelocityTracker
a fin de registrar eventos de arrastre y calcular la velocidad. Se puede transmitir la velocidad directamente a animateDecay
para la animación de navegación. Cuando queremos deslizar el valor de desplazamiento de vuelta a la posición original, especificamos el valor de desplazamiento objetivo de 0f
con el método animateTo
.
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
Pruebas
Compose ofrece ComposeTestRule
, que te permite escribir pruebas para animaciones de manera determinista con control total sobre el reloj de prueba. Esto te permite verificar los valores de animación intermedios. Además, una prueba puede ejecutarse más rápido que la duración real de la animación.
ComposeTestRule
expone su reloj de prueba como mainClock
. Puedes configurar la propiedad autoAdvance
como "false" para controlar el reloj en tu código de prueba. Después de iniciar la animación que deseas probar, el reloj puede moverse con advanceTimeBy
.
Debes tener en cuenta que advanceTimeBy
no mueve el reloj exactamente según la duración especificada. En cambio, se redondea a la duración más cercana que sea multiplicador de la duración del fotograma.
@get:Rule
val rule = createComposeRule()
@Test
fun testAnimationWithClock() {
// Pause animations
rule.mainClock.autoAdvance = false
var enabled by mutableStateOf(false)
rule.setContent {
val color by animateColorAsState(
targetValue = if (enabled) Color.Red else Color.Green,
animationSpec = tween(durationMillis = 250)
)
Box(Modifier.size(64.dp).background(color))
}
// Initiate the animation.
enabled = true
// Let the animation proceed.
rule.mainClock.advanceTimeBy(50L)
// Compare the result with the image showing the expected result.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
Compatibilidad con herramientas
Android Studio admite la inspección de updateTransition
y animatedVisibility
en Animation Preview. Puedes hacer lo siguiente:
- Obtener una vista previa de una transición fotograma por fotograma
- Inspeccionar los valores en todas las animaciones de la transición
- Obtener una vista previa de la transición entre cualquier estado inicial y objetivo
- Inspeccionar y coordinar varias animaciones a la vez
Cuando inicies la Animation Preview, verás el panel "Animations" en el que podrás ejecutar cualquier transición incluida en la vista previa. La transición y cada uno de los valores de la animación están etiquetados con un nombre predeterminado. Puedes personalizar la etiqueta especificando el parámetro label
en las funciones updateTransition
y AnimatedVisibility
. Para obtener más información, consulta Animation Preview.
Más información
Si deseas obtener más información sobre las animaciones en Jetpack Compose, consulta los siguientes recursos adicionales:
Ejemplos
Entradas de blog
- Cómo personalizar AnimatedContent en Jetpack Compose
- Aceleración en las curvas de aceleración en Compose