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 da 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 explicandor as APIs de nível baixo, que proporcionam maior controle e personalização.
O diagrama abaixo ajuda você a decidir qual API usar ao implementar sua animação.
- Se estiver animando uma mudança de conteúdo no layout:
- Para animar aparecimento e desaparecimento:
- Use
AnimatedVisibility
.
- Use
- Para trocar conteúdo com base no estado:
- Em conteúdo com fading cruzado:
- Use
Crossfade
.
- Use
- Caso contrário, use
AnimatedContent
.
- Em conteúdo com fading cruzado:
- Ou use
Modifier.animateContentSize
.
- Para animar aparecimento e desaparecimento:
- Se a animação for baseada em estado:
- Para a animação acontecer durante a composição:
- Em animações infinitas:
- Em animações com vários valores simultâneos:
- Use
updateTransition
.
- Use
- Caso contrário, use
animate*AsState
.
- Para a animação acontecer durante a composição:
- Se quiser ter um controle preciso sobre a duração da animação:
- Use
Animation
, comoTargetBasedAnimation
ouDecayAnimation
.
- Use
- Se a animação for a única fonte da verdade:
- Use
Animatable
.
- Use
- Caso contrário, use
AnimationState
ouanimate
.
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
A função de composição
AnimatedVisibility
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.
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 mais informações.
Exemplos da EnterTransition
e ExitTransition
A função de composição AnimatedVisibility
também oferece uma variante que recebe uma classe
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,
enter = fadeIn(),
exit = fadeOut()
) {
// Fade in/out the background and the foreground.
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
.
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 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 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.
AnimatedContent (experimental)
O elemento AnimatedContent
que pode ser composto 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 mostrado.
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
. Você pode criar um ContentTransform
combinando um
EnterTransition
com um ExitTransition
,
usando a função de infixo with
. Aplique uma SizeTransform
ao 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")
}
O objeto EnterTransition
define como o conteúdo de destino vai aparecer, e
ExitTransition
define como o conteúdo inicial vai 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 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 elementos filhos
Assim como AnimatedVisibility
, o modificador animateEnterExit
está disponível dentro da lambda de conteúdo de AnimatedContent
. Use esse
modificador para aplicar o EnterAnimation
e o ExitAnimation
a cada um dos filhos diretos ou indiretos
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")
}
}
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:
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 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)
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 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)
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) }
}
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 definir 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))
APIs Animation 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 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
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
A 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 da 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 { mutableStateOf(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.
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
.
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
)
)
A 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 destino 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
executam 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 vai 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
- Veja mais
AnimationVector
A maioria das APIs de animação do Compose oferece suporte a 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 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() })
A Color
é essencialmente um conjunto de quatro valores: vermelho, verde, azul e alfa. A
Color
é convertida 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 suporte a um novo tipo de dados como um valor de animação,
crie seu TwoWayConverter
e o forneça à API. Por exemplo,
é possível usar animateValueAsState
para animar o tipo de dado 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)
}
)
)
}
Recursos vetoriais animados (experimental)
Para usar um recurso AnimatedVectorDrawable
, carregue o arquivo drawable usando animatedVectorResource
e transmita um boolean
para alternar entre o estado inicial e final do drawable.
@Composable
fun AnimatedVectorDrawable() {
val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = "Timer",
modifier = Modifier.clickable {
atEnd = !atEnd
},
contentScale = ContentScale.Crop
)
}
Para ver mais informações sobre o formato do arquivo drawable, consulte Animar gráficos drawable.
Animações de itens de lista
Se você quiser animar a reordenação dos itens em uma lista ou grade lenta, consulte a documentação de animação de itens de layout lento.
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. Primeiro, talvez seja 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 destino, 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, o 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 à posição original, o deslocamento de destino 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.
// `assertAgainGolden` needs to be implemented in your code.
rule.onRoot().captureToImage().assertAgainstGolden()
}
Suporte a ferramentas
O Android Studio tem suporte à inspeção de
updateTransition
e
animatedVisibility
na
visualização de animação. Você pode fazer o
seguinte:
- Visualizar uma transição frame a frame
- Inspecionar valores de todas as animações na transição
- Visualizar uma transição entre qualquer estado inicial e de segmentação
- Inspecionar e coordenar várias animações de uma vez
Ao iniciar a visualização de animação, o painel "Animations" vai ser mostrado. Nele, é 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. Você pode personalizar o rótulo
especificando o parâmetro label
nas funções updateTransition
e AnimatedVisibility
. Para mais informações, consulte
Visualização de animação.
Saiba mais
Para saber mais sobre animações no Jetpack Compose, consulte estes outros recursos:
- Folha de referência da animação do Compose (em inglês)
Exemplos
Postagens do blog
Codelabs
Vídeos
- Animação reimaginada (em inglês)