1. Introdução
Ú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
- Conhecimentos básicos sobre Kotlin
- Conhecimentos básicos sobre o Compose, incluindo:
- Layout simples (coluna, linha, caixa etc.)
- Elementos simples da IU (botão, texto etc.)
- Estados e recomposição
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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:
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.
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.
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 transitionSpec
s 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.
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.
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.
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 ). 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*
.
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.
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.
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.
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 valorupdateTransition
para animar vários valoresinfiniteTransition
para animar valores indefinidamenteAnimatable
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: