Animação boa é aquela que o usuário não percebe — ele só sente que o app é fluido, rápido e “caro”. No Jetpack Compose, diferente do antigo sistema de Views, animar não é mais uma tarefa de quem desenha nem uma parafernália de ObjectAnimator e XML. Animação virou uma consequência natural de mudar de estado: você descreve o que quer mostrar para cada estado e o Compose calcula os quadros intermediários para você.

Este guia mostra como criar animações no Jetpack Compose em 2026, do botão que muda de cor até listas que reordenam com fluidez, passando por transições de tela, loaders e animações imperativas. Se você ainda está consolidando a base, vale revisar antes o guia de Jetpack Compose e o tutorial de Jetpack Compose básico, porque este texto pressupõe que você já entende composables, modifiers e state.

Resposta rápida: qual API usar?

Antes de mergulhar, use este mapa mental para escolher a ferramenta certa. A maioria dos casos cai em um destes cinco grupos:

SituaçãoAPI recomendada
Animar um valor único (cor, tamanho, opacidade)animate*AsState
Mostrar ou esconder um elementoAnimatedVisibility
Trocar conteúdo entre estados distintosAnimatedContent ou Crossfade
Coordenar várias propriedades numa mesma mudançaupdateTransition
Animação que roda para sempre (loader, pulse)rememberInfiniteTransition

A regra prática: comece pela API mais simples do topo da tabela. Se ela não resolver, desça uma linha. Quase nunca você precisa de animações manuais com Animatable.

Dependências

As APIs de animação vêm no artefato androidx.compose.animation, que já entra automaticamente quando você usa o Compose BOM. Em um projeto criado pelo Android Studio em 2026, o libs.versions.toml costuma trazer algo como:

[versions]
composeBom = "2026.07.00"

[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.

O parâmetro label é obrigatório em 2026 para qualquer função animate* e updateTransition. Ele aparece no Layout Inspector e nos relatórios de recomposição, e ajuda muito a depurar animações travadas.

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 é thread-safe (vive no composition, mas só uma coroutine anima por vez) e é a base sobre a qual as outras APIs são construídas.

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: não brigue com a recomposição

Animações lentas quase sempre vêm de recomposição excessiva, não da API em si. Três hábitos mantêm tudo a 60fps:

  1. Anime valores, não composables. Mudar scale via Modifier.graphicsLayer recompõe menos do que trocar o Modifier.size. Quanto menos o composable recompor, mais fluido.
  2. Derive de estado estável. Use remember e derivedStateOf para evitar recalcular specs a cada frame.
  3. Confira no Layout Inspector. A aba “Animations” e o contador de recomposição mostram exatamente qual composable está pegando fogo.

Para testar se a animação continua correta após refatorar, vale o artigo sobre testes de screenshot no Compose, que captura regressões visuais na CI.

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 fintake: 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

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 animações complexas feitas por designers no After Effects, que não dá para reproduzir declarativamente.

Qual a diferença entre AnimatedContent e Crossfade? Crossfade só faz o fade cruzado entre os estados. AnimatedContent permite definir transitionSpec customizado (slide, scale, expansão), sendo muito mais flexível.

animateItemPlacement ainda funciona? Funciona com aviso de depreciação. Em projetos novos use Modifier.animateItem(), que é a API unificada e recomendada a partir do Compose Foundation 1.7.

Animações gastam muita bateria? Animações reativas a estado (que só rodam durante a interação) têm custo desprezível. O problema é rememberInfiniteTransition rodando em segundo plano — pause-o quando o elemento sair da tela.

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.