Modificadores de animações e elementos combináveis

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

Elementos combináveis animados integrados

O Compose oferece vários elementos combináveis que animam a aparência, o desaparecimento e as mudanças de layout do conteúdo.

Animar aparecimento e desaparecimento

Elemento combinável verde aparecendo e desaparecendo
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. Personalize essa transição especificando objetos 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 anterior, é 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 páginas de referência para mais informações.

Exemplos de transição de entrada e saída

EnterTransition ExitTransition
fadeIn
Um elemento da interface aparece gradualmente.
fadeOut
Um elemento da interface desaparece gradualmente da tela.
slideIn
Um elemento da interface desliza para dentro da tela.
slideOut
Um elemento da interface desliza para fora da tela.
slideInHorizontally
Um elemento da interface desliza horizontalmente para a visualização.
slideOutHorizontally
Um elemento da interface desliza horizontalmente para fora da visualização.
slideInVertically
Um elemento da interface desliza verticalmente para a área de visualização.
slideOutVertically
Um elemento da interface desliza verticalmente para fora da visualização.
scaleIn
Um elemento da interface aumenta e entra na visualização.
scaleOut
Um elemento da interface diminui e sai da tela.
expandIn
Um elemento da interface se expande para a visualização a partir de um ponto central.
shrinkOut
Um elemento da interface encolhe para fora da visualização até um ponto central.
expandHorizontally
Um elemento da interface se expande horizontalmente para a visualização.
shrinkHorizontally
Um elemento da interface encolhe horizontalmente para fora da visualização.
expandVertically
Um elemento da interface se expande verticalmente para a visualização.
shrinkVertically
Um elemento da interface encolhe verticalmente para fora da visualização.

A função de composição AnimatedVisibility também oferece uma variante que recebe um argumento MutableTransitionState. Isso permite que você acione uma animação assim que o elemento combinável AnimatedVisibility for adicionado à á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 combinável 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)
    )
}

Para saber mais sobre como usar Transition para gerenciar animações, consulte Animar várias propriedades simultaneamente com uma transição.

Animar com base no estado de destino

O elemento AnimatedContent combinável anima o conteúdo de acordo com um estado de destino.

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

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. É possível criar uma instância de ContentTransform combinando um objeto EnterTransition com um objeto ExitTransition usando a função de infixo with. Aplique uma SizeTransform ao objeto 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() togetherWith
                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() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition define como o conteúdo de destino aparecerá e ExitTransition define como o conteúdo inicial 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)) togetherWith
                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
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

Animar transições de entrada e saída de 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.

Animar entre dois layouts

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, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

Modificadores de animação integrados

O Compose oferece modificadores para animar mudanças específicas diretamente em elementos combináveis.

Animar mudanças no tamanho de elementos combináveis

Elemento combinável verde animando a mudança de tamanho de maneira suave.
Figura 2. Elemento combinável que anima suavemente 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.