Animações com base no valor

Esta página descreve como criar animações baseadas em valores no Jetpack Compose, com foco em APIs que animam valores com base nos estados atual e de destino.

Animar um único valor com animate*AsState

As funções animate*AsState são APIs de animação simples em Compose para animar um único valor. Você só precisa fornecer o valor de destino (ou valor final), e a API inicia a animação do valor atual para o valor especificado.

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

var enabled by remember { mutableStateOf(true) }

val animatedAlpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer { alpha = animatedAlpha }
        .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 destino como valor inicial. A partir desse momento, sempre que você fornecer um valor de segmentação diferente a essa função de composição, uma animação vai 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.

Por padrão, o Compose fornece funções animate*AsState para Float, Color, Dp, Size, Offset, Rect, Int, IntOffset, e IntSize. Para adicionar suporte a outros tipos de dados, forneça um TwoWayConverter ao método animateValueAsState que use um tipo genérico.

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

Animar várias propriedades simultaneamente com uma transição

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 enum personalizado para verificar a segurança de tipos, como neste exemplo:

enum class BoxState {
    Collapsed,
    Expanded
}

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

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

É possível usar uma das funções de 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(label = "rectangle") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "border width") { 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)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

Quando a transição chegar ao estado de segmentação, Transition.currentState será igual a Transition.targetState. Você pode usar isso como um indicador de conclusão da transição.

Às vezes, você pode querer ter um estado inicial diferente do primeiro estado de destino. É possível usar updateTransition com MutableTransitionState para isso. Por exemplo, isso permite iniciar a animação assim que o código entra na composição.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

Para uma transição mais complexa que envolva várias funções de composição, é 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 reconhece 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, label = "dialer state")
    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 a transição com AnimatedVisibility e AnimatedContent

AnimatedVisibility e AnimatedContent estão disponíveis como funções de extensão de Transition. O targetState para Transition.AnimatedVisibility e Transition.AnimatedContent é derivado do Transition, e aciona animações de entrada, saída e sizeTransform conforme necessário quando o Transition's targetState muda. Essas funções de extensão permitem elevar todas as animações de entrada, saída e sizeTransform que seriam internas a AnimatedVisibility/AnimatedContent para a Transition. Com essas funções de extensão, é possível observar a mudança de estado de AnimatedVisibility/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, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = 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 e a tornar uma transição reutilizável

Para casos de uso simples, definir animações de transição na mesma função de composição da IU é uma opção 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 de composição.

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. É possível extrair a implementação de transição para a nova função separada. Esse padrão é útil quando você precisa centralizar a lógica da animação ou tornar animações complexas 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, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

Criar uma animação de repetição infinita com rememberInfiniteTransition

InfiniteTransition contém uma ou mais animações filhas, como Transition, mas as animações começam a ser executadas assim que entram na composição e não param a menos que sejam removidas. É possível criar uma instância de InfiniteTransition com rememberInfiniteTransition, e adicionar animações filhas com animateColor, animateFloat, ou animateValue. Também é necessário definir um infiniteRepeatable para as especificações de animação.

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

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

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 criadas com base nas APIs de animação de nível baixo.

As funções animate*AsState são APIs simples que renderizam uma mudança de valor instantânea como um valor de animação. Essa funcionalidade é apoiada pela Animatable, uma API baseada em corrotinas para animar um único valor.

updateTransition cria um objeto de transição que pode gerenciar diversos valores de animação e executá-los quando um estado muda. rememberInfiniteTransition é semelhante, mas cria uma transição infinita que pode gerenciar várias animações que continuam 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, que é mais fundamental. Embora a maioria dos apps não interaja diretamente com Animation, é possível acessar alguns dos recursos de personalização dela por APIs de nível mais alto. Consulte Personalizar animações para mais informações sobre AnimationVector e AnimationSpec.

Relação entre APIs de animação de baixo nível
Figura 1. Relação entre APIs de animação de nível baixo.

Animatable: animação de valor único baseada em corrotinas

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 o Compose cancela qualquer animação em andamento.

Muitos recursos de Animatable, incluindo animateTo, são 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 de composição LaunchedEffect 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 anterior, você cria e lembra 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á uma animação com a outra cor. Se uma animação estiver em andamento quando o valor mudar, o Compose vai cancelar a animação, e a nova animação será iniciada a partir do valor atual com a velocidade atual.

Essa API Animatable é a implementação subjacente para animate*AsState mencionada na seção anterior. O uso direto de Animatable oferece um controle mais detalhado de várias maneiras:

  • Primeiramente, Animatable pode ter um valor inicial diferente do primeiro valor de destino. Por exemplo, o exemplo de código anterior mostra uma caixa cinza no início, que é animada imediatamente 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 destino. Isso é útil quando a animação 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 deslizar rapidamente.

Consulte Gesto e animação para ver mais informações.

Por padrão, Animatable oferece suporte a Float e Color, mas é possível usar qualquer tipo de dados 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.

Animation: animação controlada manualmente

Animation é a API Animation de nível mais baixo disponível. Muitas das animações que estudamos até agora se baseiam na Animation. Há dois Animation subtipos: TargetBasedAnimation e DecayAnimation.

Use Animation somente para controlar manualmente o tempo da animação. A Animation não tem estado e não tem nenhum conceito do ciclo de vida. Ela serve como um mecanismo de cálculo de animações para as APIs de nível mais alto.

TargetBasedAnimation

Outras APIs abrangem a maioria dos casos de uso, mas usar a TargetBasedAnimation diretamente permite controlar o tempo de duração da animação. No exemplo a seguir, você controla manualmente o tempo de duração da TargetAnimation com base no tempo para a renderização do frame indicado por withFrameNanos.

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

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

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

DecayAnimation

Ao contrário de TargetBasedAnimation, DecayAnimation não exige que um targetValue seja fornecido. Em vez disso, ela calcula o targetValue com base nas condições iniciais, definidas pela initialVelocity e o initialValue, além da DecayAnimationSpec fornecida.

Animações de decaimento são geralmente usadas após um gesto de deslizar rapidamente para desacelerar os elementos até uma parada. A velocidade da animação começa com o valor definido por initialVelocityVector e diminui ao longo do tempo.