Personalizar a transição de elementos compartilhados

Para personalizar a execução da animação de transição de elemento compartilhado, alguns parâmetros podem ser usados para mudar a transição dos elementos compartilhados.

Especificação da animação

Para mudar a especificação da animação usada para o movimento de tamanho e posição, especifique um parâmetro boundsTransform diferente em Modifier.sharedElement(). Isso fornece a posição Rect inicial e a posição Rect de destino.

Por exemplo, para fazer com que o texto no exemplo anterior se mova com um movimento de arco, especifique o parâmetro boundsTransform para usar uma especificação keyframes:

val textBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
    keyframes {
        durationMillis = boundsAnimationDurationMillis
        initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
        targetBounds at boundsAnimationDurationMillis
    }
}
Text(
    "Cupcake", fontSize = 28.sp,
    modifier = Modifier.sharedBounds(
        rememberSharedContentState(key = "title"),
        animatedVisibilityScope = animatedVisibilityScope,
        boundsTransform = textBoundsTransform
    )
)

Você pode usar qualquer AnimationSpec. Este exemplo usa uma especificação keyframes.

Figura 1. Exemplo mostrando diferentes parâmetros boundsTransform

Modo de redimensionamento

Ao animar entre dois limites compartilhados, você pode definir o parâmetro resizeMode como RemeasureToBounds ou ScaleToBounds. Esse parâmetro determina como o elemento compartilhado faz a transição entre os dois estados. ScaleToBounds primeiro mede o layout filho com as restrições de lookahead (ou de destino). Em seguida, o layout estável da criança é dimensionado para caber nos limites compartilhados. ScaleToBounds pode ser considerado uma "escala gráfica" entre os estados.

Por outro lado, RemeasureToBounds mede e refaz o layout filho de sharedBounds com restrições fixas animadas com base no tamanho de destino. A nova medição é acionada pela mudança de tamanho dos limites, que pode ocorrer a cada frame.

Para elementos combináveis Text, recomendamos ScaleToBounds, porque ele evita o relayout e o reflow do texto em linhas diferentes. RemeasureToBounds é recomendado para limites que têm proporções diferentes e se você quiser uma continuidade fluida entre os dois elementos compartilhados.

A diferença entre os dois modos de redimensionamento pode ser vista nos exemplos a seguir:

ScaleToBounds

RemeasureToBounds

Ativar e desativar elementos compartilhados de forma dinâmica

Por padrão, sharedElement() e sharedBounds() são configurados para animar as mudanças de layout sempre que uma chave correspondente é encontrada no estado de destino. No entanto, talvez você queira desativar essa animação de forma dinâmica com base em condições específicas, como a direção da navegação ou o estado atual da interface.

Para controlar se a transição de elemento compartilhado ocorre, personalize o SharedContentConfig transmitido para rememberSharedContentState(). A propriedade isEnabled determina se o elemento compartilhado está ativo.

O exemplo a seguir demonstra como definir uma configuração que só ativa a transição compartilhada ao navegar entre telas específicas (por exemplo, apenas de A para B), desativando-a para outras.

SharedTransitionLayout {
    val transition = updateTransition(currentState)
    transition.AnimatedContent { targetState ->
        // Create the configuration that depends on state changing.
        fun animationConfig() : SharedTransitionScope.SharedContentConfig {
            return object : SharedTransitionScope.SharedContentConfig {
                override val SharedTransitionScope.SharedContentState.isEnabled: Boolean
                    // For this example, we only enable the transition in one direction
                    // from A -> B and not the other way around.
                    get() =
                        transition.currentState == "A" && transition.targetState == "B"
            }
        }
        when (targetState) {
            "A" -> Box(
                modifier = Modifier
                    .sharedElement(
                        rememberSharedContentState(
                            key = "shared_box",
                            config = animationConfig()
                        ),
                        animatedVisibilityScope = this
                    )
                    // ...
            ) {
                // Your content
            }
            "B" -> {
                Box(
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(
                                key = "shared_box",
                                config = animationConfig()
                            ),
                            animatedVisibilityScope = this
                        )
                        // ...
                ) {
                    // Your content
                }
            }
        }
    }
}

Por padrão, se um elemento compartilhado for desativado durante uma animação em andamento, ele ainda concluirá a animação atual em andamento para evitar a remoção acidental de animações em trânsito. Se você precisar remover o elemento enquanto a animação estiver em andamento, substitua shouldKeepEnabledForOngoingAnimation na interface SharedContentConfig para retornar "false".

Pular para o layout final

Por padrão, ao fazer a transição entre dois layouts, o tamanho do layout é animado entre o estado inicial e o final. Esse comportamento pode ser indesejável ao animar conteúdo como texto.

O exemplo a seguir ilustra o texto de descrição "Lorem Ipsum" entrando na tela de duas maneiras diferentes. No primeiro exemplo, o texto é refluído à medida que entra, à medida que o contêiner aumenta de tamanho. No segundo exemplo, o texto não é refluído à medida que cresce. A adição de Modifier.skipToLookaheadSize() impede o reflow à medida que ele cresce.

Sem Modifier.skipToLookaheadSize(): observe o texto "Lorem Ipsum" sendo refluído

Modifier.skipToLookaheadSize() - observe que o texto "Lorem Ipsum" mantém o estado final no início da animação

Recorte e sobreposições

Para que os elementos compartilhados sejam compartilhados entre diferentes elementos combináveis, a renderização do elemento combinável é elevada em uma sobreposição de camada quando a transição é iniciada para a correspondência no destino. O efeito disso é que ele vai escapar dos limites do pai e das transformações de camada (por exemplo, alfa e escala).

Ele será renderizado sobre outros elementos de interface não compartilhados. Quando a transição terminar, o elemento será descartado da sobreposição para o próprio DrawScope.

Para recortar um elemento compartilhado em uma forma, use a função Modifier.clip() padrão. Coloque-o após o sharedElement():

Image(
    painter = painterResource(id = R.drawable.cupcake),
    contentDescription = "Cupcake",
    modifier = Modifier
        .size(100.dp)
        .sharedElement(
            rememberSharedContentState(key = "image"),
            animatedVisibilityScope = this@AnimatedContent
        )
        .clip(RoundedCornerShape(16.dp)),
    contentScale = ContentScale.Crop
)

Se você precisar garantir que um elemento compartilhado nunca seja renderizado fora de um contêiner pai, defina clipInOverlayDuringTransition em sharedElement(). Por padrão, para limites compartilhados aninhados, clipInOverlayDuringTransition usa o caminho de recorte do sharedBounds() pai.

Para manter elementos de interface específicos, como uma barra inferior ou um botão de ação flutuante, sempre na parte de cima durante uma transição de elemento compartilhado, use Modifier.renderInSharedTransitionScopeOverlay(). Por padrão, esse modificador mantém o conteúdo na sobreposição durante o período em que a transição compartilhada está ativa.

Por exemplo, no Jetsnack, a BottomAppBar precisa ser colocada na parte de cima do elemento compartilhado até que a tela não esteja visível. A adição do modificador ao elemento combinável o mantém elevado.

Sem Modifier.renderInSharedTransitionScopeOverlay()

Com Modifier.renderInSharedTransitionScopeOverlay()

Talvez você queira que o elemento combinável não compartilhado seja animado e permaneça na parte de cima dos outros elementos combináveis antes da transição. Nesses casos, use renderInSharedTransitionScopeOverlay().animateEnterExit() para animar o elemento combinável à medida que a transição de elemento compartilhado é executada:

JetsnackBottomBar(
    modifier = Modifier
        .renderInSharedTransitionScopeOverlay(
            zIndexInOverlay = 1f,
        )
        .animateEnterExit(
            enter = fadeIn() + slideInVertically {
                it
            },
            exit = fadeOut() + slideOutVertically {
                it
            }
        )
)

Figura 2. Barra de apps inferior deslizando para dentro e para fora à medida que a animação faz a transição.

No caso raro de você querer que o elemento compartilhado não seja renderizado em uma sobreposição, defina renderInOverlayDuringTransition em sharedElement() como "false".

Notificar layouts irmãos sobre mudanças no tamanho do elemento compartilhado

Por padrão, sharedBounds() e sharedElement() não notificam o contêiner pai sobre mudanças de tamanho à medida que o layout faz a transição.

Para propagar mudanças de tamanho para o contêiner pai à medida que ele faz a transição, mude o parâmetro placeholderSize para PlaceholderSize.AnimatedSize. Isso faz com que o item cresça ou diminua. Todos os outros itens no layout respondem à mudança.

PlaceholderSize.ContentSize (padrão)

PlaceholderSize.AnimatedSize

(Observe como os outros itens na lista se movem para baixo em resposta ao crescimento de um item)