Como animar elementos no Jetpack Compose

1. Introdução

Logotipo do Jetpack Compose

Última atualização: 21/11/2023

Neste codelab, você aprenderá a usar algumas APIs Animation no Jetpack Compose.

O Jetpack Compose é um kit de ferramentas moderno criado para simplificar o desenvolvimento de interfaces. Se você não tem experiência com o Jetpack Compose, recomendamos que você faça estes codelabs primeiro:

O que você vai aprender

  • Como usar várias APIs de animação básicas

Pré-requisitos

O que é necessário

2. Etapas da configuração

Faça o download do código do codelab. Você pode clonar o repositório desta maneira:

$ git clone https://github.com/android/codelab-android-compose.git

Se preferir, faça o download do repositório como um arquivo ZIP:

Importe o projeto AnimationCodelab no Android Studio.

Como importar o codelab de animação para o Android Studio

O projeto tem vários módulos:

  • start é o estado inicial do codelab.
  • finished é o estado final do app após a conclusão do codelab.

Confira se start está selecionado no menu suspenso para a configuração de execução.

O botão Start selecionado no Android Studio

Vamos começar a trabalhar em vários cenários de animação no próximo capítulo. Todos os snippets de código em que trabalhamos neste codelab estão marcados com um comentário // TODO. Um bom truque é abrir a janela da ferramenta "TODO" no Android Studio e navegar até cada um dos comentários "TODO" do capítulo.

Lista "TODO" no Android Studio

3. Como animar uma mudança de valor simples

Vamos começar com uma das APIs de animação mais simples do Compose: a API animate*AsState. Essa API precisa ser usada ao animar mudanças de State.

Execute a configuração start e tente alternar as guias clicando nos botões "Home" e "Work" na parte de cima. Isso não muda o conteúdo da guia, mas é possível ver a mudança na cor do segundo plano do conteúdo.

Guia Home selecionada

Guia Work selecionada

Clique em TODO 1 na janela de ferramentas "TODO" e confira como isso é implementado. Ele está no elemento combinável Home.

val backgroundColor = if (tabPage == TabPage.Home) Seashell else GreenLight

Aqui, tabPage é um elemento TabPage apoiado por um objeto State. Dependendo do valor, a cor do plano de fundo é trocada entre pêssego e verde. Queremos animar essa mudança de valor.

Para animar uma mudança de valor simples como essa, podemos usar a API animate*AsState. Para criar um valor de animação, una o valor a uma mudança com a variante correspondente dos elementos de composição animate*AsState (neste caso, animateColorAsState). O valor retornado é um objeto State<T>, então podemos usar uma propriedade delegada local (link em inglês) com uma declaração by para tratá-lo como uma variável normal.

val backgroundColor by animateColorAsState(
        targetValue = if (tabPage == TabPage.Home) Seashell else GreenLight,
        label = "background color")

Execute o app novamente e tente mudar de guia. A mudança de cor agora é animada.

Animação de mudança de cor em ação entre guias

4. Como animar a visibilidade

Se você rolar o conteúdo do app, vai perceber que o botão de ação flutuante cresce e diminui de acordo com a direção da rolagem.

Botão de ação flutuante Edit aberto

Botão de ação flutuante Edit pequeno

Encontre o comentário TODO 2-1 e confira o que a função faz. Ele está no elemento combinável HomeFloatingActionButton. O texto "EDIT" (editar) é mostrado ou ocultado usando uma instrução if.

if (extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}

Animar essa mudança de visibilidade é tão simples quanto substituir if por um elemento combinável AnimatedVisibility.

AnimatedVisibility(extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}

Execute o app e confira como o FAB aumenta e diminui agora.

Animação do botão de ação flutuante de edição

AnimatedVisibility executa a animação sempre que o valor Boolean especificado mudar. Por padrão, AnimatedVisibility aumenta o elemento quando ele precisa ser mostrado. Na hora de ocultar, ele é encolhido. Esse comportamento funciona muito bem neste exemplo com o FAB, mas também podemos personalizá-lo.

Clique no FAB. A mensagem "Edit feature is not supported" (o recurso de edição não tem suporte) vai aparecer. A mensagem também usa AnimatedVisibility para animar o aparecimento e o desaparecimento dela. Em seguida, você vai personalizar esse comportamento para que a mensagem apareça de cima para baixo e desapareça de baixo para cima.

Mensagem informando que o recurso de edição não tem suporte

Encontre TODO 2-2 e confira o código no elemento combinável 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 a animação, adicione os parâmetros enter e exit ao elemento combinável AnimatedVisibility.

O parâmetro enter precisa ser uma instância de EnterTransition. Neste exemplo, podemos usar a função slideInVertically para criar uma EnterTransition e slideOutVertically para a transição de saída. Mude o código desta maneira:

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(),
    exit = slideOutVertically()
)

Execute o app novamente. Ao clicar no botão de edição, você vai perceber que a animação está melhor, mas não exatamente correta, porque o comportamento padrão de slideInVertically e slideOutVertically usa metade da altura do item.

A saída é interrompida na metade do caminho

Para a transição de entrada: podemos ajustar o comportamento padrão para usar toda a altura do item definindo o parâmetro initialOffsetY. O initialOffsetY precisa ser uma lambda que retorne a posição inicial.

A lambda recebe um argumento, a altura do elemento. Para garantir que o item deslize do topo da tela, retornamos o valor negativo, já que a parte de cima tem o valor 0. Queremos que a animação comece de -height a 0 (a posição de repouso final) para que funcione por todo o caminho de cima para baixo.

Ao usar slideInVertically, o deslocamento de destino é sempre de 0 (pixel). O initialOffsetY pode ser especificado como um valor absoluto ou uma porcentagem da altura total do elemento usando uma função lambda.

Da mesma forma, slideOutVertically presume que o deslocamento inicial é 0, então apenas targetOffsetY precisa ser especificado.

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)
        )
    }
}

Ao executar o app de novo, observe que a animação está mais inline com o que esperávamos:

Animação de chegada com deslocamento funcionando

Podemos personalizar mais nossa animação com o parâmetro animationSpec. animationSpec é um parâmetro comum para várias APIs de animação, incluindo EnterTransition e ExitTransition. Podemos transmitir um dos vários tipos de AnimationSpec para especificar como o valor de animação precisa mudar com o tempo. Neste exemplo, vamos usar uma AnimationSpec simples baseada na duração. Ela pode ser criada com a função tween. A duração é de 150 ms e o easing é LinearOutSlowInEasing. Para a animação de saída, vamos usar a mesma função tween no parâmetro animationSpec, mas com duração de 250 ms e easing FastOutLinearInEasing.

O código resultante vai ficar assim:

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)
        )
    }
}

Execute o app e clique no FAB novamente. A mensagem agora desliza de cima para baixo com diferentes funções e durações de easing:

Animação mostrando a mensagem de edição deslizando de cima para baixo

5. Como animar mudanças no tamanho do conteúdo

O app mostra vários temas no conteúdo. Clique em um deles para que o texto seja aberto e mostrado. O card se expande e diminui quando o corpo de texto é mostrado ou ocultado.

Lista de temas fechada

Lista de temas aberta

Confira o código para TODO 3 no elemento combinável TopicRow.

Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
) {
    // ... the title and the body
}

O elemento Column aqui muda de tamanho conforme o conteúdo é modificado. Podemos animar a mudança adicionando o modificador animateContentSize.

Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
        .animateContentSize()
) {
    // ... the title and the body
}

Execute o app e clique em um dos temas. Ele abre e fecha com uma animação.

Animação da lista de temas abrindo e fechando

animateContentSize também pode ser personalizado com uma animationSpec personalizada. Temos opções para mudar o tipo de animação de mola para interpolação etc. Consulte a documentação sobre como personalizar animações para mais informações.

6. Como animar vários valores

Agora que estamos familiarizados com algumas APIs básicas de animação, vamos conferir a API Transition, que permite criar animações mais complexas. Com a API Transition, podemos saber quando todas as animações em uma Transition são concluídas, o que não é possível com as APIs animate*AsState individuais que discutimos anteriormente. A API Transition também permite definir transitionSpecs diferentes ao fazer a transição entre estados diferentes. Confira como ela pode ser usada:

Para este exemplo, personalizamos o indicador de guia. É um retângulo mostrado na guia selecionada.

Guia Home selecionada

Guia Work selecionada

Encontre TODO 4 no elemento combinável HomeTabIndicator e confira como o indicador da guia é implementado.

val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) PaleDogwood else Green

Aqui, indicatorLeft é a posição horizontal da borda esquerda do indicador na linha da guia. indicatorRight é a posição horizontal da borda direita do indicador. A cor também troca entre pêssego e verde.

Para animar esses diversos valores simultaneamente, podemos usar uma Transition. Uma Transition pode ser criada com a função updateTransition. Transmita o índice da guia selecionada como o parâmetro targetState.

Cada valor de animação pode ser declarado com as funções de extensão animate* de Transition. Neste exemplo, usamos animateDp e animateColor. Elas usam um bloco lambda, e podemos especificar o valor desejado para cada um dos estados. Já sabemos quais são os valores de destino, então podemos unir os valores como mostrado abaixo. É possível usar uma declaração by e torná-la uma propriedade delegada local novamente, já que as funções animate* retornam um 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
}

Execute o app e observe que a mudança de guia ficou muito mais interessante. Ao clicar na guia e mudar o valor do estado tabPage, todos os valores associados a transition começam a animação de mudança para o valor especificado do estado desejado.

Animação entre as guias Home e Work

Além disso, podemos especificar o parâmetro transitionSpec para personalizar o comportamento da animação. Por exemplo, podemos dar um efeito elástico para o indicador fazendo com que a borda mais próxima do destino se mova mais rápido que a outra. Podemos usar a função de infixo isTransitioningTo em lambdas transitionSpec para determinar a direção da mudança do 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
}

Execute o app novamente e tente trocar de guia.

Efeito elástico personalizado na mudança de guia

A inspeção de transições na prévia do Compose pode ser usada com o Android Studio. Para usar a prévia de animações, inicie o Modo interativo clicando no ícone "Start Animation Preview" no canto superior direito de um elemento combinável na prévia (ícone Ícone de prévia da animação). Tente clicar no ícone do elemento combinável PreviewHomeTabBar. Isso vai abrir um novo painel "Animations".

Clique no botão de ícone "Play" para mostrar a animação. Também é possível arrastar a barra de busca para conferir cada um dos frames de animação. Para descrever melhor os valores de animação, especifique o parâmetro label em updateTransition e os métodos animate*.

Procurar animações no Android Studio

7. Como repetir animações

Tente clicar no botão de atualização ao lado da temperatura atual. O app vai começar a carregar as informações mais recentes sobre o clima (simuladas). Até que o carregamento seja concluído, você vai notar um círculo cinza e uma barra funcionando como um indicador. Vamos animar o valor alfa desse indicador para deixar mais claro que o processo está em andamento.

Imagem estática do card de informação atuando como marcador de posição que ainda não está animado.

Encontre TODO 5 no elemento combinável LoadingRow.

val alpha = 1f

Queremos animar o valor repetidamente entre 0 f e 1 f. Para isso, podemos usar InfiniteTransition. Essa API é semelhante à API Transition da seção anterior. Ambas animam diversos valores, mas enquanto Transition anima os valores com base nas mudanças de estado, InfiniteTransition os anima indefinidamente.

Para criar uma InfiniteTransition, use a função rememberInfiniteTransition. Em seguida, cada mudança de valor de animação pode ser declarada com uma das funções de extensão animate* de InfiniteTransition. Neste caso, estamos animando um valor alfa. Vamos usar animatedFloat. O parâmetro initialValue precisa ser 0f, e o targetValue 1f. Também podemos especificar uma AnimationSpec para essa animação, mas essa API usa apenas uma InfiniteRepeatableSpec. Use a função infiniteRepeatable para criar uma. Essa AnimationSpec envolve todas as AnimationSpec com base em duração e as torna repetitíveis. Por exemplo, o código resultante vai ficar parecido com o exemplo abaixo.

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"
)

O repeatMode padrão é RepeatMode.Restart. Ele faz a transição de initialValue para targetValue e começa novamente no initialValue. Ao definir repeatMode como RepeatMode.Reverse, a animação passa de initialValue para targetValue e de targetValue para initialValue. A animação passa de 0 para 1 e de 1 para 0.

A animação keyFrames é outro tipo de animationSpec (sendo que algumas são tween e spring), o que permite mudar o valor em andamento em diferentes milissegundos. Inicialmente, definimos durationMillis como 1.000 ms. Em seguida, podemos definir os principais frames da animação, por exemplo, aos 500 ms de animação. Queremos que o valor alfa seja 0,7 f. Isso vai mudar a progressão da animação: ela vai avançar rapidamente de 0 a 0,7 em até 500 ms da animação e de 0,7 a 1,0 de 500 ms a 1.000 ms da animação, diminuindo a velocidade mais perto do fim.

Se quisermos mais de um frame-chave, podemos definir vários keyFrames, desta maneira:

animation = keyframes {
   durationMillis = 1000
   0.7f at 500
   0.9f at 800
}

Execute o app e clique no botão "Refresh". Agora, o indicador de carregamento é animado.

Repetindo o conteúdo do marcador animado

8. Animação por gestos

Nesta seção final, você vai aprender a executar animações com base em entradas de toque. Vamos criar um modificador swipeToDismiss do zero.

Encontre TODO 6-1 no modificador swipeToDismiss. Aqui, estamos tentando criar um modificador que torna o elemento deslizável com toque. Quando o elemento é mostrado na borda da tela, chamamos o callback onDismissed para que ele possa ser removido.

Para criar um modificador swipeToDismiss, precisamos entender alguns conceitos importantes. Primeiro, o usuário coloca o dedo na tela, gerando um evento de toque com as coordenadas x e y. Em seguida, ele move o dedo para a direita ou esquerda, mudando x e y com base no movimento. O item em que ele está tocando precisa ser movido com o dedo. Portanto, vamos atualizar a posição do item com base na posição e velocidade do evento de toque.

É possível usar vários dos conceitos descritos na documentação sobre gestos do Compose. Usando o modificador pointerInput, temos acesso de baixo nível a eventos de toque do ponteiro de entrada e podemos monitorar a velocidade com que o usuário arrasta o dedo pela tela usando o mesmo ponteiro. Se o usuário soltar o item antes que possa ultrapassar o limite para ser dispensado, o item vai voltar à posição original.

Precisamos considerar vários aspectos exclusivos desse cenário. Primeiro, qualquer animação em andamento pode ser interceptada por um evento de toque. Segundo, o valor da animação pode não ser a única fonte da verdade. Em outras palavras, pode ser necessário sincronizar o valor da animação com valores de eventos de toque.

Animatable é a API de nível mais baixo que discutimos até agora. Ela tem vários recursos úteis em cenários de gestos, como a possibilidade de se adaptar instantaneamente ao novo valor de um gesto e parar qualquer animação em andamento quando um novo evento de toque é acionado. Vamos criar uma instância de Animatable e a usar para representar o deslocamento horizontal do elemento deslizante. Importe Animatable de androidx.compose.animation.core.Animatable, e não 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) {
            // ...

Em TODO 6-2, acabamos de receber um evento de toque. Vamos precisar interceptar a animação se ela estiver em execução. Para isso, chame stop em Animatable. A chamada vai ser ignorada se a animação não estiver em execução. O VelocityTracker vai ser usado para calcular a velocidade do movimento de um usuário da esquerda para a direita. awaitPointerEventScope é uma função de suspensão que pode aguardar eventos de entrada do usuário e responder a eles.

// 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 {

Em TODO 6-3, estamos sempre recebendo eventos de arrastar. Precisamos sincronizar a posição do evento de toque no valor da animação. Para isso, podemos usar snapTo em Animatable. O método snapTo precisa ser chamado dentro de outro bloco launch, já que awaitPointerEventScope e horizontalDrag são escopos de corrotina restritos. Isso significa que eles só podem suspend (suspender) eventos de ponteiro awaitPointerEvents, e snapTo não é um desses eventos.

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()

}

Em TODO 6-4, o elemento é solto deslizando rapidamente. Precisamos calcular a posição final de encaixe para decidir se é necessário deslizar o elemento de volta para a posição original ou o descartar e invocar o callback. Usamos o objeto decay criado anteriormente para calcular o 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)

Em TODO 6-5, estamos prestes a iniciar a animação. Mas antes disso, queremos definir limites de valor máximo e mínimo para interromper o elemento Animatable quando forem alcançados (-size.width e size.width, já que não queremos que offsetX possa ser maior que esses dois valores). O modificador pointerInput permite acessar o tamanho do elemento pela propriedade size. Ele vai ser usado para conferir nossos limites.

offsetX.updateBounds(
    lowerBound = -size.width.toFloat(),
    upperBound = size.width.toFloat()
)

Em TODO 6-6, finalmente podemos iniciar nossa animação. Primeiro, vamos comparar a posição de encaixe da animação de deslize que calculamos anteriormente e o tamanho do elemento. Se a posição de encaixe estiver abaixo do tamanho, isso significa que a velocidade da animação não foi suficiente. Podemos usar animateTo para animar o valor de volta a 0 f. Caso contrário, vamos usar animateDecay para iniciar a animação de deslizar rapidamente. Quando a animação for concluída (provavelmente pelos limites definidos), poderemos chamar o callback.

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 fim, confira TODO 6-7. Já configuramos todas as animações e gestos, então basta aplicar o deslocamento ao elemento. Isso vai garantir o movimento com o valor produzido pelo nosso gesto ou animação:

.offset { IntOffset(offsetX.value.roundToInt(), 0) }

Como resultado desta seção, você vai ter um código como este:

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) }
}

Execute o app e tente deslizar um dos itens de tarefa. Você vai notar que o elemento desliza de volta para a posição padrão ou desaparece e é removido, dependendo da velocidade do movimento. Você também pode parar o elemento enquanto ele está sendo animado.

Animação por gesto de deslizar para dispensar itens

9. Parabéns!

Parabéns! Você aprendeu sobre as APIs básicas de animação do Compose.

Neste codelab, você aprendeu a usar:

APIs de animação de alto nível:

  • animatedContentSize
  • AnimatedVisibility

APIs de animação de baixo nível:

  • animate*AsState para animar um único valor
  • updateTransition para animar vários valores
  • infiniteTransition para animar valores indefinidamente
  • Animatable para criar animações personalizadas com gestos de toque

Qual é a próxima etapa?

Confira os outros codelabs no Programa de treinamentos do Compose.

Para saber mais, consulte Animações do Compose e estes documentos de referência: