Capítulo 19: Animações Fluidas no Compose: AnimatedVisibility, Animatable e Molas

Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose


🗺️ Mapa Conceitual da Hierarquia de APIs de Animação do Compose

flowchart TD
    subgraph HIGH_LEVEL["1. APIs de Alto Nível (Comportamento Declarativo Imediato)"]
        AV["AnimatedVisibility (Entrada / Saída de Nós: fadeIn, slideIn, expandVertically)"]
        AC["AnimatedContent (Transição Fluida entre Estados Diferentes)"]
        CF["Crossfade (Alternância Simples com Opacidade Cruzada)"]
    end

    subgraph MID_LEVEL["2. APIs de Nível Intermediário (Animação de Valores Únicos)"]
        STATE_ANIM["animate*AsState (animateFloatAsState, animateColorAsState, animateDpAsState)"]
        ACS["animateContentSize (Redimensionamento Automático de Layouts)"]
    end

    subgraph LOW_LEVEL["3. APIs de Baixo Nível (Controle Fino e Concorrência Coroutine)"]
        ANIMATABLE["Animatable<T, V> (Controle manual de snapping, stop e física de gestos)"]
        TRANS["updateTransition (Sincronização de Múltiplas Propriedades Simultâneas)"]
        INF["rememberInfiniteTransition (Pulsações, Shimmers e Loaders Contínuos)"]
    end

    subgraph SPECS["Especificações de Movimento (AnimationSpec)"]
        SPRING["spring(dampingRatio, stiffness) -> Física Natural de Molas (Padrão Ouro)"]
        TWEEN["tween(durationMillis, easing) -> Curvas Temporais Interpoladas"]
        KEYFRAMES["keyframes { ... } -> Pontos de Animação Específicos"]
    end

    HIGH_LEVEL -.-> SPECS
    MID_LEVEL -.-> SPECS
    LOW_LEVEL -.-> SPECS

    style HIGH_LEVEL fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style MID_LEVEL fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
    style LOW_LEVEL fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style SPRING fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. A Filosofia de Movimento no Jetpack Compose

No design de software móvel contemporâneo, a animação deixou de ser um mero enfeite decorativo para se tornar parte vital da ergonomia de software. Transições fluidas comunicam continuidade espacial, guiam o foco de atenção do usuário e confirmam fisicamente as interações de toque:

  1. Física Baseada em Molas (Spring) vs Interpolações Lineares: Animações temporizadas clássicas (tween com duração fixa de 300 ms) parecem robóticas e não reagem bem se o usuário interromper o movimento no meio do caminho. A especificação spring() baseia-se em leis da física clássica (rigidez e amortecimento). Se o usuário tocar ou arrastar um elemento enquanto ele ainda se move, a mola preserva a velocidade vetorial acumulada, gerando uma resposta incrivelmente natural.
  2. Abstração Declarativa com AnimatedVisibility: Em vez de manipular alpha, scale e visibility com dezenas de linhas imperativas de ViewPropertyAnimator, o Compose permite combinar transições através do operador + (ex: fadeIn() + expandVertically()), cuidando do descarte do nó na árvore de renderização apenas após o término do fade.
  3. Animatable e Concorrência: Para animações orientadas a gestos (como arrastar um card para o lado para dispensá-lo), o Animatable permite iniciar a animação dentro de uma coroutine via animateTo(). Se um novo toque ocorrer, o Compose cancela suavemente a coroutine anterior e recalcula a trajetória instantaneamente.

📊 2. Comparativo de Especificações de Animação (AnimationSpec)

Especificação Parâmetros Principais Comportamento Cinético Recomendação de Uso
spring() dampingRatio, stiffness Baseado em física de massa e amortecimento Padrão Ouro: gestos, expansão de cards, toques
tween() durationMillis, easing Duração temporal fixa com curvas Bezier Fades de tela cheia, splash screens, loaders temporizados
keyframes() durationMillis, at() Define valores intermediários exatos na linha do tempo Efeitos visuais de chacoalhar (shake) em senhas inválidas
repeatable() iterations, animation Repete uma animação por N ciclos Badges pulsantes e chamadas de atenção temporárias
infiniteRepeatable() animation, repeatMode Execução contínua sem término Shimmers de carregamento (Skeleton loaders)

💻 3. Laboratório Prático: Card Interativo com Efeito Shimmer e Mola

O exemplo abaixo demonstra a criação de uma transição fluida combinando física de molas (spring), redimensionamento automático de layout e efeito de rotação de chevron:

package br.com.portal.mobile.android.cap19

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp

@Composable
fun CardExpansivelAnimado(
    titulo: String,
    conteudoOculto: String,
    modifier: Modifier = Modifier
) {
    var expandido by remember { mutableStateOf(false) }

    // 1. Animação de Rotação com Mola Levemente Elástica
    val anguloRotacao by animateFloatAsState(
        targetValue = if (expandido) 180f else 0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ),
        label = "RotacaoChevron"
    )

    // 2. Animação Cromática do Fundo do Card
    val corFundo by animateColorAsState(
        targetValue = if (expandido) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
        animationSpec = spring(stiffness = Spring.StiffnessLow),
        label = "CorFundoAnimada"
    )

    Card(
        modifier = modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(12.dp))
            .clickable { expandido = !expandido }
            .animateContentSize( // Ajusta altura suavemente quando o texto se expande
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioNoBouncy,
                    stiffness = Spring.StiffnessLow
                )
            ),
        colors = CardDefaults.cardColors(containerColor = corFundo)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = titulo,
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold
                )

                Icon(
                    imageVector = Icons.Default.KeyboardArrowDown,
                    contentDescription = null,
                    modifier = Modifier.rotate(anguloRotacao)
                )
            }

            // 3. Entrada e Saída Suave do Conteúdo Condicional
            AnimatedVisibility(
                visible = expandido,
                enter = fadeIn() + expandVertically(),
                exit = fadeOut() + shrinkVertically()
            ) {
                Column {
                    Spacer(modifier = Modifier.height(12.dp))
                    Text(
                        text = conteudoOculto,
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }
            }
        }
    }
}

⚠️ 4. Armadilhas Comuns em Animações Mobile

  1. Recompor a Árvore Inteira em Animações Contínuas: Se você passar um valor animado de 60 FPS diretamente para um Composable de texto ou layout, todo o nó será recomposto a cada frame. Utilize sempre modificadores na fase de desenho como Modifier.graphicsLayer { rotationZ = angulo } ou Modifier.drawBehind, que operam direto na GPU sem recompor a árvore.
  2. Ignorar as Opções de Acessibilidade do Sistema: Usuários com labirintite ou sensibilidade motora frequentemente ativam a opção “Remover Animações” nas configurações de Acessibilidade do Android. Verifique se seu design respeita essas preferências sem quebrar a lógica de navegação.
  3. Animações Demasiadamente Longas: Transições que duram mais de 400 milissegundos passam a sensação de que o aplicativo é lento e travado. Para transições de interface rotineiras, mantenha durações entre 150 ms e 300 ms.