Nesta página, descrevemos como criar animações baseadas em valor 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 no
Compose para animar um único valor. Somente o valor de segmentação
(ou valor final) precisa ser informado, 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 agora é um valor de animação
entre os valores fornecidos (1f ou 0.5f, neste 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 nenhuma classe de animação ou processar
interrupções. 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 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 oferece funções animate*AsState para Float, Color,
Dp, Size, Offset, Rect, Int, IntOffset e IntSize. É possível adicionar
suporte a outros tipos de dados fornecendo um TwoWayConverter para
animateValueAsState, que usa um tipo genérico.
É possível personalizar as especificações de animação fornecendo um
AnimationSpec. Consulte AnimationSpec para mais informações.
Animar várias propriedades simultaneamente com uma transição
Transition gerencia uma ou mais animações como filhas e as executa
simultaneamente entre vários estados.
Os estados podem ser de qualquer tipo de dado. Em muitos casos, é possível usar um tipo de 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 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(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. Veja mais informações em AnimationSpec.
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 uma transição chega ao estado de destino, Transition.currentState é
igual a Transition.targetState. Você pode usar isso como um indicador de que
a transição foi concluída.
Às vezes, você pode querer ter um estado inicial diferente do primeiro
estado de destino. Para isso, use updateTransition com MutableTransitionState. Por exemplo, ele permite que você inicie a animação assim que o código
entrar 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 combináveis, é possível
usar createChildTransition para criar uma transição filha. Essa técnica é
útil para separar as responsabilidades entre vários subcomponentes em um elemento combinável
complexo. 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 targetState
do Transition 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 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 mais detalhes.
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 da 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 que se repete infinitamente com 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 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 alto nível mencionadas na seção anterior são baseadas nas APIs de animação de baixo nível.
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 por
Animatable, uma API baseada em corrotina 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 são combináveis, exceto
Animatable, o que significa que você pode criar essas animações 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, é possível acessar alguns dos
recursos de personalização dele por APIs de nível mais alto. Consulte Personalizar
animações para mais informações sobre AnimationVector e AnimationSpec.
Animatable: animação de valor único baseada em corrotina
Animatable é um armazenador de valores que pode animar o valor conforme ele é alterado
usando animateTo. Essa é a API que faz backup da 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 você precisa uni-las em um escopo de corrotina apropriado. 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 flag booleana
ok, a cor é animada para Color.Green ou Color.Red. Qualquer
mudança subsequente no valor booleano inicia uma animação para a outra cor.
Se uma animação estiver em andamento quando o valor mudar, o Compose vai cancelar a
animação, e a nova vai começar com o valor do snapshot atual e a
velocidade atual.
Essa API Animatable é a implementação subjacente de animate*AsState
mencionada na seção anterior. O uso direto de Animatable oferece
um controle mais refinado de várias maneiras:
- Primeiro,
Animatablepode 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, o
Animatableoferece mais operações no valor do conteúdo, especificamentesnapToeanimateDecay.snapTodefine o valor atual como o valor de destino imediatamente. Isso é útil quando a animação não é a única fonte da verdade e precisa ser sincronizada com outros estados, como eventos de toque.animateDecayinicia uma animação que reduz a velocidade especificada. Isso é útil para implementar o comportamento de rolagem.
Consulte Gestos e animações para 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 mais
informações.
É possível personalizar as especificações de animação fornecendo uma AnimationSpec.
Consulte AnimationSpec para mais informações.
Animation: animação controlada manualmente
A 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 subtipos de Animation: TargetBasedAnimation e DecayAnimation.
Use Animation apenas para controlar manualmente o tempo da animação. 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 APIs de nível mais alto.
TargetBasedAnimation
Outras APIs abrangem a maioria dos casos de uso, mas usar a TargetBasedAnimation diretamente permite
que você controle 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 da TargetBasedAnimation, a 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.
Essas animações são geralmente usadas após um gesto rápido de deslizar 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.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Personalizar animações
- Animações no Compose
- Modificadores de animações e elementos combináveis