1. Introducción
Última actualización: 21/11/2023
En este codelab, aprenderás a usar algunas de las APIs de Animation en Jetpack Compose.
Jetpack Compose es un kit de herramientas moderno diseñado para simplificar el desarrollo de IU. Si recién estás empezando a usar Jetpack Compose, hay varios codelabs que podrías probar antes de este.
- Aspectos básicos de Jetpack Compose
- Diseños en Jetpack Compose
- Cómo usar el estado en Jetpack Compose
Qué aprenderás
- Cómo usar varias APIs básicas de Animation
Requisitos previos
- Conocimientos básicos de Kotlin
- Conocimientos básicos de Compose, que incluyen lo siguiente:
- Diseño simple (columna, fila, cuadro, etc.)
- Elementos simples de la IU (botón, texto, etc.)
- Estados y recomposiciones
Requisitos
2. Cómo prepararte
Descarga el código del codelab. Puedes clonar el repositorio de la siguiente manera:
$ git clone https://github.com/android/codelab-android-compose.git
También tienes la opción de descargar el repositorio como archivo ZIP:
Importa el proyecto AnimationCodelab
en Android Studio.
El proyecto tiene varios módulos:
start
es el estado inicial del codelab.finished
es el estado final de la app después de completar este codelab.
Asegúrate de que el elemento start
esté seleccionado en el menú desplegable para la configuración de ejecución.
Comenzaremos a trabajar en varias situaciones de animación en el siguiente capítulo. Cada fragmento de código con el que trabajaremos en este codelab está marcado con un comentario // TODO
. Un buen truco es abrir la ventana de herramientas TODO en Android Studio y navegar a cada uno de los comentarios del capítulo.
3. Cómo animar un cambio simple de valor
Comencemos con una de las APIs de Animation más simples de Compose: las APIs de animate*AsState
. Debes usar esta API para animar los cambios de State
.
Ejecuta la configuración de start
e intenta cambiar de pestaña; para ello, haz clic en los botones "Home" y "Work" en la parte superior. No cambia el contenido de la pestaña, pero puedes ver que cambia el color de fondo.
Haz clic en TODO 1 en la ventana de herramientas de TODO para ver la implementación. Se encuentra en el elemento Home
componible.
val backgroundColor = if (tabPage == TabPage.Home) Seashell else GreenLight
Aquí, tabPage
es una TabPage
respaldada por un objeto State
. Según su valor, el color de fondo cambia entre durazno y verde. Queremos animar este cambio de valor.
Para animar un cambio de valor simple como este, podemos usar las APIs de animate*AsState
. Para crear un valor de animación, une el valor cambiante con la variante correspondiente de elementos de componibilidad animate*AsState
, en este caso, animateColorAsState
. El valor que se muestra es un objeto State<T>
, por lo que podemos usar una propiedad delegada local con una declaración by
para tratarla como una variable normal.
val backgroundColor by animateColorAsState(
targetValue = if (tabPage == TabPage.Home) Seashell else GreenLight,
label = "background color")
Vuelve a ejecutar la app y cambia de pestaña. Ahora el cambio de color es animado.
4. Cómo animar la visibilidad
Si te desplazas por el contenido de la app, notarás que el botón de acción flotante se expande y se contrae según la dirección del desplazamiento.
Busca TODO 2-1 y mira cómo funciona. Se encuentra en el elemento de componibilidad HomeFloatingActionButton
. El texto que dice "EDIT" se muestra o se oculta con una sentencia if
.
if (extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
Animar este cambio de visibilidad es tan simple como reemplazar if
por un elemento de componibilidad AnimatedVisibility
.
AnimatedVisibility(extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
Ejecuta la app y observa cómo el BAF se expande y se contrae ahora.
AnimatedVisibility
ejecuta su animación cada vez que cambia el valor especificado de Boolean
. De forma predeterminada, AnimatedVisibility
muestra el elemento con un fundido de entrada y una expansión, y lo oculta con un fundido de salida y una contracción. Este comportamiento funciona muy bien para este ejemplo con un BAF, pero también podemos personalizarlo.
Si haces clic en el BAF, deberías ver un mensaje que diga "Edit feature is not supported" (No se admite la función de edición). También usa AnimatedVisibility
para animar su aparición y desaparición. A continuación, personalizarás este comportamiento para que el mensaje se deslice desde la parte superior y hacia afuera de ella.
Busca TODO 2-2 y revisa el código en el elemento de componibilidad EditMessage
.
AnimatedVisibility(
visible = shown
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Para personalizar la animación, agrega los parámetros enter
y exit
al elemento de componibilidad AnimatedVisibility
.
El parámetro enter
debe ser una instancia de EnterTransition
. En este ejemplo, podemos usar la función slideInVertically
a fin de crear un EnterTransition
y un slideOutVertically
para la transición de salida. Cambia el código de la siguiente manera:
AnimatedVisibility(
visible = shown,
enter = slideInVertically(),
exit = slideOutVertically()
)
Vuelve a ejecutar la app. Al hacer clic en el botón Editar, notarás que la animación se ve mejor, pero no es la correcta, ya que el comportamiento predeterminado de slideInVertically
y slideOutVertically
usa la mitad del alto del elemento.
Para la transición de entrada, podemos ajustar el comportamiento predeterminado a fin de usar toda la altura del elemento y animarlo correctamente mediante la configuración del parámetro initialOffsetY
. initialOffsetY
debe ser una lambda que muestre la posición inicial.
La lambda recibe un argumento: la altura del elemento. Para garantizar que el elemento se deslice desde la parte superior de la pantalla, se muestra su valor negativo, ya que en la parte superior de la pantalla se muestra el valor 0. Queremos que la animación comience de -height
a 0
(su posición final en reposo) para que comience desde arriba y tenga una animación.
Cuando se usa slideInVertically
, el desplazamiento objetivo después de la diapositiva siempre es 0
(píxel). initialOffsetY
se puede especificar como un valor absoluto o un porcentaje de la altura completa del elemento a través de una función lambda.
De manera similar, slideOutVertically
supone que el desplazamiento inicial es 0, por lo que solo se debe especificar targetOffsetY
.
AnimatedVisibility(
visible = shown,
enter = slideInVertically(
// Enters by sliding down from offset -fullHeight to 0.
initialOffsetY = { fullHeight -> -fullHeight }
),
exit = slideOutVertically(
// Exits by sliding up from offset 0 to -fullHeight.
targetOffsetY = { fullHeight -> -fullHeight }
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Si vuelves a ejecutar la app, podemos ver que la animación se ajusta más a lo que esperábamos:
Podemos personalizar más nuestra animación con el parámetro animationSpec
. animationSpec
es un parámetro común para muchas APIs de Animation, incluidas EnterTransition
y ExitTransition
. Podemos pasar uno de varios tipos de AnimationSpec
para especificar cómo debe cambiar con el tiempo el valor de la animación. En este ejemplo, usaremos un AnimationSpec
simple basado en la duración. Se puede crear con la función tween
. La duración es de 150 ms y la velocidad es de LinearOutSlowInEasing
. Para la animación de salida, usemos la misma función tween
con el parámetro animationSpec
, pero con una duración de 250 ms y una aceleración de FastOutLinearInEasing
.
El código resultante debería verse de la siguiente manera:
AnimatedVisibility(
visible = shown,
enter = slideInVertically(
// Enters by sliding down from offset -fullHeight to 0.
initialOffsetY = { fullHeight -> -fullHeight },
animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
),
exit = slideOutVertically(
// Exits by sliding up from offset 0 to -fullHeight.
targetOffsetY = { fullHeight -> -fullHeight },
animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Ejecuta la app y vuelve a hacer clic en el BAF. Puedes ver que el mensaje ahora se desliza hacia adentro y afuera desde la parte superior con diferentes funciones de aceleración y duraciones:
5. Cómo animar el cambio de tamaño del contenido
La app muestra varios temas en el contenido. Haz clic en uno de ellos para que se abra y muestre el texto del cuerpo correspondiente a ese tema. La tarjeta que contiene el texto se expande y se contrae cuando se muestra u oculta el cuerpo.
Consulta el código de TODO 3 en el elemento de componibilidad TopicRow
.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// ... the title and the body
}
Aquí, el elemento de componibilidad Column
cambia de tamaño a medida que cambia su contenido. Podemos animar el cambio de su tamaño agregando el modificador animateContentSize
.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.animateContentSize()
) {
// ... the title and the body
}
Ejecuta la app y haz clic en uno de los temas. Puedes ver que se expande y se contrae con una animación.
animateContentSize
también se puede personalizar con una animationSpec
de tu elección. Podemos proporcionar opciones para cambiar el tipo de animación de resorte a interpolación, etc. Consulta la documentación de Personalización de animaciones para obtener más información.
6. Cómo animar varios valores
Ahora que conocemos algunas APIs básicas de Animation, veamos la API de Transition
, que nos permite crear animaciones más complejas. El uso de la API de Transition
nos permite realizar un seguimiento de cuándo finalizan todas las animaciones en un Transition
, lo cual no es posible cuando se usan las APIs individuales de animate*AsState
que vimos anteriormente. La API de Transition
también nos permite definir diferentes transitionSpec
cuando se realiza la transición entre diferentes estados. Veamos cómo podemos usarla:
Para este ejemplo, personalizaremos el indicador de pestañas. Es un rectángulo que se muestra en la pestaña seleccionada.
Busca TODO 4 en el elemento de componibilidad HomeTabIndicator
y observa cómo se implementa el indicador de la pestaña.
val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) PaleDogwood else Green
Aquí, indicatorLeft
es la posición horizontal del borde izquierdo del indicador en la fila de pestañas. indicatorRight
es la posición horizontal del borde derecho del indicador. El color también cambia entre durazno y verde.
Para animar estos valores de forma simultánea, podemos usar una Transition
. Puedes crear un Transition
con la función updateTransition
. Pasa el índice de la pestaña seleccionada actualmente como el parámetro targetState
.
Cada valor de animación puede declararse con las funciones de extensión animate*
de Transition
. En este ejemplo, usamos animateDp
y animateColor
. Estas toman un bloque lambda y podemos especificar el valor objetivo para cada uno de los estados. Ya sabemos cuáles deberían ser sus valores objetivo, por lo que podemos unirlos como se muestra a continuación. Ten en cuenta que podemos usar una declaración by
y convertirla en una propiedad delegada local aquí nuevamente porque las funciones animate*
muestran un objeto State
.
val transition = updateTransition(tabPage, label = "Tab indicator")
val indicatorLeft by transition.animateDp(label = "Indicator left") { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(label = "Indicator right") { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor(label = "Border color") { page ->
if (page == TabPage.Home) PaleDogwood else Green
}
Ejecuta la app y verás que el cambio de pestaña ahora es mucho más interesante. Cuando haces clic en la pestaña, se cambia el valor del estado tabPage
, todos los valores de animación asociados con transition
comienzan a animarse al valor especificado para el estado objetivo.
Además, podemos especificar el parámetro transitionSpec
para personalizar el comportamiento de la animación. Por ejemplo, podemos lograr un efecto elástico para el indicador si el borde más cercano al objetivo se mueve más rápido que el otro borde. Podemos usar la función infija isTransitioningTo
en lambdas transitionSpec
para determinar la dirección del cambio de estado.
val transition = updateTransition(
tabPage,
label = "Tab indicator"
)
val indicatorLeft by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
// Indicator moves to the right.
// The left edge moves slower than the right edge.
spring(stiffness = Spring.StiffnessVeryLow)
} else {
// Indicator moves to the left.
// The left edge moves faster than the right edge.
spring(stiffness = Spring.StiffnessMedium)
}
},
label = "Indicator left"
) { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
// Indicator moves to the right
// The right edge moves faster than the left edge.
spring(stiffness = Spring.StiffnessMedium)
} else {
// Indicator moves to the left.
// The right edge moves slower than the left edge.
spring(stiffness = Spring.StiffnessVeryLow)
}
},
label = "Indicator right"
) { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor(
label = "Border color"
) { page ->
if (page == TabPage.Home) PaleDogwood else Green
}
Vuelve a ejecutar la app y prueba cambiar de pestaña.
Android Studio admite la inspección de transiciones en la vista previa de Compose. Para usar la Animation Preview, inicia el modo interactivo haciendo clic en el ícono "Start Animation Preview" en la esquina superior derecha de un elemento componible en la vista previa (ícono de ). Prueba hacer clic en el ícono del elemento PreviewHomeTabBar
componible. Se abrirá un nuevo panel "Animations".
Para ejecutar la animación, haz clic en el ícono del botón "Play". También puedes arrastrar la barra de búsqueda para ver cada uno de los fotogramas de animación. Para obtener una mejor descripción de los valores de animación, puedes especificar el parámetro label
en updateTransition
y los métodos animate*
.
7. Cómo repetir animaciones
Haz clic en el ícono del botón de actualización junto a la temperatura actual. La app comenzará a cargar la información meteorológica más reciente (esta es una simulación). Hasta que se complete la carga, verás un indicador de carga, que es un círculo gris y una barra. Animemos el valor alfa de este indicador para aclarar que el proceso está en curso.
Busca TODO 5 en el elemento de componibilidad LoadingRow
.
val alpha = 1f
Deseamos animar este valor entre 0f y 1f varias veces. Para ello, podemos usar InfiniteTransition
. Esta API es similar a la API de Transition
de la sección anterior. Ambas animan varios valores, pero mientras que Transition
los anima en función de los cambios de estado, InfiniteTransition
los anima indefinidamente.
Para crear un InfiniteTransition
, usa la función rememberInfiniteTransition
. Luego, cada cambio de valor de la animación se puede declarar con una de las funciones de extensión animate*
de InfiniteTransition
. En este caso, animaremos un valor alfa, así que usaremos animatedFloat
. El parámetro initialValue
debe ser 0f
, y el targetValue
, 1f
. También podemos especificar un AnimationSpec
para esta animación, pero esta API solo toma un InfiniteRepeatableSpec
. Usa la función infiniteRepeatable
para crear una. Este AnimationSpec
une cualquier AnimationSpec
basado en la duración y lo hace repetible. Por ejemplo, el código resultante debería verse como se muestra a continuación.
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
0.7f at 500
},
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
El repeatMode
predeterminado es RepeatMode.Restart
. Esta transición va de initialValue
a targetValue
y vuelve a comenzar en el initialValue
. Si estableces repeatMode
en RepeatMode.Reverse
, la animación pasará de initialValue
a targetValue
y, luego, de targetValue
a initialValue
. La animación avanzará de 0 a 1 y, luego, de 1 a 0.
La animación keyFrames
es otro tipo de animationSpec
(algunos son tween
y spring
) que permite cambios en el valor en curso a diferentes milisegundos. Inicialmente, configuramos durationMillis
en 1,000 ms. Luego, podemos definir fotogramas clave en la animación. Por ejemplo, a los 500 ms, nos gustaría que el valor alfa sea 0,7 f. Esto cambiará la progresión de la animación: avanzará rápidamente de 0 a 0.7 en los primeros 500 ms de la animación, y de 0.7 a 1.0 entre los 500 ms y 1,000 ms de la animación, ralentizando el proceso hacia el final.
Si queremos más de un fotograma clave, podemos definir varios keyFrames
de la siguiente manera:
animation = keyframes {
durationMillis = 1000
0.7f at 500
0.9f at 800
}
Ejecuta la app y haz clic en el botón para actualizar. Ahora puedes ver la animación del indicador de carga.
8. Animación basada en gestos
En esta sección final, aprenderemos a ejecutar animaciones basadas en entradas táctiles. Compilaremos un modificador swipeToDismiss
desde cero.
Busca TODO 6-1 en el modificador swipeToDismiss
. Aquí, intentamos crear un modificador que haga que el elemento sea deslizable con el tacto. Cuando se coloca el elemento en el borde de la pantalla, llamamos a la devolución de llamada onDismissed
para quitarlo.
Para compilar un modificador swipeToDismiss
, debemos comprender algunos conceptos clave. En primer lugar, un usuario coloca el dedo en la pantalla y genera un evento táctil con una coordenada x y una y. Luego, mueve el dedo hacia la derecha o la izquierda y traslada x e y en función de su movimiento. El elemento que está tocando debe moverse con el dedo, por lo que actualizaremos la posición del elemento según la velocidad y la posición del evento táctil.
Podemos usar varios de los conceptos descritos en la documentación de gestos de Compose. Con el modificador pointerInput
, podemos obtener acceso detallado a los eventos táctiles del puntero entrante y realizar un seguimiento de la velocidad en la que el usuario arrastra con el mismo puntero. Si el usuario levanta el dedo antes de que el elemento haya pasado el límite para descartarlo, el elemento volverá a su posición.
Hay varias cosas únicas que debes tener en cuenta en esta situación. En primer lugar, un evento táctil puede interceptar cualquier animación en curso. En segundo lugar, es posible que el valor de la animación no sea la única fuente de información. En otras palabras, es posible que debamos sincronizar el valor de la animación con los valores provenientes de eventos táctiles.
Animatable
es la API más detallada que vimos hasta el momento. Tiene varias funciones que son útiles en situaciones de gestos, como la capacidad de ajustar instantáneamente al nuevo valor que proviene de un gesto y detener cualquier animación en curso cuando se activa un nuevo evento táctil. Crearemos una instancia de Animatable
y la usaremos para representar el desplazamiento horizontal del elemento deslizable. Asegúrate de importar Animatable
de androidx.compose.animation.core.Animatable
y no de androidx.compose.animation.Animatable
.
val offsetX = remember { Animatable(0f) } // Add this line
// used to receive user touch events
pointerInput {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// ...
TODO 6-2 es donde acabamos de recibir un evento táctil. Debemos interceptar la animación si se está ejecutando actualmente. Para ello, se debe llamar a stop
en el Animatable
. Ten en cuenta que la llamada se ignora si la animación no se está ejecutando. Se usará VelocityTracker
para calcular qué tan rápido se mueve un usuario de izquierda a derecha. awaitPointerEventScope
es una función de suspensión que puede esperar eventos de entrada del usuario y responder a ellos.
// Wait for a touch down event. Track the pointerId based on the touch
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // Add this line to cancel any on-going animations
// Prepare for drag events and record velocity of a fling gesture
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
En TODO 6-3, recibimos continuamente eventos de arrastre. Debemos sincronizar la posición del evento táctil en el valor de la animación. Para ello, podemos usar snapTo
en Animatable
. Se debe llamar a snapTo
dentro de otro bloque launch
, ya que awaitPointerEventScope
y horizontalDrag
son permisos de corrutinas restringidos. Esto significa que solo pueden usar suspend
para awaitPointerEvents
; snapTo
no es un evento de puntero.
horizontalDrag(pointerId) { change ->
// Add these 4 lines
// Get the drag amount change to offset the item with
val horizontalDragOffset = offsetX.value + change.positionChange().x
// Need to call this in a launch block in order to run it separately outside of the awaitPointerEventScope
launch {
// Instantly set the Animable to the dragOffset to ensure its moving
// as the user's finger moves
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
if (change.positionChange() != Offset.Zero) change.consume()
}
En TODO 6-4, el elemento se acaba de lanzar y deslizar. Debemos calcular la posición final en la que se establece el deslizamiento para decidir si debemos desplazar el elemento de nuevo a la posición original, o bien alejarlo y, luego, invocar la devolución de llamada. Usaremos el objeto decay
creado anteriormente para calcular targetOffsetX
:
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Add this line to calculate where it would end up with
// the current velocity and position
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
En las TODO 6-5, estamos a punto de comenzar la animación. Pero antes, queremos establecer límites de valores superiores e inferiores para el Animatable
de modo que este se detenga en cuanto alcance los límites (-size.width
y size.width
, ya que no queremos que el offsetX
pueda extenderse más allá de estos dos valores). El modificador pointerInput
nos permite acceder al tamaño del elemento mediante la propiedad size
, así que usemos eso para obtener nuestros límites.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
En TODO 6-6, podemos comenzar la animación. Primero, compararemos la posición de desplazamiento definida que calculamos anteriormente y el tamaño del elemento. Si la posición de desplazamiento es inferior al tamaño, significa que la velocidad del deslizamiento no fue suficiente. Podemos usar animateTo
para volver a animar el valor a 0 f. De lo contrario, usaremos animateDecay
para iniciar la animación de desplazamiento. Cuando finalice la animación (probablemente, cerca de los límites que establecimos antes), podremos llamar a la devolución de llamada.
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
Por último, consulta TODO 6-7. Configuramos todas las animaciones y los gestos, así que no olvides aplicar el desplazamiento al elemento, esto moverá el elemento en pantalla al valor producido por nuestro gesto o animación:
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
Como resultado de esta sección, obtendrás un código como el siguiente:
private fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
// This Animatable stores the horizontal offset for the element.
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// Wait for a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
// Interrupt any ongoing animation.
offsetX.stop()
// Prepare for drag events and record velocity of a fling.
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Record the position after offset
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
// Overwrite the Animatable value while the element is dragged.
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
change.consumePositionChange()
}
}
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Calculate where the element eventually settles after the fling animation.
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
// The animation should end as soon as it reaches these bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back to the default position.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
}
}
}
// Apply the horizontal offset to the element.
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
Ejecuta la app e intenta deslizar uno de los elementos de la tarea. Puedes ver que el elemento regresa a la posición predeterminada o se desliza y desaparece según la velocidad del deslizamiento. También puedes capturar el elemento durante la animación.
9. ¡Felicitaciones!
¡Felicitaciones! Aprendiste las APIs básicas de Animation de Compose.
En este codelab, aprendimos a usar lo siguiente:
APIs de Animation generales:
animatedContentSize
AnimatedVisibility
APIs de Animation detalladas:
animate*AsState
para animar un solo valorupdateTransition
para animar varios valoresinfiniteTransition
para animar valores indefinidamenteAnimatable
para compilar animaciones personalizadas con gestos táctiles
¿Qué sigue?
Consulta los otros codelabs sobre la ruta de aprendizaje de Compose.
Para obtener más información, consulta Animaciones de Compose y estos documentos de referencia: