Animar um único valor com animate*AsState
As funções animate*AsState
são as APIs de animação mais simples do Compose, usadas para
animar um único valor. Você só precisa informar o valor de destino (ou final), 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 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).
var enabled by remember { mutableStateOf(true) } val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha") 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 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, esse elemento combinável é recomposto e retorna um valor de animação
atualizado a cada frame.
O Compose oferece funções animate*AsState
para Float
,
Color
, Dp
, Size
, Offset
, Rect
, Int
, IntOffset
, e
IntSize
. Para adicionar suporte a outros tipos de dados facilmente, basta fornecer 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 as executa
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:
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, 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. 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
. 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 = 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 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, 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 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, 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 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 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 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. 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, 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
usando rememberInfiniteTransition
. Animações filhas podem ser adicionadas com
animateColor
, animatedFloat
ou animatedValue
. 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 Animation de baixo nível
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 diversos 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
.
Animatable
: animação de valor único baseada em corrotina
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 vai 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
de composição 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.
Animation
: animação controlada manualmente
Animation
é a API de animação 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
.
A Animation
só deve ser usada 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 as 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 abaixo,
o tempo de duração da TargetAnimation
é controlado manualmente 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 {:#customize-animations}
- Animações no Compose
- Modificadores de animação e elementos combináveis