Modificadores de animações e elementos combináveis

O Compose vem com elementos combináveis e modificadores integrados para processar casos de uso comuns de animação.

Elementos combináveis animados integrados

Animar aparecimento e desaparecimento com AnimatedVisibility

Elemento combinável verde sendo mostrado e ocultado
Figura 1. Animar o aparecimento e desaparecimento de um item em uma coluna

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

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

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

EnterTransition ExitTransition
fadeIn
animação de exibição gradual
fadeOut
animação de esmaecimento
slideIn
animação de entrada
slideOut
animação de saída
slideInHorizontally
animação de entrada horizontal
slideOutHorizontally
animação de saída horizontal
slideInVertically
animação de entrada vertical
slideOutVertically
animação de saída vertical
scaleIn
animação de aumento gradual
scaleOut
animação de diminuição gradual
expandIn
animação de expansão
shrinkOut
animação de redução
expandHorizontally
animação de expansão horizontal
shrinkHorizontally
animação de redução horizontal
expandVertically
animação de expansão vertical
shrinkVertically
animação de redução vertical

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.

var visible by remember { mutableStateOf(true) }

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.

var visible by remember { mutableStateOf(true) }

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(label = "color") { 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.

Animar com base no estado de destino com AnimatedContent

O elemento AnimatedContent combinável 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.colorScheme.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 transições de entrada e saída filhas

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.

Animar entre dois layouts com Crossfade

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")
    }
}

Modificadores de animação integrados

Animar mudanças no tamanho do elemento combinável com animateContentSize

A animação do elemento combinável verde muda de forma suave.
Figura 2. Animação suave do elemento combinável entre um tamanho pequeno e um maior

O modificador animateContentSize anima uma mudança de tamanho.

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

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.