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
- 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 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.
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:
Obtén el código de partida
Para comenzar, descarga el código de partida:
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 y Expandir menos a tu app.
Í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 |
Material Design proporciona una serie de íconos organizados en categorías comunes para la mayoría de tus necesidades.
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
y Icons.Filled.ExpandMore
de esta biblioteca.
- En el panel Project, abre Gradle Scripts > build.gradle.kts (Module :app).
- Desplázate hasta el final de la función
build.gradle.kts (Module :app)
. En el bloquedependencies{}
, 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.
- En
MainActivity.kt
, después de la funciónDogItem()
, crea una nueva función de componibilidad llamadaDogItemButton()
. - Pasa un
Boolean
para el estado expandido, una expresión lambda para el controlador onClick del botón y unModifier
opcional de la siguiente manera:
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
}
- Dentro de la función
DogItemButton()
, agrega un elementoIconButton()
componible que acepte un parámetro llamadoonClick
con una expresión lambda que use la sintaxis lambda al final, que se invoque cuando se presione este ícono y unmodifier
opcional. ConfiguraIconButton's onClick
ymodifier value parameters
para que sean iguales a los valores que se pasaron aDogItemButton
.
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
){
IconButton(
onClick = onClick,
modifier = modifier
) {
}
}
- Dentro del bloque de lambda
IconButton()
, agrega un elementoIcon
componible y estableceimageVector value-parameter
enIcons.Filled.ExpandMore
. Esto es lo que se mostrará al final del elemento de lista . Android Studio te muestra una advertencia para los parámetrosIcon()
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
)
}
- Agrega el parámetro de valor
tint
y establece el color del ícono enMaterialTheme.colorScheme.secondary
. Agrega el parámetro con nombrecontentDescription
y establécelo en el recurso de cadenasR.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.
- Al comienzo de
DogItem()
, agrega un elementovar
para guardar el estado expandido del elemento de la lista. Establece el valor inicial enfalse
.
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) }
- Muestra el botón de ícono dentro del elemento de la lista. En el elemento
DogItem()
componible, al final del bloqueRow
, después de la llamada aDogInformation()
, agregaDogItemButton()
. Pasa el estadoexpanded
y una lambda vacía para la devolución de llamada. Definirás la acciónonClick
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*/ }
)
}
- Consulta
WoofPreview()
en el panel Design.
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.
Agrega el separador a la fila del elemento de la lista
- En
DogItem()
, entreDogInformation()
yDogItemButton()
, agrega unSpacer
. Pasa unModifier
conweight(1f)
. El elementoModifier.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*/ }
)
}
- 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.
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.
- Crea una nueva función de componibilidad llamada
DogHobby()
, que reciba el ID de recurso de cadenas de un pasatiempo y unModifier
opcional.
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
) {
}
- Dentro de la función
DogHobby()
, crea unColumn
y pasa el modificador que se pasó aDogHobby()
.
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
){
Column(
modifier = modifier
) {
}
}
- Dentro del bloque de
Column
, agrega dos elementosText
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
)
}
- En
DogItem()
, el elementoDogHobby()
componible se encontrará debajo de laRow
que contieneDogIcon()
,DogInformation()
,Spacer()
yDogItemButton()
. Para ello, une laRow
con unaColumn
de modo que el pasatiempo se pueda agregar debajo de laRow
.
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*/ }
)
}
}
- Agrega
DogHobby()
después deRow
como segundo elemento secundario deColumn
. Pasadog.hobbies
, que contiene el pasatiempo único del perro que se pasó y unmodifier
con el padding para el elementoDogHobby()
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)
)
)
}
}
}
- Consulta
WoofPreview()
en el panel Design. Observa el pasatiempo del perro que se muestra.
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.
- En la función de componibilidad
DogItem()
, en la llamada a funciónDogItemButton()
, define la expresión lambdaonClick()
, cambia el valor del estado booleanoexpanded
atrue
cuando se hace clic en el botón y vuelve a cambiarlo afalse
si se vuelve a hacer clic en el botón.
DogItemButton(
expanded = expanded,
onClick = { expanded = !expanded }
)
- En la función
DogItem()
, une la llamada a funciónDogHobby()
con una verificaciónif
en el valor booleanoexpanded
.
@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
.
- 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) en la esquina superior derecha del panel de Design (Diseño). De esta manera, se inicia la vista previa en el modo interactivo.
- 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.
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 y ExpandLess
para mostrar la flecha hacia arriba .
- En la función
DogItemButton()
, agrega una sentenciaif
que actualice el valorimageVector
basado en el estadoexpanded
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
.
- 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
yExpandLess
.
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 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.
Rebote alto | Sin rebote |
Rididez alta | 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)
)
)
}
}
- En
MainActivity.kt
, enDogItem()
, agrega un parámetromodifier
al diseñoColumn
.
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
...
Card(
...
) {
Column(
modifier = Modifier
){
...
}
}
}
- 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.
- Para Woof, la animación se activa y desactiva sin rebote. Para lograrlo, agrega el parámetro
animationSpec
a la llamada a funciónanimateContentSize()
. Configúralo en una animación de resorte conDampingRatioNoBouncy
para que no haya rebote en ella y un parámetroStiffnessMedium
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
)
)
)
- 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.
¡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.
- En
DogItem()
, declara un color y delega su inicialización a la funciónanimateColorAsState()
.
import androidx.compose.animation.animateColorAsState
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
val color by animateColorAsState()
...
}
- Configura el parámetro con nombre
targetValue
según el valor booleanoexpanded
. Si se expande el elemento, configúralo con el colortertiaryContainer
. De lo contrario, configúralo con el colorprimaryContainer
.
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,
)
...
}
- Establece
color
como el modificador en segundo plano deColumn
.
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
...
Card(
...
) {
Column(
modifier = Modifier
.animateContentSize(
...
)
)
.background(color = color)
) {...}
}
- 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 colortertiaryContainer
.
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.
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.