Animación simple con Jetpack Compose

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

1. Antes de comenzar

En este codelab, aprenderás a agregar una animación simple a tu app para Android. Las animaciones pueden hacer que tu app sea más interactiva, interesante y fácil de interpretar para los usuarios. Si animas las actualizaciones individuales en una pantalla llena de información, el usuario podrá ver los cambios.

Hay muchos tipos de animaciones que se pueden usar en una interfaz de usuario de la aplicación. Los elementos pueden desvanecerse a medida que aparecen o desaparecen, pueden moverse dentro o fuera de la pantalla, o pueden transformarse de formas interesantes. Esto permite que la IU de la app sea expresiva y fácil de usar.

Las animaciones también pueden agregar un estilo refinado a tu app, lo que le da un aspecto elegante, y también ayuda al usuario:

Las animaciones que recompensan al usuario por una tarea pueden hacer que los momentos clave del recorrido del usuario sean más significativos.

Los elementos animados que responden a la entrada del teclado proporcionan comentarios que muestran si la acción se realizó correctamente.

Los elementos de lista animados son marcadores de posición que indican que se está cargando el contenido.

Una acción animada de deslizar para abrir invita y fomenta el gesto necesario.

Los íconos animados pueden complementar o mejorar el significado del ícono.

Requisitos previos

  • Conocimientos sobre Kotlin, incluidas funciones, lambdas y elementos sin estado que admiten composición
  • Conocimientos básicos de compilación de diseños en Jetpack Compose
  • Conocimientos básicos sobre cómo crear listas en Jetpack Compose
  • Conocimientos básicos de Material Design

Qué aprenderás

  • Cómo compilar una animación de resorte simple con Jetpack Compose

Qué compilarás

  • Compilarás la app de Woof a partir del codelab sobre temas de Material con Jetpack Compose y agregarás una animación simple para reconocer la acción del usuario.

Requisitos

  • La versión más reciente de Android Studio
  • Conexión a Internet a fin de descargar el código de inicio

2. Mira el video con instrucciones para compilar (opcional)

Si quieres ver cómo uno de los instructores del curso completa el codelab, reproduce el siguiente video.

Se recomienda expandir el video a pantalla completa (con el ícono Este símbolo muestra 4 esquinas en un cuadrado destacado para indicar el modo de pantalla completa. en la esquina inferior derecha del video) para que puedas ver Android Studio y el código con mayor claridad.

Este paso es opcional. También puedes omitir el video y comenzar con las instrucciones del codelab de inmediato.

3. Descripción general de la app

En el codelab sobre temas de Material con Jetpack Compose, creaste una app de Woof con Material Design, que muestra una lista de perros e información acerca de ellos.

7252aa244a54ad90.png

En este codelab, agregarás animación a la app de Woof. Agregarás información de un pasatiempo, que se mostrará cuando expandas el elemento de la lista. También, agregarás una animación de resorte para animar el elemento de la lista que se expande:

1e9cf1dbc490924a.gif

Obtén el código de inicio

Para comenzar, descarga el código de inicio:

Descargar ZIP

Como alternativa, puedes clonar el repositorio de GitHub para el código:

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

Puedes explorar el código en el repositorio de GitHub de Woof app.

4. Ícono de agregar más

El primer paso para compilar una animación de resorte es agregar un ícono de expandir más f88173321938c003.png. El ícono para expandir más proporciona un botón para que el usuario expanda el elemento de la lista.

9fbd3fb0daf35fd3.png

Íconos

Los íconos son símbolos que ayudan a los usuarios a comprender la interfaz de usuario comunicando de forma visual la función prevista. Suelen inspirarse en los objetos del mundo físico que se espera que haya experimentado un usuario. Por lo general, el diseño del ícono reduce el nivel de detalle al mínimo necesario para que le resulte familiar al usuario. Por ejemplo, un lápiz en el mundo físico se usa para escribir, de manera que el equivalente de ícono generalmente indica crear o editar.

Foto de Angelina Litvin en Unsplash

Ícono de lápiz en blanco y negro

Material Design proporciona una serie de íconos organizados en categorías comunes para la mayoría de tus necesidades.

bfdb896506790c69.png

Cómo agregar una dependencia de Gradle

Agrega la dependencia de biblioteca material-icons-extended a tu proyecto. Usarás los íconos Icons.Filled.ExpandLess 30c384f00846e69b.png y Icons.Filled.ExpandMore f88173321938c003.png de esta biblioteca.

  1. En el panel del proyecto, abre Gradle Scripts > build.gradle (Module: Woof.app).

f7fe58e936bbad3e.png

  1. Desplázate hasta el final de la función build.gradle (Module: Woof.app). En el bloque dependencies{}, agrega la siguiente línea:
implementation "androidx.compose.material:material-icons-extended:$compose_version"

Agrega el ícono que admite composición

Agrega una función para mostrar el ícono de expandir más de la biblioteca de íconos de Material y usarla como botón.

  1. En MainActivity.kt, después de la función DogItem(), crea una nueva función de componibilidad llamada DogItemButton().
  2. Pasa un Boolean para el estado expandido, una expresión lambda para el evento de clic del botón y un Modifier opcional de la siguiente manera:
@Composable
private fun DogItemButton(
    expanded: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {

}
  1. Dentro de la función DogItemButton(), agrega un elemento IconButton() que admita composición y acepte un parámetro llamado onClick con una expresión lambda que use la sintaxis lambda al final, que se invoque cuando se presione este ícono y establécelo en el argumento onClick que se pasó.
@Composable
private fun DogItemButton(
   // ...
) {
   IconButton(onClick = onClick) {

   }
}
  1. Dentro del bloque de lambda IconButton(), agrega un elemento Icon que admita composición con un parámetro denominado imageVector y establécelo en Icons.Filled.ExpandMore. Este es el botón de ícono f88173321938c003.png que se mostrará al final del elemento de la lista. Android Studio te muestra una advertencia para los parámetros que admiten composición Icon() que corregirás en pasos posteriores.
  2. Agrega el parámetro tint con nombre y establece el color del ícono en MaterialTheme.colors.secondary. Agrega el parámetro con nombre contentDescription y establécelo en el recurso de strings R.string.expand_button_content_description.
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore

IconButton(onClick = onClick) {
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       tint = MaterialTheme.colors.secondary,
       contentDescription = stringResource(R.string.expand_button_content_description)
   )
}

Mostrar el ícono

Para mostrar el elemento componible DogItemButton(), agrégalo al diseño.

  1. Al comienzo de la función DogItem() que admite composición, agrega un var para guardar el estado expandido del elemento de lista. Establece el valor inicial en false.
var expanded by remember { mutableStateOf(false) }
  1. Al final del bloque Row de la función que admite composición DogItem(), llama a la función DogItemButton() y, luego, pasa un estado expandido y una lambda vacía para la devolución de llamada. Este código muestra el botón de ícono en el elemento de la lista.
  2. Para mostrar el botón de ícono dentro del elemento de lista, en la función que admite composición DogItem() al final del bloque Row, después de la llamada a DogInformation(), realiza una llamada a DogItemButton(). Pasa el estado expanded y una lambda vacía para la devolución de llamada. Definirás esta función lambda en un paso posterior.
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. Build & Refresh la vista previa en el panel Design

a49643f08701a8d.png

Observa que el botón para expandir más no está alineado al final del elemento de la lista. Deberás corregir eso en el siguiente paso.

Alinear el botón para expandir más

Para alinear el botón para expandir más al final del elemento de la lista, debes agregar un espaciador en el diseño con el atributo Modifier.weight().

En la app de Woof, cada fila de un elemento de la lista incluye una imagen de perro, información sobre él y un botón para expandir. Agregarás un elemento que admite composición espaciador antes del botón para expandir más con el peso 1f a fin de alinear correctamente el ícono del botón. Como el espaciador es el único elemento secundario ponderado de la fila, completará el espacio restante en la fila después de medir la longitud del otro elemento secundario no ponderado.

6c2b523849f0f626.png

Agrega el espaciador a la fila del elemento de la lista

  1. Al final del bloque Row en la función que admite composición DogItem(), agrega un Spacer. Pasa un Modifier con weight(1f). El elemento Modifier.weight() hace que el espaciador llene el espacio restante en la fila.
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(Modifier.weight(1f))
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. Build & Refresh la vista previa en el panel Design Observa que el botón para expandir más ahora está alineado al final del elemento de la lista.

f6a140413de9ad54.png

5. Agrega un elemento componible para mostrar un pasatiempo

En esta tarea, agregarás elementos Text que admiten composición a fin de mostrar información sobre el pasatiempo del perro.

66ea5cc5c7253d55.png

  1. Crea una nueva función que admita composición llamada DogHobby(), que reciba el ID de recurso de la string de un pasatiempo y un Modifier opcional.
  2. Dentro de la función DogHobby(), crea una columna con los siguientes atributos de padding para agregar espacio entre la columna y los elementos secundarios que admiten composición.
import androidx.annotation.StringRes

@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
            top = 8.dp,
            bottom = 16.dp,
            end = 16.dp
       )
   ) { }
}
  1. Dentro del bloque de columnas, agrega dos elementos que admiten composición Text: uno para mostrar el texto Acerca de sobre la afición y otro para mostrar la información del pasatiempo.

3051387c4b9c7455.png

  1. En el texto Acerca de, establece el estilo en h3 (Encabezado 3) y el color en onBackground. Para obtener información sobre un pasatiempo, establece el estilo en body1.
Column(
   modifier = modifier.padding(
       //..
   )
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.h3,
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.body1,
   )
}
  1. Así se ve la función DogHobby() que admite composición y que se completó.
@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
           top = 8.dp,
           bottom = 16.dp,
           end = 16.dp
       )
   ) {
       Text(
           text = stringResource(R.string.about),
           style = MaterialTheme.typography.h3
       )
       Text(
           text = stringResource(dogHobby),
           style = MaterialTheme.typography.body1
       )
   }
}
  1. Para mostrar el elemento componible DogHobby(), en DogItem(), une el elemento Row con un elemento Column. Realiza una llamada a la función DogHobby() y pasa el dog.hobbies como parámetro, después del Row como el segundo elemento secundario.
Column() {
   Row(
       //..
   ) {
       //..
   }
   DogHobby(dog.hobbies)
}

La función DogItem() completa debería verse de la siguiente manera:

@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
        elevation = 4.dp,
       modifier = modifier.padding(8.dp)
   ) {
       Column() {
           Row(
               modifier = Modifier
                   .fillMaxWidth()
                   .padding(8.dp)
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { expanded = !expanded },
               )
           }
           DogHobby(dog.hobbies)
       }
   }
}
  1. Build & Refresh la vista previa en el panel Design Observa el pasatiempo del perro que se muestra.

9e2e68a4bc4a8ae1.png

6. Muestra u oculta el pasatiempo con un clic

Tu app tiene un botón para expandir más elementos para cada elemento de la lista, pero aún no funciona. En esta sección, agregarás la opción de ocultar o revelar la información del pasatiempo cuando el usuario hace clic en el botón para expandir más.

  1. En elDogItem() función que admite composición, en laDogItemButton() llamada de función, define laonClick() expresión lambda, cambia elexpanded el valor del estado booleano atrue al hacer clic en el botón y volver a cambiarlofalse si se vuelve a hacer clic en el botón.
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. En la función DogItemButton(), une la llamada a función DogHobby() con una verificación if en el valor booleano expanded.
// No need to copy over
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       //..
   ) {
       Column() {
           Row(
               //..
           ) {
               //..
           }
           if (expanded) {
               DogHobby(dog.hobbies)
           }
       }
   }
}

En el código anterior, la información del pasatiempo del perro solo se muestra si el valor de expanded es true.

  1. La vista previa muestra cómo se ve la IU, y también puedes interactuar con ella. Para interactuar con la vista previa de la IU, haz clic en el botón Interactive Mode 42379dbe94a7a497.png en la esquina superior derecha del panel Design. De esta manera, se inicia la vista previa en el modo interactivo.

2a4ad1f3d2d0bff7.png

  1. Haz clic en el botón para expandir más a fin de interactuar con la vista previa. Ten en cuenta que la información de pasatiempos del perro está oculta y se revela cuando haces clic en el botón para expandir más.

6ee6774b5b14c7e1.gif

Observa que el ícono de botón para expandir más permanece igual cuando se expande el elemento de la lista. Para una mejor experiencia del usuario, cambiarás el ícono para que ExpandMore muestre la flecha hacia abajo c761ef298c2aea5a.png y ExpandLess para mostrar la flecha hacia arriba b380f933be0b6ff4.png.

  1. En la función DogItemButton(), actualiza el valor imageVector según el estado expanded de la siguiente manera:
import androidx.compose.material.icons.filled.ExpandLess

@Composable
private fun DogItemButton(
   //..
) {
   IconButton(onClick = onClick) {
       Icon(
           imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
           //..
       )
   }
}
  1. Ejecuta la app en un dispositivo o emulador, o vuelve a usar el modo interactivo en la vista previa. Observa que el ícono alterna entre los íconos ExpandMore c761ef298c2aea5a.png y ExpandLess b380f933be0b6ff4.png.

bf8bb280a774a6d4.gif

Buen trabajo al actualizar el ícono.

Cuando expandiste el elemento de la lista, ¿notaste el cambio abrupto de altura? El cambio de altura abrupto no parece una app pulida. Para resolver este problema, agrega una animación a la app.

7. Agregar animación

En las animaciones, se pueden agregar elementos visuales que informen a los usuarios sobre lo que sucede en tu app. Son particularmente útiles cuando la IU cambia de estado, como cuando se carga contenido nuevo o cuando hay acciones nuevas disponibles. Las animaciones también pueden agregar un estilo refinado a tu app.

En esta sección, agregarás una animación de resorte para animar el cambio de altura del elemento de la lista.

Animación de rebote

La animación de primavera es una animación basada en la física impulsada por una fuerza de resorte. Con una animación de resorte, el valor y la velocidad de movimiento se calculan en función de la fuerza de resorte que se aplica.

Por ejemplo, si arrastras el ícono de una app por la pantalla y lo levantas con el dedo, el ícono regresará a su ubicación original mediante una fuerza invisible.

En la siguiente animación, se muestra el efecto de resorte. Una vez que el dedo se suelte del ícono, el ícono se retrocederá y imitará un resorte.

7b52f63dc639c28d.gif

Efecto de primavera

Las siguientes dos propiedades guían la fuerza del resorte:

  • Proporción de amortiguamiento: Corresponde a la recompensa del resorte.
  • Nivel de rigidez: La rigidez del manantial, es decir, la velocidad con la que avanza hacia el final.

A continuación, se muestran algunos ejemplos de animaciones con diferentes proporciones de amortiguamiento y niveles de rigidez.

efecto primaveralRebote alto

efecto primaveralSin rebote

Rididez alta

Rigidez muy baja

Ahora, agregarás una animación de resorte a la app.

  1. En MainActivity.kt, en DogItem(), agrega un parámetro modifier al diseño Column.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
          modifier = Modifier
       ){
           //..
       }
   }
}

Observa la llamada a la función DogHobby() en la función que admite composición DogItem(). La información del pasatiempo del perro se incluye en la composición, según el valor booleano expanded. La altura del elemento de la lista cambia en función de si la información del pasatiempo es visible u oculta. Usa el modificador animateContentSize para agregar una transición entre las alturas nueva y anterior.

// No need to copy over
@Composable
fun DogItem(...) {

        //..
           if (expanded) {
               DogHobby(dog.hobbies)
           }
}
  1. Encadena el modificador con el modificador animateContentSize para animar el cambio de tamaño (altura de elemento de la lista).
import androidx.compose.animation.animateContentSize

Column(
           modifier = Modifier
               .animateContentSize()
       ) {
            //..
       }

Con tu implementación actual, estás animando la altura del elemento de la lista en tu app. Sin embargo, la animación es tan sutil que resulta difícil distinguirla cuando ejecutas la app. Para solucionar este problema, usarás un parámetro animationSpec opcional que te permita personalizar la animación.

  1. Agrega el parámetro animationSpec a la llamada a función animateContentSize(). Configúrala en una animación de resorte con los parámetros DampingRatioMediumBouncy y StiffnessLow.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioMediumBouncy,
               stiffness = Spring.StiffnessLow
           )
       )
)
  1. Compila y actualiza la vista previa en el panel Design y usa el modo interactivo o ejecuta tu app en un emulador o dispositivo para ver la animación de resorte en acción.

8cf711b8821b4696.gif

Vuelve a ejecutar la app en tu emulador o dispositivo y disfruta de tu atractiva app con animaciones.

1e9cf1dbc490924a.gif

8. (Opcional) Experimenta con otras animaciones

animate*AsState

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

Compose proporciona funciones animate*AsState() para Float, Color, Dp, Size, Offset y Int, entre otras. Puedes agregar compatibilidad con otros tipos de datos mediante animateValueAsState() que toma un tipo genérico.

Usa la función animateColorAsState() para animar el color cuando se expande un elemento de la lista.

Sugerencia:

  1. Declara un color y delega su inicialización a la función animateColorAsState().
  2. Configura el parámetro con nombre targetValue según el valor booleano expanded.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   val color by animateColorAsState(
       targetValue = if (expanded) Green25 else MaterialTheme.colors.surface,
   )
   Card(
       //..
   ) {...}
}
  1. Establece el color que declaraste anteriormente como el modificador en segundo plano en Column.
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   //..
                   )
               )
               .background(color = color)
       ) {...}
}

9. Obtén el código de la solución

Para descargar el código del codelab terminado, puedes usar este comando de git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución, puedes hacerlo en GitHub.

10. Conclusión

¡Felicitaciones! Agregaste un botón para ocultar y revelar información sobre el perro. Mejoraste la experiencia del usuario con las animaciones de primavera. También aprendiste a usar el modo interactivo en el panel de diseño.

También puedes probar un tipo diferente de animación de Jetpack Compose. No olvides compartir tu trabajo en redes sociales con el hashtag #AndroidBasics.

Más información