Introdução ao Sistema de Animação no Jetpack Compose
O Jetpack Compose oferece um conjunto abrangente de ferramentas para criar animações em interfaces de usuário. Este guia explora as API's disponíveis, desde funções de baixo nível até componentes de alto nível, com foco em práticas e exemplos práticos.
API's de Baixo Nível para Animações
As API's de baixo nível proporcionam controle granular sobre o comportamento das animações. Elas são a base para as API's de alto nível e permitem personalizações avançadas.
Uso de animate\*AsState
Esta família de funções simplifica a criação de animações entre valores inicial e final. O sistema gerencia automaticamente o estado e a interpolação.
@Composable
fun CaixaAnimada() {
var expandido by remember { mutableStateOf(false) }
val tamanho by animateDpAsState(
targetValue = if (expandido) 200.dp else 50.dp,
label = "tamanhoCaixa"
)
Box(
modifier = Modifier
.background(Color.LightGray)
.size(tamanho)
.clickable { expandido = !expandido }
)
}
Ao definir um targetValue, a função calcula automaticamente a trajetória da animação. O parâmetro animationSpec permite personalizar a curva e a duração. Internamente, animate\*AsState utiliza Animatable para gerenciar o estado.
Controle com Animatable
Para cenários que exigem controle explícito do estado inicial e atualizações sob demanda, Animatable oferece maior flexibilidade. As animações devem ser executadas dentro de uma coroutine.
@Composable
fun CaixaComAnimatable() {
var modo by remember { mutableStateOf(false) }
val dimensao = remember { Animatable(50.dp, Dp.VectorConverter) }
val cor = remember { Animatable(Color.Blue) }
LaunchedEffect(modo) {
launch { dimensao.animateTo(if (modo) 200.dp else 50.dp) }
launch { cor.animateTo(if (modo) Color.Red else Color.Blue) }
}
Box(
modifier = Modifier
.background(cor.value)
.size(dimensao.value)
.clickable { modo = !modo }
)
}
Note que múltiplas animações podem ser executadas em paralelo usando launch separados. Os tipos primitivos como Color e Float possuem conversores embutidos, dispensando a necessidade de um TwoWayConverter explícito.
Sincronização com Transition
Quando múltiplas propriedades devem animar sincronizadamente baseadas em um único estado, Transition é a solução ideal. Ela garante que todas as animações comecem e terminem juntas.
@Composable
fun CaixaComTransition() {
var ativo by remember { mutableStateOf(false) }
val transicao = updateTransition(targetState = ativo, label = "estado")
val tamanho by transicao.animateDp(label = "tamanho") { estado ->
if (estado) 200.dp else 50.dp
}
val cor by transicao.animateColor(label = "cor") { estado ->
if (estado) Color.Red else Color.Blue
}
Box(
modifier = Modifier
.background(cor)
.size(tamanho)
.clickable { ativo = !ativo }
)
}
A abordagem baseada em estado (Transition) difere da abordagem baseada em valor (Animatable), oferecendo melhor oragnização para animações complexas que dependem de uma mesma condição.
Animações Infinitas com rememberInfiniteTransition
Para animações que devem repetir continuamente até que o componente seja descartado, utilize rememberInfiniteTransition. O ciclo é interrompido automaticamente quando o composable sai da composição.
@Composable
fun CaixaInfinita() {
val transicaoInfinita = rememberInfiniteTransition(label = "loop")
val cor by transicaoInfinita.animateColor(
initialValue = Color.Blue,
targetValue = Color.Red,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "corInfinita"
)
Box(modifier = Modifier.background(cor).size(100.dp))
}
Observe que a especificação da animação (animationSpec) deve ser do tipo InfiniteRepeatableSpec para garantir o comportamneto de repetição contínua.
Depuração de Animações no Android Studio
O Android Studio oferece suporte nativo para visualizar e depurar animações durante o desenvolvimento. Ao adicionar a anotação @Preview a um composable, é possível inspecionar e ajustar parâmetros diretamente no painel de preview.
Para animações baseadas em Transition, a ferramenta de preview fornece controles avançados para reprodução, velocidade e inspeção de estados intermediários.
Especificação de Animações (AnimationSpec)
O AnimationSpec controla como uma animação se comporta ao longo do tempo. Diversas implementações pré-definidas cobrem diferentes necessidades.
Animação Elástica (SpringSpec)
Simula o comportamento de uma mola física. Os parâmetros dampingRatio e stiffness controlam a suavidade e a velocidade da animação.
@Composable
fun Elastico() {
var mudar by remember { mutableStateOf(false) }
val tamanho by animateDpAsState(
targetValue = if (mudar) 200.dp else 50.dp,
animationSpec = spring(dampingRatio = 0.3f, stiffness = 300f)
)
Box(modifier = Modifier.size(tamanho).clickable { mudar = !mudar })
}
Animação com Duração Fixa (TweenSpec)
Especifica uma duração total e permite aplicar curvas de aceleração (Easing). É ideal para transições com tempo previsível.
@Composable
fun TweenExemplo() {
var alternar by remember { mutableStateOf(false) }
val tamanho by animateDpAsState(
targetValue = if (alternar) 200.dp else 50.dp,
animationSpec = tween(
durationMillis = 500,
delayMillis = 100,
easing = CubicBezierEasing(0.1f, 0.8f, 0.2f, 1.0f)
)
)
Box(modifier = Modifier.size(tamanho).clickable { alternar = !alternar })
}
A classe CubicBezierEasing permite definir curvas personalizadas, enquanto constantes como LinearEasing oferecem comportamentos predefinidos.
Animação por Quadros-Chave (KeyframesSpec)
Permite definir valores em pontos específicos da linha do tempo, oferecendo controle preciso sobre a trajetória da animação.
@Composable
fun QuadrosChave() {
var mudar by remember { mutableStateOf(false) }
val tamanho by animateDpAsState(
targetValue = if (mudar) 200.dp else 50.dp,
animationSpec = keyframes {
durationMillis = 1500
50.dp at 0
100.dp at 500 with LinearEasing
150.dp at 1000
200.dp at 1500
}
)
Box(modifier = Modifier.size(tamanho).clickable { mudar = !mudar })
}
Outras Especificações
snap(): Transição instantânea sem interpolação.repeatable(): Repete uma animação baseada em tempo por um número definido de vezes.infiniteRepeatable(): Repete uma animação indefinidamente.
Conversores para Tipos Personalizados (TwoWayConverter)
O TwoWayConverter permite que tipos de dados arbitrários sejam animados, convertendo-os para um AnimationVector interno. O framework já fornece conversores para tipos comuns como Dp, Color e Offset.
data class Ponto(val x: Float, val y: Float)
val PontoConverter = TwoWayConverter<ponto animationvector2d="">(
convertToVector = { ponto -> AnimationVector2D(ponto.x, ponto.y) },
convertFromVector = { vetor -> Ponto(vetor.v1, vetor.v2) }
)
@Composable
fun AnimacaoPersonalizada(destino: Ponto) {
val pontoAtual by animateValueAsState(
targetValue = destino,
typeConverter = PontoConverter,
label = "ponto"
)
// Use pontoAtual para posicionamento
}</ponto>
API's de Alto Nível para Cenários Comuns
Estas API's encapsulam a complexidade das API's de baixo nível, oferecendo soluções prontas parra situações frequentes.
Visibilidade Animada (AnimatedVisibility)
Gerencia a entrada e saída de composable com efeitos de transição configuráveis.
@Composable
fun PainelAnimado() {
var visivel by remember { mutableStateOf(true) }
Column {
AnimatedVisibility(
visible = visivel,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
Box(
modifier = Modifier
.background(Color.Yellow)
.size(150.dp)
)
}
Button(onClick = { visivel = !visivel }) {
Text("Alternar")
}
}
}
Para monitorar o estado da animação, utilize MutableTransitionState. Isso permite reagir a eventos como início e fim das transições.
Conteúdo Animado (AnimatedContent)
Transiciona suavemente entre diferentes composable baseados em um estado chave.
@Composable
fun AlternadorDeConteudo() {
var tipo by remember { mutableStateOf("texto") }
Column {
Button(onClick = { tipo = if (tipo == "texto") "ícone" else "texto" }) {
Text("Mudar Conteúdo")
}
AnimatedContent(
targetState = tipo,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { tipoAtual ->
when (tipoAtual) {
"texto" -> Text("Conteúdo Textual", style = MaterialTheme.typography.h6)
"ícone" -> Icon(Icons.Default.Star, contentDescription = "Estrela")
}
}
}
}
O SizeTransform pode ser adicionado para animar mudanças de tamanho durante a transição.
Modificador animateContentSize
Aplica animações automáticas quando as dimensões de um composable mudam.
@Composable
fun CaixaExpansivel() {
var expandido by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.background(Color.Green)
.animateContentSize()
.padding(16.dp)
.clickable { expandido = !expandido }
) {
Text(
text = if (expandido) "Texto muito longo que pode ser expandido ou recolhido." else "Curto",
maxLines = if (expandido) Int.MAX_VALUE else 1
)
}
}
Posicione este modificador antes de modificadores de tamanho na cadeia para garantir o funcionamento correto.
Cenários Avançados e Integrações
Animações em Listas (LazyColumn)
Use animateItemPlacement() para adicionar animações de reordenação em listas carregadas sob demanda.
LazyColumn {
itemsIndexed(
items = lista,
key = { _, item -> item.id }
) { _, item ->
ItemDeLista(
item = item,
modifier = Modifier.animateItemPlacement(
animationSpec = tween(durationMillis = 300)
)
)
}
}
Animações em Navegação (navigation-compose)
Configure transições para destinos de navegação usando os parâmetros enterTransition e exitTransition na função composable.
Animações com Vetores Animados e Lottie
Para animações vetoriais complexas, integre recursos AnimatedVectorDrawable ou utilize a biblioteca Lottie para reproduzir animações exportadas do After Effects.
@Composable
fun CarregamentoLottie() {
val composicao by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.carregando))
val progresso by animateLottieCompositionAsState(composicao)
LottieAnimation(
composition = composicao,
progress = { progresso },
modifier = Modifier.size(100.dp)
)
}