Animação

O Jetpack Compose fornece APIs avançadas e extensíveis que facilitam a implementação de várias animações na IU do app. Este documento descreve como usar essas APIs e qual API usar dependendo do caso de animação.

Visão geral

As animações são essenciais em um app para dispositivos móveis moderno, porque proporcionam uma experiência simples e compreensível para o usuário. Muitas APIs de animação do Jetpack Compose estão disponíveis como funções que podem ser compostas, assim como layouts e outros elementos de IU. Elas têm suporte de APIs de nível mais baixo criadas com funções de suspensão de corrotina do Kotlin. Este guia começa com as APIs de nível alto, que são úteis em muitos cenários práticos, e segue para explicar as APIs de nível baixo, que proporcionam maior controle e personalização.

O gráfico abaixo ajuda você a decidir qual API usar ao implementar sua animação.

  • Se estiver animando uma mudança de conteúdo no layout:
    • Se você estiver animando o aparecimento e o desaparecimento:
      • Use AnimationVisibility.
    • Trocando conteúdo com base no estado:
      • Para conteúdo com fading cruzado:
        • Use Crossfade.
      • Caso contrário, use AnimatedContent.
    • Caso contrário, use Modifier.contentSize.
  • Se a animação for baseada em estado:
    • Se a animação acontecer durante a composição:
      • Se a animação for infinita:
        • use a função rememberInfiniteTransition.
      • Se estiver animando vários valores simultaneamente:
        • use a função updateTransition.
      • Caso contrário, use animate*AsState.
  • Se quiser ter um controle preciso sobre a duração da animação:
    • Use Animation.
  • Se a animação for a única fonte da verdade:
    • Use Animatable.
  • Caso contrário, use AnimationState ou animate.

Fluxograma descrevendo a árvore de decisões para escolha da API de animação adequada

APIs de animação de nível alto

O Compose oferece APIs de animação de nível alto para diversos padrões de animação comuns, usados em muitos apps. Essas APIs são adaptadas para se alinharem com as práticas recomendadas de Movimentos no Material Design (link em inglês).

AnimatedVisibility (experimental)

A função AnimatedVisibility que pode ser composta anima o aparecimento e desaparecimento de conteúdo.

var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
    Text(text = "Edit")
}

Por padrão, o aparecimento do conteúdo ocorre com esmaecimento e expansão e o desaparecimento com esmaecimento e encolhimento. A transição pode ser personalizada especificando EnterTransition e ExitTransition.

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically(
        // Slide in from 40 dp from the top.
        initialOffsetY = { with(density) { -40.dp.roundToPx() } }
    ) + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}

Como mostrado no exemplo acima, é possível combinar vários objetos EnterTransition ou ExitTransition com um operador +, sendo que cada um aceita parâmetros opcionais para personalizar o comportamento. Consulte as referências para ver mais informações.

Exemplos de EnterTransition

fadeIn:

slideIn:

slideInHorizontally:

slideInVertically:

scaleIn:

expandIn:

expandHorizontally:

expandVertically:

Exemplos de ExitTransition

fadeOut:

slideOut:

slideOutHorizontally:

slideOutVertically:

scaleOut:

shrinkOut:

shrinkHorizontally:

shrinkVertically:

A AnimatedVisibility também oferece uma variante que recebe um MutableTransitionState. Isso permite que você acione uma animação assim que a AnimatedVisibility for adicionada à árvore de composição. Também é útil para observar o estado da animação.

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

Animar a entrada e a saída de filhas

O conteúdo em AnimatedVisibility (animações filhas diretas ou indiretas) pode usar o modificador animateEnterExit para especificar um comportamento de animação diferente para cada uma delas. O efeito visual de cada uma dessas filhas é uma combinação das animações especificadas na função AnimatedVisibility e nas animações de entrada e saída das próprias filhas.

AnimatedVisibility(
    visible = visible,
    // Fade in/out the background and the foreground.
    enter = fadeIn(),
    exit = fadeOut()
) {
    Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

Em alguns casos, pode ser necessário fazer com que AnimatedVisibility não aplique animações para que cada filha possa ter animações diferentes usando animateEnterExit. Para fazer isso, especifique EnterTransition.None e ExitTransition.None na função AnimatedVisibility que pode ser composta.

Adicionar uma animação personalizada

Se você quiser adicionar efeitos de animação personalizados, além das animações de entrada e saída integradas, acesse a instância Transition usando a propriedade transition dentro da lambda de conteúdo da AnimatedVisibility. Todos os estados de animação adicionados à instância de transição serão executados simultaneamente com as animações de entrada e saída de AnimatedVisibility. A AnimatedVisibility aguarda até que todas as animações em Transition tenham terminado antes de remover o conteúdo. A AnimatedVisibility não consegue considerar animações de saída criadas de forma independente da Transition, como o uso de animate*AsState. Por isso, é possível que o elemento do conteúdo seja removido antes do fim da transição.

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope.transition() to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(modifier = Modifier.size(128.dp).background(background))
}

Consulte updateTransition para saber mais sobre a Transition.

AnimatedContent (experimental)

A função AnimatedContent que pode ser composta anima o conteúdo de acordo com um estado de destino.

Row {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(targetState = count) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

Use sempre o parâmetro lambda e o reflita no conteúdo. A API usa esse valor como a chave para identificar o conteúdo que está sendo exibido.

Por padrão, o conteúdo inicial esmaece e o conteúdo de destino aparece gradualmente. Esse comportamento é chamado de esmaecimento gradual (link em inglês). É possível personalizar esse comportamento de animação especificando um objeto ContentTransform para o parâmetro transitionSpec. É possível criar uma ContentTransform combinando uma EnterTransition com uma ExitTransition, usando a função de infixo with. É possível aplicar uma SizeTransform à ContentTransform anexando a função de infixo using.

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically({ height -> height }) + fadeIn() with
                slideOutVertically({ height -> -height }) + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically({ height -> -height }) + fadeIn() with
                slideOutVertically({ height -> height }) + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition define como o conteúdo de destino aparecerá e ExitTransition define como o conteúdo inicial desaparecerá. Além de todas as funções de EnterTransition e ExitTransition disponíveis para AnimatedVisibility, AnimatedContent oferece slideIntoContainer e slideOutOfContainer. Essas são alternativas convenientes a slideInHorizontally/Vertically e slideOutHorizontally/Vertically, que calculam a distância do deslizamento de acordo com base nos tamanhos do conteúdo inicial e do conteúdo de destino do AnimatedContent.

SizeTransform define como o tamanho será animado entre o conteúdo inicial e de destino. Ao criar a animação, você tem acesso ao tamanho inicial e ao tamanho de destino. SizeTransform também controla se o conteúdo precisa ser cortado para o tamanho do componente durante as animações.

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colors.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) with
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

Animar a entrada e saída de filhas

Assim como AnimatedVisibility, o modificador animateEnterExit está disponível dentro da lambda de conteúdo de AnimatedContent. Use esse modificador para aplicar EnterAnimation e ExitAnimation a cada uma das filhas diretas ou indiretas separadamente.

Adicionar uma animação personalizada

Assim como AnimatedVisibility, o campo transition está disponível dentro da lambda de conteúdo de AnimatedContent. Use esse campo para criar um efeito de animação personalizado que será executado simultaneamente com a transição AnimatedContent. Consulte updateTransition para saber mais.

animateContentSize

O modificador animateContentSize anima uma mudança de tamanho.

var message by remember { mutableStateOf("Hello") }
Box(
    modifier = Modifier.background(Color.Blue).animateContentSize()
) {
    Text(text = message)
}

Fading cruzado

O Crossfade é animado entre dois layouts com uma animação de fading cruzado. Ao alternar o valor transmitido para o parâmetro current, o conteúdo muda com uma animação de fading cruzado.

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

APIs de animação de nível baixo

Todas as APIs de animação de nível alto mencionadas na seção anterior são baseadas nas APIs de animação de nível baixo.

As funções animate*AsState são as APIs mais simples, que renderizam uma mudança de valor instantânea como um valor de animação. Essa função tem o suporte de Animatable, uma API baseada em corrotinas para animar um único valor. updateTransition cria um objeto de transição que pode gerenciar vários valores de animação e executá-los com base em uma mudança de estado. rememberInfiniteTransition é semelhante, mas cria uma transição infinita que pode gerenciar várias animações que permanecem em execução indefinidamente. Todas essas APIs podem ser compostas, exceto Animatable, o que significa que essas animações podem ser criadas fora da composição.

Todas essas APIs são baseadas na API Animation mais fundamental. Embora a maioria dos apps não interaja diretamente com Animation, alguns dos recursos de personalização para Animation são disponibilizados por APIs de nível alto. Consulte Personalizar animações para ver mais informações sobre AnimationVector e AnimationSpec.

Diagrama mostrando a relação entre as várias APIs de animação de nível baixo

animate*AsState

As funções animate*AsState são as APIs de animação mais simples do Compose, usadas para animar um único valor. Somente o valor final (ou valor de segmentação) precisa ser informado, e a API inicia a animação do valor atual para o valor especificado.

Veja abaixo um exemplo de animação alfa usando essa API. Ao simplesmente unir o valor de segmentação em animateFloatAsState, o valor alfa se torna um valor de animação entre os valores fornecidos (1f ou 0.5f, nesse caso).

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Não é necessário criar uma instância de classe de animação nem processar a interrupção. Internamente, um objeto de animação (ou seja, uma instância Animatable) será criado e lembrado no local de chamada, tendo o primeiro valor de segmentação como valor inicial. A partir desse momento, sempre que você fornecer um valor de segmentação diferente a essa função que pode ser composta, uma animação será iniciada automaticamente na direção desse valor. Caso já exista uma animação em andamento, ela será iniciada do valor atual (e velocidade) e será animada na direção do valor desejado. Durante a animação, essa função é recomposta e retorna um valor de animação atualizado a cada frame.

O Compose oferece funções animate*AsState para Float, Color, Dp, Size, Bounds, Offset, Rect, Int, IntOffset e IntSize. É possível adicionar compatibilidade com outros tipos de dados facilmente, fornecendo ao animateValueAsState um TwoWayConverter que use um tipo genérico.

É possível personalizar as especificações de animação fornecendo uma AnimationSpec. Consulte AnimationSpec para saber mais.

Animatable

Animatable é um marcador de valor que pode animar o valor à medida que ele muda usando animateTo. Essa é a API que suporta a implementação de animate*AsState. Ela garante continuação consistente e exclusividade mútua, o que significa que a mudança de valor é sempre contínua e qualquer animação em andamento será cancelada.

Muitos recursos de Animatable, incluindo animateTo, são fornecidos como funções de suspensão. Isso significa que eles precisam ser agrupados em um escopo de corrotina adequado. Por exemplo, é possível usar a função LaunchedEffect que pode ser composta para criar um escopo somente para a duração do valor-chave especificado.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize().background(color.value))

No exemplo acima, criamos e lembramos de uma instância de Animatable com o valor inicial de Color.Gray. Dependendo do valor da sinalização booleana ok, a cor será animada como Color.Green ou Color.Red. Qualquer mudança posterior no valor booleano iniciará a animação com a outra cor. Caso haja uma animação em andamento quando o valor mudar, ela será cancelada e a nova animação será iniciada a partir do valor atual com a velocidade atual.

Essa é a implementação da animação que suporta a API animate*AsState mencionada na seção anterior. Comparado a animate*AsState, o uso de Animatable proporciona um controle mais detalhado sobre diversos aspectos. Primeiramente, Animatable pode ter um valor inicial diferente do primeiro valor de segmentação. Isso pode ser observado no exemplo de código acima, que mostra uma caixa cinza no início começando a ser animada imediatamente e mudar para verde ou vermelho. Em segundo lugar, Animatable fornece mais operações sobre o valor do conteúdo, ou seja, snapTo e animateDecay. snapTo define imediatamente o valor atual como o valor de segmentação. Isso é útil quando a animação em si não é a única fonte da verdade e precisa ser sincronizada com outros estados, como eventos de toque. animateDecay inicia uma animação que reduz a velocidade especificada. Isso é útil para implementar o comportamento de rolagem. Consulte Gesto e animação para ver mais informações.

Animatable é compatível com Float e Color, mas qualquer tipo de dados pode ser usado fornecendo um TwoWayConverter. Consulte AnimationVector para ver mais informações.

É possível personalizar as especificações de animação fornecendo uma AnimationSpec. Consulte AnimationSpec para saber mais.

updateTransition

Transition gerencia uma ou mais animações como filhas e executa essas animações de forma simultânea em vários estados.

Os estados podem ser de qualquer tipo de dados. Em muitos casos, é possível usar um tipo de enum personalizado para garantir a segurança, como neste exemplo:

private enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition cria e lembra de uma instância de Transition e atualiza o estado dela.

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

É possível usar uma das funções da extensão animate* para definir uma animação filha nessa transição. Especifique os valores de segmentação para cada um dos estados. Essas funções animate* retornam um valor de animação que é atualizado a cada frame durante a animação quando o estado de transição é atualizado com updateTransition.

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

Você também pode transmitir um parâmetro transitionSpec para especificar uma AnimationSpec diferente para cada combinação de mudanças de estado de transição. Consulte AnimationSpec para saber mais.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)
            else ->
                tween(durationMillis = 500)
        }
    }
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colors.primary
        BoxState.Expanded -> MaterialTheme.colors.background
    }
}

Quando a transição chegar ao estado de segmentação, Transition.currentState será igual a Transition.targetState. Isso pode ser usado como um sinal de conclusão da transição.

Em alguns casos, nós queremos ter um estado inicial diferente do primeiro estado de segmentação. É possível usar updateTransition com MutableTransitionState para fazer isso. Isso permite iniciar a animação assim que o código entra na composição, por exemplo.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)
// ...

Para uma transição mais complexa que envolva várias funções que podem ser compostas, é possível usar createChildTransition para criar uma transição filha. Essa técnica é útil para fazer separações em vários subcomponentes em uma função que pode ser composta complexa. A transição mãe saberá todos os valores de animação nas transições filhas.

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState)
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Usar transição com AnimatedVisibility e AnimatedContent

AnimatedVisibility e AnimatedContent estão disponíveis como funções de extensão de Transition. O targetState da Transition.AnimatedVisibility e do Transition.AnimatedContent é derivado da Transition e aciona as transições de entrada e saída conforme necessário quando o targetState da Transition muda. Essas funções de extensão permitem que todas as animações de entrada, saída e sizeTransform, que seriam internas a AnimatedVisibility/AnimatedContent, sejam elevadas para a Transition. Com essas funções de extensão, é possível observar a mudança de estado de AnimatedVisibility e AnimatedContent externamente. Em vez de um parâmetro booleano visible, essa versão de AnimatedVisibility usa uma lambda que converte o estado de destino da transição mãe em um booleano.

Consulte AnimatedVisibility e AnimatedContent para saber mais.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected)
val borderColor by transition.animateColor { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    elevation = elevation
) {
    Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Encapsular uma transição e a tornar reutilizável

Para casos de uso simples, definir animações de transição na mesma função que pode ser composta da IU é uma opção perfeitamente válida. No entanto, ao trabalhar em um componente complexo com vários valores de animação, é possível que você queira separar a implementação de animação da IU que pode ser composta.

Você pode fazer isso criando uma classe que contenha todos os valores de animação e uma função "update" que retorne uma instância dessa classe. A implementação de transição pode ser extraída para a nova função separada. Esse padrão é útil quando há a necessidade de centralizar a lógica da animação ou fazer com que animações complexas sejam reutilizáveis.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState)
    val color = transition.animateColor { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Compatibilidade com ferramentas

O Android Studio é compatível com a inspeção de transições na visualização do Compose.

  • Visualização da transição frame a frame
  • Inspeção de valor para todas as animações na transição
  • Visualização da transição entre qualquer estado inicial e de segmentação

Ao iniciar o inspetor de animação, você verá o painel "Animations" abaixo da visualização interativa, em que é possível executar qualquer transição incluída na visualização. A transição e cada um dos valores de animação são rotulados com um nome padrão. É possível personalizar o nome especificando o parâmetro label nas funções updateTransition e animate*. Para saber mais sobre a visualização do Compose, consulte Visualização do layout.

rememberInfiniteTransition

A InfiniteTransition contém uma ou mais animações filhas, como Transition. Contudo, essas animações começam a ser executadas assim que entram na composição e só são interrompidas se forem removidas. É possível criar uma instância de InfiniteTransition com rememberInfiniteTransition. As animações filhas podem ser adicionadas com animateColor, animatedFloat ou animatedValue. Também é necessário especificar um infiniteRepeatable para as especificações de animação.

val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

Box(Modifier.fillMaxSize().background(color))

TargetBasedAnimation

TargetBasedAnimation é a API de animação de nível mais baixo que vimos até agora. Outras APIs abrangem a maioria dos casos de uso, mas usar diretamente TargetBasedAnimation permite que você controle o tempo de duração da animação. No exemplo abaixo, o tempo de duração de TargetAnimation é controlado manualmente com base no tempo para a renderização do frame indicado por withFrameMillis.

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

Personalizar animações

Geralmente, muitas das APIs de animação aceitam parâmetros para personalização de comportamentos.

AnimationSpec

A maioria das APIs de animação permite que os desenvolvedores personalizem as especificações de animação usando um parâmetro AnimationSpec opcional.

val alpha: Float by animateFloatAsState(
    targetValue = if (enabled) 1f else 0.5f,
    // Configure the animation duration and easing.
    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)

Existem diferentes tipos de AnimationSpec para criação de diferentes tipos de animação.

spring

A spring cria uma animação baseada em física entre os valores inicial e final. Ela precisa de dois parâmetros: dampingRatio e stiffness.

dampingRatio define o grau de mobilidade da spring. O valor padrão é Spring.DampingRatioNoBouncy.

Gráfico animado exibindo o comportamento de diferentes proporções de amortecimento

stiffness define a velocidade de movimento da spring em direção ao valor final. O valor padrão é Spring.StiffnessMedium.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy,
        stiffness = Spring.StiffnessMedium
    )
)

spring pode processar interrupções de forma melhor que tipos de AnimationSpec baseados em duração, porque garante a continuidade da velocidade quando o valor de segmentação muda entre animações. spring é usada como o AnimationSpec padrão por muitas APIs de animação, como animate*AsState e updateTransition.

tween

tween executa animações entre os valores inicial e final na durationMillis especificada, usando uma curva de easing. Consulte Easing para ver mais informações. Também é possível especificar delayMillis para adiar o início da animação.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = tween(
        durationMillis = 300,
        delayMillis = 50,
        easing = LinearOutSlowInEasing
    )
)

keyframes

keyframes executa animações com base nos valores resumidos especificados em carimbos de data/hora diferentes na duração da animação. O valor de animação será interpolado entre dois valores keyframe a qualquer momento. O easing pode ser especificado para qualquer um dos keyframes para determinar a curva de interpolação.

Especificar os valores como 0 ms e o tempo de duração é opcional. Caso esses valores não sejam especificados, o padrão será os valores inicial e final da animação, respectivamente.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = keyframes {
        durationMillis = 375
        0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
        0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
        0.4f at 75 // ms
        0.4f at 225 // ms
    }
)

repeatable

repeatable executa uma animação baseada em duração (como tween ou keyframes) repetidamente, até alcançar a contagem de iteração especificada. É possível transmitir o parâmetro repeatMode para especificar se a animação será repetida começando do início (RepeatMode.Restart) ou do final (RepeatMode.Reverse).

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = repeatable(
        iterations = 3,
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    )
)

infiniteRepeatable

infiniteRepeatable é semelhante a repeatable, mas essa função se repete por uma quantidade infinita de iterações.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    )
)

Animações que usam infiniteRepeatable não são executadas em testes que usam ComposeTestRule. O componente será renderizado usando o valor inicial de cada valor de animação.

snap

snap é uma AnimationSpec especial que muda imediatamente o valor para o valor final. Você pode especificar delayMillis para atrasar o início da animação.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = snap(delayMillis = 50)
)

Easing

As operações AnimationSpec baseadas em duração (como tween ou keyframes) usam Easing para ajustar a fração da animação. Isso permite que o valor de animação acelere e desacelere, em vez de se mover a uma taxa constante. Uma fração é um valor entre 0 (início) e 1,0 (fim) indicando o ponto atual da animação.

O easing é, na verdade, uma função que usa um valor de fração entre 0 e 1,0 e retorna um ponto flutuante. O valor retornado pode estar fora do limite para representar uma ultrapassagem ou uma redução. Um easing personalizado pode ser criado, como no código abaixo.

val CustomEasing = Easing { fraction -> fraction * fraction }

@Composable
fun EasingUsage() {
    val value by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = 300,
            easing = CustomEasing
        )
    )
    // … …
}

O Compose oferece várias funções Easing integradas que abrangem a maioria dos casos de uso. Consulte Speed - Material Design (link em inglês) para ver mais informações sobre qual easing usar dependendo da situação.

  • FastOutSlowInEasing
  • LinearOutSlowInEasing
  • FastOutLinearEasing
  • LinearEasing
  • CubicBezierEasing

AnimationVector

A maioria das APIs de animação do Compose oferecem compatibilidade com Float, Color, Dp e outros tipos de dados básicos como valores de animação prontos. Contudo, às vezes é necessário animar outros tipos de dados, incluindo os dados personalizados. Durante a animação, qualquer valor de animação é representado como um AnimationVector. O valor é convertido em um AnimationVector, e vice-versa, por um TwoWayConverter correspondente, para que o sistema de animação principal possa processá-lo de maneira uniforme. Por exemplo, uma Int é representada como um AnimationVector1D contendo um único valor flutuante. Para TwoWayConverter, o Int é assim:

val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
    TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })

Color é essencialmente um conjunto de quatro valores: vermelho, verde, azul e alfa. Portanto, Color é convertido em um AnimationVector4D contendo quatro valores flutuantes. Dessa forma, cada tipo de dados usado nas animações é convertido em AnimationVector1D, AnimationVector2D, AnimationVector3D ou AnimationVector4D, dependendo da dimensionalidade. Isso permite que diferentes componentes do objeto sejam animados de forma independente, com rastreamentos de velocidade próprios. Os conversores integrados para tipos de dados básicos podem ser acessados usando Color.VectorConverter, Dp.VectorConverter e assim por diante.

Caso queira adicionar compatibilidade com um novo tipo de dados como um valor de animação, crie seu próprio TwoWayConverter e forneça-o à API. Por exemplo, é possível usar animateValueAsState para animar o tipo de dados personalizado da seguinte forma:

data class MySize(val width: Dp, val height: Dp)

@Composable
fun MyAnimation(targetSize: MySize) {
    val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
        targetSize,
        TwoWayConverter(
            convertToVector = { size: MySize ->
                // Extract a float value from each of the `Dp` fields.
                AnimationVector2D(size.width.value, size.height.value)
            },
            convertFromVector = { vector: AnimationVector2D ->
                MySize(vector.v1.dp, vector.v2.dp)
            }
        )
    )
}

Gesto e animação (avançado)

Há diversos fatores que precisam ser considerados ao trabalhar com a combinação de eventos de toque e animações, em comparação com quando trabalhamos apenas com animações. Primeiramente, pode ser necessário interromper uma animação em andamento quando o evento de toque for iniciado, já que a interação do usuário tem maior prioridade.

No exemplo abaixo, usamos Animatable para representar a posição de deslocamento de um componente de círculo. Os eventos de toque são processados com o modificador pointerInput. Ao detectar um novo evento de toque, animateTo é chamado para animar o valor de deslocamento para a posição de toque. Um evento de toque também pode acontecer durante a animação. Nesse caso, animateTo interrompe a animação em andamento e inicia a animação na nova posição de segmentação, mantendo a velocidade da animação interrompida.

@Composable
fun Gesture() {
    val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        // Detect a tap event and obtain its position.
                        val position = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        launch {
                            // Animate to the tap position.
                            offset.animateTo(position)
                        }
                    }
                }
            }
    ) {
        Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
    }
}

private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())

Outro padrão frequente é precisar sincronizar os valores de animação com valores originados de eventos de toque, como o recurso de arrastar. No exemplo abaixo, vemos o recurso "deslizar para dispensar" implementado como um Modifier, em vez de usar a função SwipeToDismiss. O deslocamento horizontal do elemento é representado como Animatable. Essa API tem uma característica útil na animação de gestos. O valor dela pode ser mudado por eventos de toque e pela animação. Quando um evento de toque é recebido, Animatable é interrompido pelo método stop, para que qualquer animação em andamento seja interceptada.

Durante um evento de arrastar, snapTo é usado para atualizar o valor Animatable com o valor calculado de eventos de toque. Para a rolagem, o Compose fornece o VelocityTracker para gravar eventos de arrastar e calcular a velocidade. A velocidade pode ser alimentada diretamente em animateDecay para a animação com rolagem. Para mover o valor de deslocamento de volta para a posição original, o deslocamento de segmentação de 0f precisa ser especificado com o método animateTo.

fun Modifier.swipeToDismiss(
    onDismissed: () -> Unit
): Modifier = composed {
    val offsetX = remember { Animatable(0f) }
    pointerInput(Unit) {
        // Used to calculate fling decay.
        val decay = splineBasedDecay<Float>(this)
        // Use suspend functions for touch events and the Animatable.
        coroutineScope {
            while (true) {
                // Detect a touch down event.
                val pointerId = awaitPointerEventScope { awaitFirstDown().id }
                val velocityTracker = VelocityTracker()
                // Stop any ongoing animation.
                offsetX.stop()
                awaitPointerEventScope {
                    horizontalDrag(pointerId) { change ->
                        // Update the animation value with touch events.
                        launch {
                            offsetX.snapTo(
                                offsetX.value + change.positionChange().x
                            )
                        }
                        velocityTracker.addPosition(
                            change.uptimeMillis,
                            change.position
                        )
                    }
                }
                // No longer receiving touch events. Prepare the animation.
                val velocity = velocityTracker.calculateVelocity().x
                val targetOffsetX = decay.calculateTargetValue(
                    offsetX.value,
                    velocity
                )
                // The animation stops when it reaches the bounds.
                offsetX.updateBounds(
                    lowerBound = -size.width.toFloat(),
                    upperBound = size.width.toFloat()
                )
                launch {
                    if (targetOffsetX.absoluteValue <= size.width) {
                        // Not enough velocity; Slide back.
                        offsetX.animateTo(
                            targetValue = 0f,
                            initialVelocity = velocity
                        )
                    } else {
                        // The element was swiped away.
                        offsetX.animateDecay(velocity, decay)
                        onDismissed()
                    }
                }
            }
        }
    }
        .offset { IntOffset(offsetX.value.roundToInt(), 0) }
}

Teste

A ComposeTestRule oferecida pelo Compose permite programar testes de animações de forma determinística, com controle total sobre o relógio de teste. Isso permite verificar valores de animação intermediários. Além disso, a duração do teste pode ser menor do que a da animação.

A ComposeTestRule exibe o relógio de teste como mainClock. Você pode definir a propriedade autoAdvance como "false" no código de teste para controlar o relógio. Depois de iniciar a animação a ser testada, o relógio pode ser adiantado com advanceTimeBy.

Observe que o advanceTimeBy não adianta o relógio exatamente de acordo com a duração especificada. Em vez disso, a duração é arredondada para o valor mais próximo que corresponda a um multiplicador da duração do frame.

@get:Rule
val rule = createComposeRule()

@Test
fun testAnimationWithClock() {
    // Pause animations
    rule.mainClock.autoAdvance = false
    var enabled by mutableStateOf(false)
    rule.setContent {
        val color by animateColorAsState(
            targetValue = if (enabled) Color.Red else Color.Green,
            animationSpec = tween(durationMillis = 250)
        )
        Box(Modifier.size(64.dp).background(color))
    }

    // Initiate the animation.
    enabled = true

    // Let the animation proceed.
    rule.mainClock.advanceTimeBy(50L)

    // Compare the result with the image showing the expected result.
    rule.onRoot().captureToImage().assertAgainstGolden()
}

Saiba mais

Para saber mais, consulte o codelab animação do Jetpack Compose.