Guia rápido sobre animações no Compose

O Compose tem muitos mecanismos de animação integrados, e pode ser difícil saber qual escolher. Confira a seguir uma lista de casos de uso comuns de animação. Para mais informações sobre o conjunto completo de opções de API disponíveis, leia a documentação de animação do Compose.

Animar propriedades comuns de composição

O Compose oferece APIs convenientes que permitem resolver muitos casos de uso comuns de animação. Nesta seção, mostramos como animar propriedades comuns de um elemento combinável.

Animar aparecimento / desaparecimento

Elemento combinável verde sendo mostrado e ocultado
Figura 1. Como animar o aparecimento e o desaparecimento de um item em uma coluna

Use AnimatedVisibility para ocultar ou mostrar um elemento combinável. Os filhos dentro de AnimatedVisibility podem usar Modifier.animateEnterExit() para a própria transição de entrada ou saída.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

Os parâmetros de entrada e saída de AnimatedVisibility permitem configurar como um elemento combinável se comporta quando aparece e desaparece. Leia a documentação completa para mais informações.

Outra opção para animar a visibilidade de um elemento combinável é animar o Alfa ao longo do tempo usando animateFloatAsState:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

No entanto, mudar o Alfa tem a ressalva de que o elemento combinável permanece na composição e continua ocupando o espaço em que está disposto. Isso pode fazer com que os leitores de tela e outros mecanismos de acessibilidade ainda considerem o item na tela. Por outro lado, AnimatedVisibility remove o item da composição.

Como animar o alfa de um elemento combinável
Figura 2. Como animar o Alfa de um elemento combinável

Animar cor do plano de fundo

Elemento que pode ser composto com a cor do plano de fundo mudando ao longo do tempo como uma animação, em que as cores se misturam.
Figura 3. Como animar a cor do plano de fundo do elemento combinável

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

Essa opção tem um desempenho melhor do que o uso de Modifier.background(). Modifier.background() é aceitável para uma configuração de cor única, mas, ao animar uma cor ao longo do tempo, isso pode causar mais recomposições do que o necessário.

Para animar infinitamente a cor do plano de fundo, consulte a seção Como repetir uma animação.

Animar o tamanho de um elemento combinável

Elemento combinável verde animando a mudança de tamanho de forma suave.
Figura 4. Função de composição que anima suavemente entre um tamanho pequeno e um maior

O Compose permite animar o tamanho de elementos combináveis de algumas maneiras diferentes. Use animateContentSize() para animações entre mudanças de tamanho de elementos combináveis.

Por exemplo, se você tiver uma caixa com texto que pode se expandir de uma para várias linhas, use Modifier.animateContentSize() para uma transição mais suave:

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

Também é possível usar AnimatedContent com um SizeTransform para descrever como as mudanças de tamanho devem ocorrer.

Animar a posição do elemento combinável

Elemento combinável verde com animação suave para baixo e para a direita
Figura 5. Elemento combinável se movendo por um deslocamento

Para animar a posição de um elemento combinável, use Modifier.offset{ } combinado com animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

Se você quiser garantir que os elementos combináveis não sejam desenhados sobre ou sob outros elementos ao animar a posição ou o tamanho, use Modifier.layout{ }. Esse modificador propaga mudanças de tamanho e posição para o pai, o que afeta outros filhos.

Por exemplo, se você estiver movendo um Box dentro de um Column e os outros filhos precisarem se mover quando o Box se mover, inclua as informações de deslocamento com Modifier.layout{ } da seguinte maneira:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

Duas caixas,com a segunda animando a posição X, Y e a terceira respondendo ao se mover pelo valor Y também.
Figura 6. Como animar com Modifier.layout{ }

Animar o padding de um elemento combinável

Elemento combinável verde que fica menor e maior ao clicar, com padding animado
Figura 7. Elemento combinável com padding animado

Para animar o padding de um elemento combinável, use animateDpAsState combinado com Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

Animar a elevação de um elemento combinável

Figura 8. Animação de elevação do elemento combinável ao clicar

Para animar a elevação de um elemento combinável, use animateDpAsState combinado com Modifier.graphicsLayer{ }. Para mudanças de elevação únicas, use Modifier.shadow(). Se você estiver animando a sombra, usar o modificador Modifier.graphicsLayer{ } é a opção com melhor desempenho.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

Como alternativa, use o elemento combinável Card e defina a propriedade de elevação como valores diferentes por estado.

Animar escala, translação ou rotação de texto

Texto que pode ser composto dizendo "Hello" e que se anima entre um tamanho pequeno e um maior.
Figura 9. Texto com animação suave entre dois tamanhos

Ao animar a escala, a translação ou a rotação do texto, defina o parâmetro textMotion em TextStyle como TextMotion.Animated. Isso garante transições mais suaves entre as animações de texto. Use Modifier.graphicsLayer{ } para traduzir, girar ou dimensionar o texto.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Animar a cor do texto

As palavras "Hello Compose" animando a cor entre verde e azul
Figura 10. Exemplo de animação da cor do texto

Para animar a cor do texto, use a lambda color no elemento combinável BasicText:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

Alternar entre diferentes tipos de conteúdo

Tela verde com a mensagem "Carregando", tela azul com a mensagem "Carregado" e tela branca com a mensagem "Erro". Os diferentes elementos combináveis são iterados com uma animação simples.
Figura 11. Como usar o AnimatedContent para animar mudanças entre diferentes elementos que podem ser compostos (em câmera lenta)

Use AnimatedContent para animar entre diferentes elementos combináveis. Se você quiser apenas um esmaecimento padrão entre eles, use Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

AnimatedContent pode ser personalizado para mostrar muitos tipos diferentes de transições de entrada e saída. Para mais informações, leia a documentação sobre o AnimatedContent ou esta postagem do blog sobre o AnimatedContent.

Animar ao navegar para destinos diferentes

Dois elementos combináveis, um verde com o texto "Landing" e um azul com o texto "Detail", animados pelo deslizamento do elemento combinável de detalhes sobre o de destino.
Figura 12. Animação entre elementos combináveis usando o navigation-compose

Para animar transições entre elementos combináveis ao usar o artefato navigation-compose, especifique enterTransition e exitTransition em um elemento combinável. Também é possível definir a animação padrão a ser usada para todos os destinos no NavHost de nível superior:

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

Há muitos tipos diferentes de transições de entrada e saída que aplicam efeitos diferentes ao conteúdo de entrada e saída. Consulte a documentação para saber mais.

Repetir uma animação

Um fundo verde que se transforma em um fundo azul, infinitamente, ao animar entre as duas cores.
Figura 13. Cor do plano de fundo animada entre dois valores, infinitamente

Use rememberInfiniteTransition com um infiniteRepeatable animationSpec para repetir continuamente a animação. Mude RepeatModes para especificar como ele deve ir e voltar.

Use repeatable para repetir um número definido de vezes.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

Iniciar uma animação ao iniciar um elemento combinável

LaunchedEffect é executado quando um elemento combinável entra na composição. Ele inicia uma animação ao iniciar um elemento combinável. Você pode usar isso para impulsionar a mudança de estado da animação. Usar Animatable com o método animateTo para iniciar a animação na inicialização:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Criar animações sequenciais

Quatro círculos com setas verdes que se movem entre eles, uma após a outra.
Figura 14. Diagrama que indica como uma animação sequencial avança, uma por uma.

Use as APIs de corrotina Animatable para realizar animações sequenciais ou simultâneas. Chamar animateTo no Animatable um após o outro faz com que cada animação aguarde a conclusão das anteriores antes de continuar. Isso ocorre porque ela é uma função de suspensão.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

Criar animações simultâneas

Três círculos com setas verdes que se movem em direção a cada um deles, todos ao mesmo tempo.
Figura 15. Diagrama que indica como as animações simultâneas progridem, todas ao mesmo tempo.

Use as APIs de corrotina (Animatable#animateTo() ou animate) ou a API Transition para criar animações simultâneas. Se você usar várias funções de inicialização em um contexto de corrotina, as animações serão iniciadas ao mesmo tempo:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

Você pode usar a API updateTransition para usar o mesmo estado para gerar muitas animações de propriedades diferentes ao mesmo tempo. O exemplo a seguir anima duas propriedades controladas por uma mudança de estado, rect e borderWidth:

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "transition")

val rect by transition.animateRect(label = "rect") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "borderWidth") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Otimizar a performance da animação

As animações no Compose podem causar problemas de desempenho. Isso se deve à natureza do que é uma animação: mover ou mudar pixels na tela rapidamente, frame a frame, para criar a ilusão de movimento.

Considere as diferentes fases do Compose: composição, layout e desenho. Se a animação mudar a fase de layout, todos os elementos de composição afetados precisarão ser redefinidos e redesenhados. Se a animação ocorrer na fase de desenho, ela terá, por padrão, um desempenho melhor do que se fosse executada na fase de layout, já que teria menos trabalho a fazer no geral.

Para garantir que o app faça o mínimo possível durante a animação, escolha a versão lambda de um Modifier sempre que possível. Isso pula a recomposição e executa a animação fora da fase de composição. Caso contrário, use Modifier.graphicsLayer{ }, já que esse modificador sempre é executado na fase de desenho. Para mais informações, consulte a seção adiar leituras na documentação de desempenho.

Mudar o tempo da animação

Por padrão, o Compose usa animações de mola na maioria das animações. As molas, ou animações baseadas em física, parecem mais naturais. Elas também são interrompíveis, já que consideram a velocidade atual do objeto, em vez de um tempo fixo. Se você quiser substituir o padrão, todas as APIs de animação demonstradas anteriormente podem definir um animationSpec para personalizar como uma animação é executada, seja por uma determinada duração ou com mais saltos.

Confira um resumo das diferentes opções de animationSpec:

  • spring: animação baseada em física, o padrão para todas as animações. É possível mudar a rigidez ou a dampingRatio para alcançar uma aparência diferente para a animação.
  • tween (abreviação de between): animação baseada em duração, que anima entre dois valores com uma função Easing.
  • keyframes: especificação para definir valores em determinados pontos-chave de uma animação.
  • repeatable: especificação baseada em duração que é executada um determinado número de vezes, especificado por RepeatMode.
  • infiniteRepeatable: especificação baseada em duração que é executada para sempre.
  • snap: ajusta instantaneamente ao valor final sem nenhuma animação.
Duas animações que demonstram um conjunto de especificações não definido em comparação com um conjunto de especificações Spring personalizado.
Figura 16. Sem conjunto de especificações x conjunto de especificações do Spring personalizado

Leia a documentação completa para mais informações sobre animationSpecs.

Outros recursos

Para mais exemplos de animações divertidas no Compose, confira os seguintes recursos: