Personalizar a transição de elementos compartilhados

Para personalizar como a animação de transição de elementos compartilhados é executada, há alguns parâmetros que podem ser usados para mudar a forma como os elementos compartilhados fazem a transição.

Especificação da animação

Para mudar a especificação de 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 inicial de Rect e a posição de destino de Rect.

Por exemplo, para fazer o texto no exemplo anterior se mover 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, é possível 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. O ScaleToBounds primeiro mede o layout filho com as restrições de antecipação (ou de destino). Em seguida, o layout estável do filho é dimensionado para caber nos limites compartilhados. ScaleToBounds pode ser considerado uma "escala gráfica" entre os estados.

Por outro lado, RemeasureToBounds mede e define novamente o layout do elemento secundário de sharedBounds com restrições fixas animadas com base no tamanho de destino. A remedição é acionada pela mudança de tamanho dos limites, o que pode acontecer em cada frame.

Para elementos combináveis Text, recomendamos o uso de ScaleToBounds, porque ele evita o novo layout e o refluxo de texto em linhas diferentes. RemeasureToBounds é recomendado para limites com 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 dinamicamente 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, somente de A para B), enquanto a desativa 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 vai concluir a animação atual para evitar a remoção acidental de animações em andamento. Se você precisar remover o elemento enquanto a animação estiver em andamento, substitua shouldKeepEnabledForOngoingAnimation na interface SharedContentConfig para retornar falso.

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 é redistribuído conforme o contêiner aumenta de tamanho. No segundo exemplo, o texto não é ajustado conforme cresce. Adicionar Modifier.skipToLookaheadSize() impede o refluxo à medida que ele cresce.

Sem Modifier.skipToLookaheadSize(): observe o refluxo do texto "Lorem Ipsum"

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

Clipes e sobreposições

Para que os elementos compartilhados sejam compartilhados entre diferentes elementos combináveis, a renderização do elemento combinável é elevada para 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, o alfa e a escala).

Ele será renderizado sobre outros elementos da interface não compartilhados. Quando a transição for concluída, o elemento será removido da sobreposição para o próprio DrawScope.

Para cortar um elemento compartilhado em uma forma, use a função Modifier.clip() padrão. Coloque-o depois de 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 específicos da interface, 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 tempo em que a transição compartilhada está ativa.

Por exemplo, no Jetsnack, o BottomAppBar precisa ser colocado sobre o elemento compartilhado até que a tela não esteja mais visível. Adicionar o 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 sobre os outros elementos antes da transição. Nesses casos, use renderInSharedTransitionScopeOverlay().animateEnterExit() para animar o elemento que pode ser composto enquanto a transição do elemento compartilhado é executada:

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

Figura 2. Barra de apps de fundo deslizando para dentro e para fora conforme a animação faz a transição.

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

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 durante as transições de layout.

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

PlaceholderSize.ContentSize (padrão)

PlaceholderSize.AnimatedSize

(Observe como os outros itens da lista se movem para baixo em resposta ao crescimento do item)