Animação simples com o Jetpack Compose

1. Antes de começar

Neste codelab, você vai aprender a adicionar uma animação simples ao seu app Android. Animações podem tornar o app mais interativo, interessante e fácil de interpretar para os usuários. Animar atualizações individuais em uma tela cheia de informações pode ajudar o usuário a notar o que mudou.

Há muitos tipos de animações que podem ser usadas na interface do usuário de um app. Os itens podem aparecer e desaparecer, podem ser movidos para dentro ou para fora da tela ou podem ser transformados de maneiras interessantes. Isso ajuda a tornar a interface do app expressiva e fácil de usar.

As animações também dão um visual sofisticado ao app, o que proporciona uma aparência elegante e ajuda o usuário.

Pré-requisitos

  • Conhecimento sobre Kotlin, incluindo funções, lambdas e elementos combináveis sem estado.
  • Conhecimento básico sobre como criar layouts no Jetpack Compose.
  • Conhecimento básico sobre como criar listas no Jetpack Compose.
  • Conhecimento básico do Material Design.

O que você vai aprender

  • Como criar uma animação de mola simples com o Jetpack Compose.

O que você vai criar

O que é necessário

  • A versão estável mais recente do Android Studio.
  • Conexão de Internet para fazer o download do código inicial.

2. Visão geral do app

No codelab Temas do Material Design com o Jetpack Compose, você usou o Material Design para criar o app Woof, que mostra uma lista de cachorros e as informações de cada um.

36c6cabd93421a92.png

Neste codelab, você vai adicionar uma animação ao app Woof com informações sobre hobbies que serão mostrados quando você abrir o item da lista. Você também vai adicionar uma animação de mola no item da lista que está sendo aberto.

c0d0a52463332875.gif

Acessar o código inicial

Para começar, faça o download do código inicial:

Outra opção é clonar o repositório do GitHub:

$ 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

Procure o código no repositório do GitHub do Woof app (link em inglês).

3. Adicionar ícone "Mostrar mais"

Nesta seção, você vai adicionar os ícones Mostrar mais 30c384f00846e69b.png e Mostrar menos f88173321938c003.png ao app.

def59d71015c0fbe.png

Ícones

Ícones são símbolos que ajudam os usuários a entender a interface do usuário com uma comunicação visual da função pretendida. Eles costumam se inspirar em objetos do mundo físico que um usuário pode já ter visto. Muitas vezes, o design do ícone reduz o nível de detalhes para o mínimo necessário para ser facilmente reconhecido pelo usuário. Por exemplo, um lápis no mundo físico é usado para escrever. Portanto, o ícone correspondente geralmente indica criar ou editar.

Lápis no caderno Foto de Angelina Litvin no Unsplash (links em inglês).

Ícone de lápis preto e branco

O Material Design oferece vários ícones organizados em categorias comuns para a maioria das suas necessidades.

Biblioteca de ícones do Material Design

Adicionar dependência do Gradle

Adicione a dependência da biblioteca material-icons-extended ao seu projeto. Você vai usar os ícones Icons.Filled.ExpandLess 30c384f00846e69b.png e Icons.Filled.ExpandMore f88173321938c003.png desta biblioteca.

  1. No painel Project, abra Gradle Scripts > build.gradle.kts (Module :app).
  2. Role até o fim do arquivo build.gradle.kts (Module :app). No bloco dependencies{}, adicione esta linha:
implementation("androidx.compose.material:material-icons-extended")

Adicionar o elemento combinável do ícone

Adicione uma função para exibir o ícone Mostrar mais da biblioteca de ícones do Material Design e use-a como um botão.

  1. Em MainActivity.kt, após a função DogItem(), crie uma nova função combinável com o nome DogItemButton().
  2. Transmita um Boolean para o estado aberto, uma expressão lambda para o gerenciador onClick do botão e um Modifier opcional, desta forma:
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
) {
 

}
  1. Na função DogItemButton(), adicione um elemento combinável IconButton() que aceite um parâmetro onClick, uma lambda usando a sintaxe de lambda final, que é invocada quando esse ícone é pressionado, e um modifier opcional. Defina os atributos IconButton's onClick e modifier value parameters iguais àqueles transmitidos a DogItemButton.
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. No bloco da lambda IconButton(), adicione um elemento combinável Icon e defina imageVector value-parameter como Icons.Filled.ExpandMore. Essa informação será mostrada no final do item da lista f88173321938c003.png. O Android Studio mostra um aviso para os parâmetros combináveis Icon() que você vai corrigir na próxima etapa.
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. Adicione o parâmetro com o valor tint e defina a cor do ícone como MaterialTheme.colorScheme.secondary. Adicione o parâmetro com o nome contentDescription e o defina como o recurso de string 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
   )
}

Mostrar o ícone

Adicione o elemento combinável DogItemButton() ao layout para mostrá-lo.

  1. No início de DogItem(), adicione uma var para salvar o estado aberto do item da lista. Defina o valor inicial como 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. Mostre o botão do ícone no item da lista. No elemento combinável DogItem(), adicione DogItemButton() no fim do bloco Row, depois da chamada para DogInformation(). Transmita o estado expanded e uma expressão lambda vazia ao callback. Você definirá a ação onClick em uma próxima etapa.
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. Confira o WoofPreview() no painel Design.

5bbf09cd2828b6.png

O botão para mostrar mais não está alinhado ao fim do item da lista. Isso vai ser corrigido na próxima etapa.

Alinhar o botão "Expandir mais"

Para alinhar o botão "Expandir mais" ao fim do item da lista, é necessário adicionar um espaçador ao layout com o atributo Modifier.weight().

No app Woof, cada linha de itens da lista contém uma imagem e informações de um cachorro, além de um botão "Expandir mais". Você vai adicionar um elemento combinável Spacer antes do botão "Mostrar mais" com o peso 1f para alinhar corretamente o ícone do botão. Como o espaçador é o único elemento filho com peso na linha, ele preenche o espaço restante depois de medir o comprimento do outro elemento filho que não tem um peso.

733f6d9ef2939ab5.png

Adicionar o espaçador à linha do item da lista

  1. Em DogItem(), entre DogInformation() e DogItemButton(), adicione um Spacer. Transmita um Modifier com weight(1f). O Modifier.weight() faz com que o espaçador ocupe o espaço restante na linha.
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. Confira o WoofPreview() no painel Design. O botão para mostrar mais agora está alinhado ao fim do item da lista.

8df42b9d85a5dbaa.png

4. Adicionar uma função combinável para mostrar um hobby

Nesta tarefa, você vai adicionar os elementos combináveis Text para mostrar as informações de hobbies do cachorro.

bba8146c6332cc37.png

  1. Crie uma nova função combinável com o nome DogHobby(), que usa um ID de recurso de string de hobby de um cachorro e um Modifier opcional.
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
  1. Na função DogHobby(), crie uma Column e transmita o modificador transmitido para DogHobby().
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) { 

   }
}
  1. Dentro do bloco Column, adicione dois elementos combináveis Text: um para mostrar o texto About acima das informações do hobby e outro para as informações.

Defina o text do primeiro arquivo como o about do arquivo strings.xml e defina o style como labelSmall. Defina o text do segundo como dogHobby, que é transmitido e defina o style como bodyLarge.

Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}
  1. Em DogItem(), o elemento combinável DogHobby() ficará abaixo da Row que contém DogIcon(), DogInformation(), Spacer() e DogItemButton(). Para fazer isso, una a Row com uma Column para que o hobby possa ser adicionado abaixo da 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. Adicione DogHobby() depois da Row como um segundo filho da Column. Transmita dog.hobbies, que contém o hobby exclusivo do cachorro transmitido e um modifier com o padding do elemento combinável DogHobby().
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)
       )
   )
}

A função DogItem() completa vai ficar assim:

@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. Confira o WoofPreview() no painel Design. Observe os hobbies do cachorro.

Visualização do Woof com os itens na lista aberta

5. Mostrar ou ocultar o hobby ao clicar no botão

Seu app tem um botão Mostrar mais para cada item da lista, mas ele ainda não faz nada. Nesta seção, você vai adicionar a opção de ocultar ou revelar as informações de hobbies quando o usuário clicar no botão.

  1. Na função combinável DogItem(), na chamada de função DogItemButton(), defina a expressão lambda onClick(), mude o valor do estado booleano expanded para true quando o botão receber um clique e volte o valor para false se o botão for clicado novamente.
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. Na função DogItem(), junte a chamada de função DogHobby() com uma verificação if no 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)
                   )
               )
           }
       }
   }
}

Agora, as informações de hobbies do cachorro só serão mostradas se o valor de expanded for true.

  1. A prévia pode mostrar como a interface vai ficar e também permite interagir com ela. Para interagir com a visualização da interface, passe o cursor sobre o texto WoofPreview no painel Design e clique no botão do Modo interativo 42379dbe94a7a497.png no canto superior direito do painel Design. Isso inicia a visualização no Modo interativo.

74e1624d68fb4131.png

  1. Interaja com a visualização clicando no botão para mostrar mais. As informações sobre o hobby ficam ocultas e são reveladas quando você clica no botão "Mostrar mais".

Animação dos itens da lista do Woof abrindo e fechando

O ícone do botão "Mostrar mais" continua o mesmo quando o item da lista é aberto. Para uma melhor experiência do usuário, mude o ícone para que ExpandMore mostre a seta para baixo c761ef298c2aea5a.png e ExpandLess mostre a seta para cima b380f933be0b6ff4.png.

  1. Na função DogItemButton(), adicione uma instrução if que atualize o valor imageVector com base no estado expanded desta forma:
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,
           ...
       )
   }
}

Note como você programou if-else no snippet de código anterior.

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

Isso é o mesmo que usar as chaves { } neste código:

if (expanded) {

`Icons.Filled.ExpandLess`

} else {

`Icons.Filled.ExpandMore`

}

As chaves são opcionais quando há uma única linha de código para a instrução if-else.

  1. Execute o app em um dispositivo ou emulador, ou use o modo interativo na visualização novamente. O ícone alterna entre as imagens ExpandMore c761ef298c2aea5a.png e ExpandLess b380f933be0b6ff4.png.

de5dc4a953f11e65.gif

Bom trabalho ao atualizar o ícone!

Ao expandir o item da lista, você notou uma mudança repentina na altura? A mudança repentina de altura não dá a impressão de um app sofisticado. Para resolver esse problema, você vai adicionar uma animação ao app.

6. Adicionar animação

As animações podem adicionar dicas visuais que mostram aos usuários o que está acontecendo no app. Elas são especialmente úteis quando a IU muda de estado, por exemplo, quando um novo conteúdo é carregado ou novas ações ficam disponíveis. As animações também podem adicionar um visual sofisticado ao app.

Nesta seção, você vai adicionar uma animação de mola para a mudança na altura do item da lista.

Animação de mola

A animação de mola é baseada na física e impulsionada por uma força elástica. Com ela, o valor e a velocidade do movimento são calculados com base na força aplicada.

Por exemplo, se você arrastar um ícone do app pela tela e o soltar levantando o dedo, ele vai voltar para o local original movido por uma força invisível.

A animação abaixo mostra o efeito de mola. Quando o ícone é solto, ele pula, imitando uma mola.

Efeito de soltar a mola

Efeito de mola

A força de mola é guiada pelas duas propriedades abaixo:

  • Proporção de amortecimento: a elasticidade da mola.
  • Nível de rigidez: a rigidez da mola, ou seja, a velocidade com que ela se move em direção ao final.

Confira abaixo alguns exemplos de animações com proporções de amortecimento e níveis de rigidez diferentes.

Efeito de molaElasticidade alta

Efeito de molaSem elasticidade

Alta rigidez

Baixa rigidez Rigidez muito baixa

Observe a chamada de função DogHobby() na função combinável DogItem(). As informações de hobbies do cachorro são incluídas na composição com base no valor booleano expanded. A altura do item da lista muda caso as informações de hobbies estejam visíveis ou ocultas. Atualmente, a transição é desagradável. Nesta seção, você vai usar o modificador animateContentSize para adicionar uma transição mais suave entre os estados abertos e fechados.

// 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. Em MainActivity.kt, na função DogItem(), adicione um parâmetro modifier ao layout Column.
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
  1. Encadeie o modificador com animateContentSize para animar a mudança de tamanho (altura do item da lista).
import androidx.compose.animation.animateContentSize

Column(
   modifier = Modifier
       .animateContentSize()
)

Na implementação atual, você está animando a altura do item da lista no app. No entanto, a animação é tão sutil que fica difícil perceber quando você executa o app. Para resolver isso, use um parâmetro opcional animationSpec que permite personalizar a animação.

  1. Para o Woof, a animação precisa ser suave e não sacudir. Para fazer isso, adicione o parâmetro animationSpec à chamada de função animateContentSize(). Defina-o como uma animação de mola com DampingRatioNoBouncy para que não fique pulando e um parâmetro StiffnessMedium para tornar a mola um pouco mais rígida.
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. Confira WoofPreview() no painel Design e use o modo interativo ou execute o app em um emulador ou dispositivo para conferir a animação de mola em ação.

c0d0a52463332875.gif

Parabéns! Curta seu app com animações.

7. (Opcional) Fazer experimentos com outras animações

animate*AsState

As funções animate*AsState() são uma das APIs de animação mais simples do Compose, usadas para animar um único valor. Somente o valor final (ou valor de segmentação) precisa ser informado, e a API inicia a animação do valor atual para o valor especificado.

O Compose oferece funções animate*AsState() para Float, Color, Dp, Size, Offset e Int, entre outras. Você pode adicionar suporte a outros tipos de dados facilmente com animateValueAsState(), que usa um tipo genérico.

Tente usar a função animateColorAsState() para mudar a cor quando um item da lista for aberto.

  1. Em DogItem(), declare uma cor e delegue a inicialização dela à função 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. Defina o parâmetro targetValue, dependendo do valor booleano expanded. Se o item da lista estiver aberto, defina-o como a cor do tertiaryContainer. Caso contrário, defina-o como a cor do 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. Defina a color como o modificador do plano de fundo como a Column.
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
  1. Observe como a cor muda quando o item da lista é aberto. Os itens da lista não abertos têm a cor do primaryContainer, e os itens da lista abertos têm a cor do tertiaryContainer.

Animação animateAsState

8. Acessar o código da solução

Para baixar o código do codelab concluído, use este comando git:

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

Se preferir, você pode baixar o repositório como um arquivo ZIP, descompactar e abrir no Android Studio.

Se você quiser conferir o código da solução, acesse o GitHub (em inglês).

9. Conclusão

Parabéns! Você adicionou um botão para ocultar e mostrar informações sobre o cachorro. Você melhorou a experiência do usuário usando animações de mola. Você também aprendeu a usar o modo interativo no painel de Design.

Você também pode testar um tipo diferente de animação do Jetpack Compose. Não se esqueça de compartilhar seu trabalho nas redes sociais com a hashtag #AndroidBasics.

Saiba mais