Animación simple con Jetpack Compose

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 app. Los elementos pueden atenuarse cuando aparecen y desaparecen, pueden entrar o salir de la pantalla, o pueden transformarse de maneras 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:

Requisitos previos

  • Conocimientos sobre Kotlin, incluidas funciones, lambdas y elementos sin estado componibles
  • 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

Requisitos

  • La versión estable más reciente de Android Studio
  • Conexión a Internet para descargar el código de partida

2. 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.

36c6cabd93421a92.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:

c0d0a52463332875.gif

Obtén el código de partida

Para comenzar, descarga el código de partida:

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.

3. Ícono de agregar más

En esta sección, agregarás los íconos Expandir más 30c384f00846e69b.png y Expandir menos f88173321938c003.png a tu app.

def59d71015c0fbe.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.

Un lápiz en un cuaderno 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.

Biblioteca de íconos de material

Cómo agregar una dependencia de Gradle

Agrega la dependencia de la 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 Project, abre Gradle Scripts > build.gradle.kts (Module :app).
  2. Desplázate hasta el final de la función build.gradle.kts (Module :app). En el bloque dependencies{}, agrega la siguiente línea:
implementation("androidx.compose.material:material-icons-extended")

Agrega el ícono componible

Agrega una función para mostrar el ícono 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 controlador onClick 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() componible que 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 un modifier opcional. Configura IconButton's onClick y modifier value parameters para que sean iguales a los valores que se pasaron a DogItemButton.
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. Dentro del bloque de lambda IconButton(), agrega un elemento Icon componible y establece imageVector value-parameter en Icons.Filled.ExpandMore. Esto es lo que se mostrará al final del elemento de lista f88173321938c003.png. Android Studio te muestra una advertencia para los parámetros Icon() componibles que corregirás en el siguiente paso.
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

IconButton(
   onClick = onClick,
   modifier = modifier
) {
   Icon(
       imageVector = Icons.Filled.ExpandMore
   )
}
  1. Agrega el parámetro de valor tint y establece el color del ícono en MaterialTheme.colorScheme.secondary. Agrega el parámetro con nombre contentDescription y establécelo en el recurso de cadenas R.string.expand_button_content_description.
IconButton(
   onClick = onClick,
   modifier = modifier
){
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       contentDescription = stringResource(R.string.expand_button_content_description),
       tint = MaterialTheme.colorScheme.secondary
   )
}

Muestra el ícono

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

  1. Al comienzo de DogItem(), agrega un elemento var para guardar el estado expandido del elemento de la lista. Establece el valor inicial en false.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

var expanded by remember { mutableStateOf(false) }
  1. Muestra el botón de ícono dentro del elemento de la lista. En el elemento DogItem() componible, al final del bloque Row, después de la llamada a DogInformation(), agrega DogItemButton(). Pasa el estado expanded y una lambda vacía para la devolución de llamada. Definirás la acción onClick en un paso posterior.
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. Consulta WoofPreview() en el panel Design.

5bbf09cd2828b6.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.

Alinea 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 separador 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 un perro, información sobre él y un botón para expandir. Agregarás un elemento Spacer componible antes del botón para expandir más con peso 1f para alinear correctamente el ícono del botón. Como el separador es el único elemento secundario ponderado de la fila, completará el espacio restante en la fila después de medir el ancho del otro elemento secundario no ponderado.

733f6d9ef2939ab5.png

Agrega el separador a la fila del elemento de la lista

  1. En DogItem(), entre DogInformation() y DogItemButton(), agrega un Spacer. Pasa un Modifier con weight(1f). El elemento Modifier.weight() hace que el separador llene el espacio restante en la fila.
import androidx.compose.foundation.layout.Spacer

Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(modifier = Modifier.weight(1f))
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. Consulta WoofPreview() en el panel Design. Observa que el botón para expandir más ahora está alineado al final del elemento de la lista.

8df42b9d85a5dbaa.png

4. Agrega un elemento componible para mostrar un pasatiempo

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

bba8146c6332cc37.png

  1. Crea una nueva función de componibilidad llamada DogHobby(), que reciba el ID de recurso de cadenas de un pasatiempo y un Modifier opcional.
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
  1. Dentro de la función DogHobby(), crea un Column y pasa el modificador que se pasó a DogHobby().
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) {

   }
}
  1. Dentro del bloque de Column, agrega dos elementos Text componibles: uno para mostrar el texto Acerca de sobre el pasatiempo y otro para mostrar la información del pasatiempo.

Establece el text del primero como el about del archivo strings.xml y establece style como labelSmall. Establece el text del segundo en dogHobby, que se pasa y establece style en bodyLarge.

Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}
  1. En DogItem(), el elemento DogHobby() componible se encontrará debajo de la Row que contiene DogIcon(), DogInformation(), Spacer() y DogItemButton(). Para ello, une la Row con una Column de modo que el pasatiempo se pueda agregar debajo de la Row.
Column() {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
       Spacer(modifier = Modifier.weight(1f))
       DogItemButton(
           expanded = expanded,
           onClick = { /*TODO*/ }
       )
   }
}
  1. Agrega DogHobby() después de Row como segundo elemento secundario de Column. Pasa dog.hobbies, que contiene el pasatiempo único del perro que se pasó y un modifier con el padding para el elemento DogHobby() componible.
Column() {
   Row() {
      ...
   }
   DogHobby(
       dog.hobbies,
       modifier = Modifier.padding(
           start = dimensionResource(R.dimen.padding_medium),
           top = dimensionResource(R.dimen.padding_small),
           end = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_medium)
       )
   )
}

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(
       modifier = modifier
   ) {
       Column() {
           Row(
               modifier = Modifier
                   .fillMaxWidth()
                   .padding(dimensionResource(R.dimen.padding_small))
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { /*TODO*/ },
               )
           }
           DogHobby(
               dog.hobbies,
               modifier = Modifier.padding(
                   start = dimensionResource(R.dimen.padding_medium),
                   top = dimensionResource(R.dimen.padding_small),
                   end = dimensionResource(R.dimen.padding_medium),
                   bottom = dimensionResource(R.dimen.padding_medium)
               )
           )
       }
   }
}
  1. Consulta WoofPreview() en el panel Design. Observa el pasatiempo del perro que se muestra.

Vista previa de Woof con elementos de lista expandidos

5. 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 la función de componibilidad DogItem(), en la llamada a función DogItemButton(), define la expresión lambda onClick(), cambia el valor del estado booleano expanded a true cuando se hace clic en el botón y vuelve a cambiarlo a false si se vuelve a hacer clic en el botón.
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. En la función DogItem(), une la llamada a función DogHobby() con una verificación if en el valor booleano expanded.
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       ...
   ) {
       Column(
           ...
       ) {
           Row(
               ...
           ) {
               ...
           }
           if (expanded) {
               DogHobby(
                   dog.hobbies, modifier = Modifier.padding(
                       start = dimensionResource(R.dimen.padding_medium),
                       top = dimensionResource(R.dimen.padding_small),
                       end = dimensionResource(R.dimen.padding_medium),
                       bottom = dimensionResource(R.dimen.padding_medium)
                   )
               )
           }
       }
   }
}

Ahora, 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, coloca el cursor sobre el texto de WoofPreview en el panel de Design (Diseño). Luego, haz clic en el botón de Interactive Mode (Modo interactivo) 42379dbe94a7a497.png en la esquina superior derecha del panel de Design (Diseño). De esta manera, se inicia la vista previa en el modo interactivo.

74e1624d68fb4131.png

  1. Haz clic en el botón para expandir más para 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.

Animación de los elementos de la lista de Woof que se expanden y se contraen

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(), agrega una sentencia if que actualice el valor imageVector basado en 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,
           ...
       )
   }
}

Observa cómo escribiste if-else en el fragmento de código anterior.

if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore

Esto es lo mismo que usar las llaves { } en el siguiente código:

if (expanded) {

`Icons.Filled.ExpandLess`

} else {

`Icons.Filled.ExpandMore`

}

Las llaves son opcionales si hay una sola línea de código para la sentencia if-else.

  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.

de5dc4a953f11e65.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.

6. Agrega 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 retrocederá, imitando a un resorte.

Efecto de liberación del resorte

Efecto de resorte

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 de resorteRebote alto

Efecto de resorteSin rebote

Rididez alta

baja rigidez Rigidez muy baja

Observa la llamada a función DogHobby() en la función de componibilidad 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. Actualmente, la transición es molesta. En esta sección, usarás el modificador animateContentSize para agregar una transición más fluida entre los estados expandido y no expandido.

// No need to copy over
@Composable
fun DogItem(...) {
  ...
    if (expanded) {
       DogHobby(
          dog.hobbies,
          modifier = Modifier.padding(
              start = dimensionResource(R.dimen.padding_medium),
              top = dimensionResource(R.dimen.padding_small),
              end = dimensionResource(R.dimen.padding_medium),
              bottom = dimensionResource(R.dimen.padding_medium)
          )
      )
   }
}
  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
       ){
           ...
       }
   }
}
  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. Para Woof, la animación se activa y desactiva sin rebote. Para lograrlo, agrega el parámetro animationSpec a la llamada a función animateContentSize(). Configúralo en una animación de resorte con DampingRatioNoBouncy para que no haya rebote en ella y un parámetro StiffnessMedium para hacer que el resorte sea un poco más rígido.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioNoBouncy,
               stiffness = Spring.StiffnessMedium
           )
       )
)
  1. Revisa WoofPreview() en el panel Design (Diseño) y usa el modo interactivo, o bien ejecuta tu app en un emulador o dispositivo para ver la animación de resorte en acción.

c0d0a52463332875.gif

¡Genial! Disfruta de tu atractiva app con animaciones.

7. (Opcional) Experimenta con otras animaciones

animate*AsState

Las funciones animate*AsState() son una de las APIs 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 cambiar el color cuando se expande un elemento de la lista.

  1. En DogItem(), declara un color y delega su inicialización a la función animateColorAsState().
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState()
   ...
}
  1. Configura el parámetro con nombre targetValue según el valor booleano expanded. Si se expande el elemento, configúralo con el color tertiaryContainer. De lo contrario, configúralo con el color primaryContainer.
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState(
       targetValue = if (expanded) MaterialTheme.colorScheme.tertiaryContainer
       else MaterialTheme.colorScheme.primaryContainer,
   )
   ...
}
  1. Establece color como el modificador en segundo plano de Column.
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
  1. Consulta cómo cambia el color cuando se expande el elemento de la lista. Los elementos de la lista no expandida tienen un color primaryContainer, mientras que los elementos de la lista expandida tienen un color tertiaryContainer.

Animación de animateAsState

8. 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.

9. 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 resorte. También aprendiste a usar el modo interactivo en el panel de Design.

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