Jetpack Compose: Animações Avançadas Passo a Passo
Guia completo de animações avançadas no Compose: transições, AnimatedVisibility, Animatable, gestos e desempenho para interfaces Android fluidas.

Animações transformam um aplicativo funcional em uma experiência memorável. Jetpack Compose oferece uma API declarativa poderosa que simplifica significativamente a criação de interfaces fluidas. Este guia explora técnicas avançadas para construir animações performáticas e fáceis de manter.
Este tutorial pressupõe familiaridade com os fundamentos do Compose (recomposição, estado, modifiers). Para revisar o básico, vale consultar primeiro o guia de perguntas de entrevista sobre Jetpack Compose.
Fundamentos da API de Animações no Compose
O Compose disponibiliza vários níveis de API para animações. A escolha depende do nível de controle desejado e da complexidade da animação.
A API se divide em três categorias principais: animações de alto nível (AnimatedVisibility, AnimatedContent), animações baseadas em estado (animate*AsState) e animações de baixo nível (Animatable, Transition).
// Overview of the three animation API levels
@Composable
fun AnimationApiOverview() {
// High level: simple predefined animations
AnimatedVisibility(visible = isVisible) {
Text("Animated content")
}
// Intermediate level: state-driven animation
val alpha by animateFloatAsState(
targetValue = if (isSelected) 1f else 0.5f,
label = "alpha"
)
// Low level: full control over animation
val animatable = remember { Animatable(0f) }
LaunchedEffect(targetValue) {
animatable.animateTo(targetValue)
}
}Escolher o nível certo de API é crucial para manter o código legível sem perder a flexibilidade necessária.
AnimatedVisibility: Animações Elegantes de Entrada e Saída
AnimatedVisibility é o ponto de entrada ideal para animar a aparição e o desaparecimento de elementos. Essa API gerencia automaticamente a composição e a decomposição do conteúdo.
Os parâmetros enter e exit aceitam combinações de transições que definem o comportamento da animação. Essas transições podem ser combinadas pelo operador +.
@Composable
fun ExpandableCard(
title: String,
content: String,
modifier: Modifier = Modifier
) {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = modifier.clickable { isExpanded = !isExpanded }
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Icon(
imageVector = if (isExpanded) Icons.Default.ExpandLess
else Icons.Default.ExpandMore,
contentDescription = null
)
}
// Expansion animation with fade + slide
AnimatedVisibility(
visible = isExpanded,
enter = fadeIn(animationSpec = tween(300)) +
expandVertically(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200)) +
shrinkVertically(animationSpec = tween(200))
) {
Text(
text = content,
modifier = Modifier.padding(top = 12.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}O conteúdo dentro de AnimatedVisibility só é composto quando visible = true, otimizando o desempenho em listas com muitos itens expansíveis.
As transições disponíveis incluem fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut. Elas podem ser combinadas livremente para criar efeitos personalizados.
animate*AsState: Animações Orientadas por Estado
A família de funções animate*AsState anima automaticamente as mudanças de valores primitivos. É a abordagem mais idiomática para animações simples no Compose.
Cada tipo de dado tem sua função dedicada: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState, etc.
@Composable
fun InteractiveButton(
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
// Animated background color
val backgroundColor by animateColorAsState(
targetValue = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 250),
label = "backgroundColor"
)
// Animated elevation
val elevation by animateDpAsState(
targetValue = if (isSelected) 8.dp else 2.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "elevation"
)
// Animated text size
val textSize by animateFloatAsState(
targetValue = if (isSelected) 18f else 14f,
label = "textSize"
)
Surface(
modifier = modifier.clickable(onClick = onClick),
color = backgroundColor,
shadowElevation = elevation,
shape = RoundedCornerShape(12.dp)
) {
Text(
text = if (isSelected) "Selected" else "Select",
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
fontSize = textSize.sp
)
}
}O parâmetro animationSpec controla o comportamento temporal da animação. Os dois specs mais utilizados são tween (duração fixa com easing) e spring (física realista com salto).
Transition: Orquestrando Múltiplas Animações
Quando várias propriedades precisam ser animadas de forma coordenada, updateTransition fornece controle centralizado. Essa API garante que todas as animações permaneçam sincronizadas.
O padrão envolve definir um estado enum e, em seguida, criar animações para cada propriedade que dependa desse estado.
// Card state: defines visual behavior
enum class CardState { Collapsed, Expanded, Selected }
@Composable
fun AnimatedStateCard(
cardState: CardState,
modifier: Modifier = Modifier
) {
// Central transition coordinating all animations
val transition = updateTransition(
targetState = cardState,
label = "cardTransition"
)
// Card height based on state
val cardHeight by transition.animateDp(
transitionSpec = { spring(stiffness = Spring.StiffnessLow) },
label = "height"
) { state ->
when (state) {
CardState.Collapsed -> 80.dp
CardState.Expanded -> 200.dp
CardState.Selected -> 160.dp
}
}
// Border color based on state
val borderColor by transition.animateColor(
transitionSpec = { tween(300) },
label = "borderColor"
) { state ->
when (state) {
CardState.Collapsed -> Color.Transparent
CardState.Expanded -> MaterialTheme.colorScheme.outline
CardState.Selected -> MaterialTheme.colorScheme.primary
}
}
// Corner radius based on state
val cornerRadius by transition.animateDp(
label = "cornerRadius"
) { state ->
when (state) {
CardState.Collapsed -> 8.dp
CardState.Expanded -> 16.dp
CardState.Selected -> 24.dp
}
}
Card(
modifier = modifier
.height(cardHeight)
.border(2.dp, borderColor, RoundedCornerShape(cornerRadius)),
shape = RoundedCornerShape(cornerRadius)
) {
// Card content
}
}Pronto para mandar bem nas entrevistas de Android?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Animatable: Controle Total da Animação
Animatable é a API de baixo nível que oferece controle programático completo. Essa abordagem é necessária para animações interrompíveis, gestos ou cenários complexos.
Diferente de animate*AsState, Animatable permite parar, reverter ou modificar uma animação em andamento sem esperar que ela termine.
@Composable
fun SwipeableCard(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
// Horizontal offset controlled by Animatable
val offsetX = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
// Swipe threshold to trigger dismissal
val dismissThreshold = 300f
Box(
modifier = modifier
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
scope.launch {
if (abs(offsetX.value) > dismissThreshold) {
// Animate out then callback
val target = if (offsetX.value > 0) 1000f else -1000f
offsetX.animateTo(
targetValue = target,
animationSpec = tween(200)
)
onDismiss()
} else {
// Return to initial position with spring
offsetX.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy
)
)
}
}
},
onHorizontalDrag = { _, dragAmount ->
scope.launch {
// snapTo for instant finger tracking
offsetX.snapTo(offsetX.value + dragAmount)
}
}
)
}
) {
content()
}
}Os métodos chave de Animatable são animateTo() (animar até um alvo), snapTo() (mudança instantânea) e stop() (interrupção).
AnimatedContent: Transições de Conteúdo
AnimatedContent anima as transições entre conteúdos diferentes. Essa API é perfeita para mudanças de estado que modificam totalmente a interface exibida.
A chave targetState determina quando uma transição deve ocorrer. O transitionSpec define como o conteúdo que sai e o que entra interagem.
@Composable
fun CounterWithAnimation(
count: Int,
modifier: Modifier = Modifier
) {
AnimatedContent(
targetState = count,
modifier = modifier,
transitionSpec = {
// Determine animation direction
val direction = if (targetState > initialState) {
// New number enters from top
slideInVertically { height -> -height } + fadeIn() togetherWith
slideOutVertically { height -> height } + fadeOut()
} else {
// New number enters from bottom
slideInVertically { height -> height } + fadeIn() togetherWith
slideOutVertically { height -> -height } + fadeOut()
}
direction.using(SizeTransform(clip = false))
},
label = "counter"
) { targetCount ->
Text(
text = "$targetCount",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold
)
}
}O conteúdo dentro de AnimatedContent é recomposto a cada mudança de targetState. Para conteúdo complexo, vale a pena memorizar elementos custosos ou usar uma estratégia adequada de chaves.
Animações Infinitas com rememberInfiniteTransition
Para animações em loop (indicadores de carregamento, efeitos pulsantes), rememberInfiniteTransition fornece uma API dedicada que dispensa o gerenciamento manual do ciclo.
@Composable
fun PulsingDot(
color: Color = MaterialTheme.colorScheme.primary,
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
// Looping scale animation
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
// Synchronized opacity animation
val alpha by infiniteTransition.animateFloat(
initialValue = 0.5f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(600, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Box(
modifier = modifier
.size(24.dp)
.scale(scale)
.alpha(alpha)
.background(color = color, shape = CircleShape)
)
}Animações de Listas com LazyColumn
As animações de itens de lista exigem atenção especial. O modifier animateItem() (antes animateItemPlacement) anima automaticamente as reordenações.
@Composable
fun AnimatedTaskList(
tasks: List<Task>,
onToggle: (Task) -> Unit,
onDelete: (Task) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = tasks,
key = { it.id } // Stable key required for animateItem
) { task ->
var isVisible by remember { mutableStateOf(true) }
// Exit animation before deletion
AnimatedVisibility(
visible = isVisible,
exit = shrinkVertically() + fadeOut()
) {
TaskItem(
task = task,
onToggle = { onToggle(task) },
onDelete = {
isVisible = false
// Delay to let animation complete
},
modifier = Modifier.animateItem(
fadeInSpec = tween(300),
fadeOutSpec = tween(300),
placementSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
)
}
// Trigger deletion after animation
LaunchedEffect(isVisible) {
if (!isVisible) {
delay(300)
onDelete(task)
}
}
}
}
}Sem um parâmetro key estável, animateItem não consegue rastrear os itens entre recomposições. Vale usar um ID único em vez do índice da lista.
Animações com Canvas
Para efeitos visuais personalizados, combinar Canvas com valores animados oferece flexibilidade total.
@Composable
fun AnimatedProgressRing(
progress: Float, // 0f to 1f
modifier: Modifier = Modifier
) {
// Progress animation with spring for natural feel
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessVeryLow
),
label = "progress"
)
// Continuous rotation animation
val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing)
),
label = "rotation"
)
val primaryColor = MaterialTheme.colorScheme.primary
val trackColor = MaterialTheme.colorScheme.surfaceVariant
Canvas(
modifier = modifier
.size(120.dp)
.rotate(rotation)
) {
val strokeWidth = 12.dp.toPx()
val radius = (size.minDimension - strokeWidth) / 2
// Background circle (track)
drawCircle(
color = trackColor,
radius = radius,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
// Animated progress arc
drawArc(
color = primaryColor,
startAngle = -90f,
sweepAngle = animatedProgress * 360f,
useCenter = false,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round),
topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
size = Size(radius * 2, radius * 2)
)
}
}Pronto para mandar bem nas entrevistas de Android?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Otimização do Desempenho de Animações
Animações mal otimizadas podem causar jank e drenar a bateria. Estas são as boas práticas para manter 60 FPS.
A primeira regra é evitar alocações durante a animação. Vale usar graphicsLayer em vez de modifiers que disparam recomposições.
@Composable
fun OptimizedAnimatedCard(
isExpanded: Boolean,
modifier: Modifier = Modifier
) {
val scale by animateFloatAsState(
targetValue = if (isExpanded) 1.1f else 1f,
label = "scale"
)
val alpha by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0.8f,
label = "alpha"
)
Card(
modifier = modifier
// ✅ graphicsLayer: GPU modifications without recomposition
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
}
// ❌ Avoid: .scale(scale).alpha(alpha)
// These modifiers trigger recompositions
) {
Text("Card content")
}
}
// Example with remembered lambda to avoid allocations
@Composable
fun OptimizedClickableItem(
onClick: () -> Unit,
content: @Composable () -> Unit
) {
// ✅ Stable remembered lambda
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick
)
) {
content()
}
}O segundo ponto crítico diz respeito às animações em listas. Vale limitar o número de animações simultâneas e usar derivedStateOf para cálculos derivados.
@Composable
fun PerformantAnimatedList(
items: List<Item>,
modifier: Modifier = Modifier
) {
// Calculate once whether the list is empty
val isEmpty by remember {
derivedStateOf { items.isEmpty() }
}
LazyColumn(modifier = modifier) {
items(
items = items,
key = { it.id }
) { item ->
// Lightweight animation only on initial appearance
var hasAppeared by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
hasAppeared = true
}
val alpha by animateFloatAsState(
targetValue = if (hasAppeared) 1f else 0f,
animationSpec = tween(200),
label = "itemAlpha"
)
ItemCard(
item = item,
modifier = Modifier.graphicsLayer { this.alpha = alpha }
)
}
}
}Conclusão
As animações no Jetpack Compose oferecem equilíbrio entre facilidade de uso e controle avançado. Estes são os pontos chave a reter:
- ✅ Escolher o nível de API adequado conforme a complexidade (AnimatedVisibility → animate*AsState → Animatable)
- ✅ Usar
updateTransitionpara coordenar várias animações relacionadas - ✅ Preferir
springpara animações naturais etweenpara durações precisas - ✅ Sempre fornecer um parâmetro
keyestável para animações de lista - ✅ Otimizar com
graphicsLayerpara evitar recomposições desnecessárias - ✅ Testar as animações em dispositivos reais para validar o desempenho
Dominar as animações do Compose distingue os aplicativos Android profissionais. Essas técnicas, combinadas com atenção ao desempenho, permitem criar experiências de usuário fluidas e envolventes.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

As 20 perguntas mais frequentes sobre Jetpack Compose em entrevistas (2026)
As 20 perguntas de entrevista sobre Jetpack Compose mais comuns: recomposição, gerenciamento de estado, navegação, performance e padrões de arquitetura.

Kotlin 2.3 para Android: Desestruturação por Nome, KMP e Perguntas de Entrevista 2026
Perguntas de entrevista sobre Kotlin 2.3 para desenvolvedores Android em 2026. Desestruturação por nome, KMP, parâmetros de contexto, Flow e coroutines com exemplos de código.

MVVM vs MVI no Android: Qual Arquitetura Escolher em 2026?
Comparação aprofundada entre MVVM e MVI no Android: vantagens, limitações, casos de uso e um guia prático para escolher a arquitetura certa em 2026.