Resposta rápida: para criar animações no Jetpack Compose com Kotlin, escolha a API pelo tipo de mudança: use animate*AsState para um valor, AnimatedVisibility para entrada e saída, AnimatedContent para trocar conteúdos, updateTransition para coordenar propriedades e Animatable quando precisar controlar uma sequência em coroutine. Comece pela API de alto nível mais simples, use uma key estável em listas e prefira graphicsLayer para transformações executadas a cada frame. Assim você reduz código, recomposições e travamentos.

Animação boa é aquela que orienta o usuário sem atrasar a tarefa. No Jetpack Compose, diferente do antigo sistema de Views, animar não exige encadear ObjectAnimator e XML: a animação normalmente nasce de uma mudança de estado. Você descreve o valor ou conteúdo de destino, e o Compose calcula os quadros intermediários.

Este guia mostra como criar animações no Compose, do botão que muda de cor até listas que reordenam com fluidez, passando por transições de conteúdo, loaders, acessibilidade, testes e animações imperativas. Se você ainda está consolidando a base, revise o guia de Jetpack Compose e o tutorial de Jetpack Compose básico, porque os exemplos pressupõem que você já entende composables, modifiers e state.

Qual API de animação do Compose usar?

Use esta tabela como decisão rápida antes de escrever código:

SituaçãoAPI recomendadaExemplo típico
Animar um valor únicoanimate*AsStateCor, Dp, opacidade ou escala
Mostrar ou esconder um elementoAnimatedVisibilityBanner, detalhes de card ou CTA
Trocar conteúdo entre estadosAnimatedContentEtapa, contador, status ou tela interna
Fazer apenas um fade entre conteúdosCrossfadeTroca visual simples sem direção
Coordenar várias propriedadesupdateTransitionCor, escala e elevação do mesmo componente
Executar uma sequência controladaAnimatableProgresso, gesto, bounce ou animação em coroutine
Repetir enquanto o elemento está ativorememberInfiniteTransitionLoader, shimmer ou pulso
Animar itens de uma lista lazyModifier.animateItem()Inserção, remoção e reordenação
Animar mudança de tamanhoModifier.animateContentSize()Card expansível ou texto recolhível

A regra prática é começar pela API declarativa mais simples. Animatable não é “melhor” por oferecer mais controle: ele também exige controlar o ciclo de vida da coroutine, cancelamento e valores de destino. Use-o somente quando uma mudança de estado comum não representa bem o comportamento.

Dependências

As APIs principais vêm de androidx.compose.animation e androidx.compose.foundation. O Compose BOM alinha versões compatíveis, mas não adiciona as bibliotecas sozinho: o módulo ainda precisa declarar os artefatos que usa. Centralize a versão estável atual do BOM no version catalog, conforme explicado no guia de Compose BOM:

[versions]
composeBom = "<versao-estavel-atual>"

[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
compose-animation = { group = "androidx.compose.animation", name = "animation" }
compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
dependencies {
    implementation(platform(libs.compose.bom))
    implementation(libs.compose.animation)
    implementation(libs.compose.foundation)
}

O Modifier.animateContentSize() e o Modifier.animateItem() moram no foundation; o resto (AnimatedVisibility, AnimatedContent, updateTransition) vem do animation. Não há nada extra para configurar.

1. animate*AsState: o canivete suíço

Quando um valor muda de estado e você quer que ele chegue ao novo valor de forma suave, use animate*AsState. Existem variantes para Color, Dp, Float, Int, Size, Offset, Rect e Value genérico.

var selecionado by remember { mutableStateOf(false) }

val corDeFundo by animateColorAsState(
    targetValue = if (selecionado) Color(0xFF6650A4) else Color.LightGray,
    animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
    label = "corDeFundo"
)

Button(
    onClick = { selecionado = !selecionado },
    colors = ButtonDefaults.buttonColors(containerColor = corDeFundo)
) {
    Text("Selecionar")
}

Note o detalhe que confunde iniciantes: o animate*AsState retorna um State, por isso usamos by (delegação) e não =. Esquecer o by compila, mas a animação nunca acontece, porque você passa a função em vez do valor animado.

Use um label descritivo em animate*AsState, updateTransition e transições. Dependendo da versão da API ainda existem overloads sem label, mas eles podem estar descontinuados e tornam a inspeção pior. O rótulo aparece nas ferramentas de animação do Android Studio e ajuda a identificar qual valor está sendo interpolado.

2. AnimatedVisibility: entra e sai com elegância

Para elementos que aparecem e desaparecem, AnimatedVisibility cuida das transições de entrada (enter) e saída (exit) com composables prontos:

var expandido by remember { mutableStateOf(false) }

Column {
    TextButton(onClick = { expandido = !expandido }) {
        Text(if (expandido) "Recolher" else "Expandir")
    }
    AnimatedVisibility(
        visible = expandido,
        enter = fadeIn(animationSpec = tween(300)) +
                expandVertically(),
        exit = fadeOut(animationSpec = tween(200)) +
               shrinkVertically()
    ) {
        Card(Modifier.fillMaxWidth().padding(top = 8.dp)) {
            Text(
                "Conteúdo que só faz sentido quando expandido.",
                Modifier.padding(16.dp)
            )
        }
    }
}

O operador + combina transições — você pode somar um fadeIn com um slideInVertically, por exemplo. Isso é o equivalente moderno de encadear vários Animator no sistema de Views, mas sem o boilerplate.

3. AnimatedContent e Crossfade: trocar de estado

Quando o conteúdo muda com base num estado (um contador, uma aba, um passo de wizard), AnimatedContent faz a transição entre o conteúdo antigo e o novo. Crossfade é a versão simples, só com fade cruzado.

var passo by remember { mutableIntStateOf(1) }

AnimatedContent(
    targetState = passo,
    transitionSpec = {
        (slideInHorizontally { width -> width } + fadeIn() togetherWith
         slideOutHorizontally { width -> -width } + fadeOut())
    },
    label = "passoWizard"
) { passoAtual ->
    Text("Passo $passoAtual", Modifier.padding(24.dp))
}

Dica importante: a transição é definida no transitionSpec, que recebe AnimatedContentTransitionScope como receptor. Use togetherWith para ligar o enter ao exit. O lambda final ({ passoAtual -> ... }) desenha o conteúdo de cada estado — é nele que você consome o valor.

4. updateTransition: coordenar várias animações

Quando uma única mudança de estado dispara várias propriedades ao mesmo tempo (cor, tamanho, elevação), updateTransition mantém tudo sincronizado:

enum class EstadoCard { Repousando, Pressionado }

@Composable
fun CardInterativo() {
    var estado by remember { mutableStateOf(EstadoCard.Repousando) }
    val transicao = updateTransition(targetState = estado, label = "card")

    val escala by transicao.animateFloat(label = "escala") { estado ->
        when (estado) {
            EstadoCard.Repousando -> 1f
            EstadoCard.Pressionado -> 0.96f
        }
    }
    val elevacao by transicao.animateDp(label = "elevacao") { estado ->
        when (estado) {
            EstadoCard.Repousando -> 2.dp
            EstadoCard.Pressionado -> 8.dp
        }
    }

    Card(
        Modifier
            .graphicsLayer { scaleX = escala; scaleY = escala }
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = {
                        estado = EstadoCard.Pressionado
                        tryAwaitRelease()
                        estado = EstadoCard.Repousando
                    }
                )
            }
    ) {
        Text("Toque em mim", Modifier.padding(20.dp))
    }
}

A vantagem sobre vários animate*AsState isolados é que todas as propriedades partem do mesmo targetState no mesmo instante, então não há dessincronização visual. Para cards, chips e botões com feedback de toque, é a escolha certa.

5. rememberInfiniteTransition: loaders e pulsos

Animações que rodam para sempre — loaders, indicadores de pulsação, esqueletos de carregamento — usam rememberInfiniteTransition com a spec infiniteRepeatable:

@Composable
fun CoracaoBatendo(modifier: Modifier = Modifier) {
    val transicao = rememberInfiniteTransition(label = "batimento")
    val escala by transicao.animateFloat(
        initialValue = 1f,
        targetValue = 1.15f,
        animationSpec = infiniteRepeatable(
            animation = tween(700, easing = FastOutSlowInEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "escalaBatimento"
    )

    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favoritado",
        modifier = modifier.scale(escala),
        tint = Color.Red
    )
}

RepeatMode.Reverse vai e volta (efeito pulso); RepeatMode.Restart reinicia do início a cada ciclo (típico de spinners). Evite usar isso em elementos que ficam sempre visíveis na tela de fundo, porque a animação roda a 60 ou 120 quadros por segundo o tempo todo e consome bateria.

6. Animatable: controle imperativo

Quando você precisa orquestrar uma sequência (animar até um ponto, esperar, animar de novo) ou integrar com coroutines, use Animatable. Ele suspende dentro de um LaunchedEffect:

@Composable
fun BarraDeProgressoAnimada(modifier: Modifier = Modifier) {
    val progresso = remember { Animatable(0f) }

    LaunchedEffect(Unit) {
        progresso.animateTo(0.7f, animationSpec = tween(1200))
        delay(300)
        progresso.animateTo(1f, animationSpec = tween(600))
    }

    LinearProgressIndicator(
        progress = { progresso.value },
        modifier = modifier.fillMaxWidth()
    )
}

Animatable garante exclusão mútua entre animações: quando uma nova chamada de animateTo começa, a animação anterior é cancelada. Guarde a instância com remember para preservar o valor entre recomposições e trate cancelamento quando houver trabalho adicional na mesma coroutine.

7. Modifiers que animam sozinhos

Dois modifiers resolvem problemas comuns sem código extra:

  • Modifier.animateContentSize() — quando o tamanho de um composable muda (por exemplo, um texto que expande), ele interpola a transição em vez de pular de tamanho.
  • Modifier.animateItem() — dentro de LazyColumn e LazyRow, anima inserções, remoções e reordenação de itens. Em 2026 ele substitui o antigo animateItemPlacement(), que está deprecado.
LazyColumn {
    items(tarefas, key = { it.id }) { tarefa ->
        Row(
            Modifier
                .fillMaxWidth()
                .animateItem()
                .padding(12.dp)
        ) {
            Text(tarefa.titulo)
        }
    }
}

A key é indispensável aqui: sem ela, o Compose recria os itens em vez de animá-los, e o efeito some.

Specs: controlando tempo e curva

Todas as APIs aceitam um animationSpec. Os mais usados:

  • spring() — física real (mola), o padrão do Compose. Ajuste dampingRatio para mais ou menos “salto” e stiffness para velocidade.
  • tween(durationMillis, easing) — duração fixa com uma curva de easing. Use para tempos previsíveis.
  • keyframes() — define valores em instantes específicos, ideal para coreografias.
  • snap() — sem interpolação, pula direto para o valor.

Prefira spring para interações de toque (ele reage à velocidade do dedo) e tween com FastOutSlowInEasing para transições de tela.

Performance: como evitar animações travando

Uma animação pode perder frames por trabalho excessivo na UI thread, medição e layout repetidos, alocação por frame ou recomposição de uma árvore maior que o necessário. Antes de trocar de API, meça o que está acontecendo.

  1. Prefira transformações de desenho quando possível. Para escala, rotação, translação e alpha, Modifier.graphicsLayer costuma evitar uma nova medição do layout. Alterar size, por outro lado, pode exigir medir e posicionar novamente os elementos ao redor.
  2. Não recrie objetos em cada frame. Mantenha specs, listas e objetos auxiliares estáveis com remember quando fizer sentido. Use derivedStateOf apenas quando ele realmente reduzir leituras e recomposições frequentes.
  3. Dê chaves estáveis às listas. Modifier.animateItem() depende de key para reconhecer que o mesmo item mudou de posição. Sem chave estável, o Compose pode tratar o item como novo.
  4. Evite trabalho pesado no composable. Conversão de imagem, parsing, ordenação extensa e I/O não pertencem ao caminho de desenho da animação.
  5. Meça em build release. Debug, emulador lento e ferramentas de inspeção alteram o desempenho. Confirme o resultado em aparelho físico e com build comparável à produção.

Use o Layout Inspector para observar recomposições e as ferramentas de profiling para localizar frames lentos. Para mudanças importantes, combine testes funcionais com testes de screenshot no Compose e benchmarks de UI quando houver risco de regressão.

Acessibilidade: respeite quem reduz animações

Movimento em excesso pode causar desconforto, distração ou dificuldade de uso. A animação precisa comunicar hierarquia e continuidade, não bloquear conteúdo. As animações do Compose acompanham a escala de duração configurada pelo sistema; portanto, não contorne essa preferência com temporizadores manuais ou loops fora das APIs de animação.

Se o produto oferece uma opção própria de reduzir movimento, use-a para substituir deslocamentos grandes, parallax e pulsos contínuos por fades curtos ou mudanças imediatas. Mantenha o mesmo conteúdo e a mesma capacidade de interação nos dois modos: reduzir movimento não deve remover informação.

Não esconda informação essencial durante uma transição longa. Evite flashes e movimento contínuo sem função; mantenha áreas clicáveis estáveis e confira se leitores de tela recebem o estado final correto. Para uma revisão mais ampla, veja o guia de acessibilidade no Android com Compose.

Como testar animações no Jetpack Compose

Testes de UI do Compose usam um relógio controlável. Isso permite avançar o tempo sem esperar a animação em tempo real e verificar estados intermediários ou finais de forma determinística:

@get:Rule
val composeRule = createComposeRule()

@Test
fun cardFicaVisivelAposAnimacao() {
    composeRule.mainClock.autoAdvance = false

    composeRule.setContent {
        AnimatedVisibility(visible = true) {
            Text("Detalhes", Modifier.testTag("detalhes"))
        }
    }

    composeRule.mainClock.advanceTimeBy(1_000)
    composeRule.waitForIdle()

    composeRule.onNodeWithTag("detalhes").assertIsDisplayed()
}

Em testes de comportamento, valide o que a pessoa consegue ver ou fazer ao final da transição. Reserve screenshots para regressões visuais e não amarre todos os testes a durações exatas: pequenas mudanças de spec não deveriam quebrar um teste cujo objetivo é confirmar que o conteúdo aparece.

Casos de uso reais

  • Carrinho de compra em app de delivery: use AnimatedVisibility para revelar a barra flutuante de “finalizar pedido” quando há itens, e Modifier.animateItem() para a lista de produtos.
  • Onboarding em fintech: AnimatedContent entre os passos do tutorial, com slideInHorizontally para dar sensação de avanço.
  • Botão de favoritar: updateTransition coordenando ícone, cor e escala no mesmo toque.
  • Tela de carregamento: rememberInfiniteTransition em um logo que pulsa enquanto a chamada de rede não retorna.

Esses padrões cobrem a maioria dos apps Android que você encontra no mercado brasileiro, de marketplaces a apps de banco.

Perguntas frequentes

Como fazer animação no Jetpack Compose?

Crie um estado com remember, escolha um valor de destino e passe esse valor para uma API como animateFloatAsState, animateColorAsState ou AnimatedVisibility. Quando o estado mudar, o Compose interpola até o destino e redesenha a UI. Para sequências controladas, use Animatable dentro de uma coroutine.

Preciso da biblioteca Lottie para animar no Compose?

Não para a maioria dos casos. As APIs nativas cobrem transições, estados e microinterações. Lottie faz sentido para uma composição vetorial complexa preparada por design, especialmente quando recriá-la com primitivas do Compose seria caro ou difícil de manter.

Qual a diferença entre AnimatedContent e Crossfade?

Crossfade faz uma troca por fade entre os estados. AnimatedContent permite definir transitionSpec com slide, scale, fade, direção baseada no estado e comportamento diferente para entrada e saída. Use Crossfade para simplicidade e AnimatedContent quando a direção da mudança comunica significado.

animateItemPlacement ainda funciona?

Ele pode existir em projetos presos a versões antigas, mas em código atual prefira Modifier.animateItem(), a API unificada para animar inserção, remoção e posicionamento de itens lazy. Verifique a documentação da versão de Compose usada pelo projeto antes de migrar uma base legada.

animateAsState ou Animatable: qual escolher?

Use animate*AsState quando a animação é consequência direta de um estado de destino. Use Animatable quando precisa chamar animateTo, snapTo ou stop, encadear etapas, reagir a gestos ou controlar a animação dentro de uma coroutine.

Animações gastam muita bateria?

Transições curtas acionadas por interação normalmente têm custo limitado. Animações infinitas, efeitos fora da área visível e trabalho pesado em cada frame podem consumir CPU/GPU continuamente. Mantenha o movimento contínuo apenas quando ele comunica atividade e interrompa-o quando o elemento não estiver relevante.

Conclusão

O jeito Compose de animar troca a pergunta: em vez de “como eu animo isso?”, você pergunta “qual é o estado?”. A animação sai de graça quando você descreve a UI como função do estado. Comece com animate*AsState, escale para updateTransition quando precisar coordenar propriedades e guarde Animatable para sequências imperativas.

Para ir além, combine o que você aprendeu aqui com Material 3 Expressive, que traz tokens de motion baseados em Spring e transições compartilhadas com SharedTransitionLayout, e ative o Compose Hot Reload para iterar nas animações sem recompilar o app inteiro a cada ajuste. Em telas de mídia, a animação de UI (controles, scrim, expandir player) convive com o motor de playback: veja o guia de Media3 ExoPlayer com Jetpack Compose para não recriar o ExoPlayer a cada recomposição animada.