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 a transição desses elementos é feita.

Especificações de animação

Para mudar a especificação de animação usada para o movimento de tamanho e posição, é possível especificar um parâmetro boundsTransform diferente em Modifier.sharedElement(). Isso fornece a posição Rect inicial e a posição Rect desejada.

Por exemplo, para fazer com que o texto do 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. Neste exemplo, uma especificação keyframes é usada.

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. O ScaleToBounds mede primeiro o layout filho com as restrições de visualização antecipada (ou destino). Em seguida, o layout estável do filho é dimensionado para caber nos limites compartilhados. ScaleToBounds pode ser considerada uma "escala gráfica" entre os estados.

Enquanto RemeasureToBounds mede e refaz o layout do layout filho de sharedBounds com restrições fixas animadas com base no tamanho de destino. A resmedição é acionada pela mudança do tamanho dos limites, que pode ser todos os frames.

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

A diferença entre os dois modos de redimensionamento é mostrada nos exemplos a seguir:

ScaleToBounds

RemeasureToBounds

Pular para o layout final

Por padrão, ao fazer a transição entre dois layouts, o tamanho dele é animado entre o estado inicial e final. Esse pode ser um comportamento 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 é reorganizado à medida que o contêiner aumenta de tamanho. No segundo, o texto não sofre reflow à medida que cresce. A adição de Modifier.skipToLookaheadSize() impede o reflow à medida que ela cresce.

Sem Modifier.skipToLookahead(). Observe o reflow do texto "Lorem Ipsum"

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

Recorte e sobreposições

Um conceito importante ao criar elementos compartilhados no Compose é que, para que eles 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 dele (por exemplo, alfa e escala).

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

Para recortar um elemento compartilhado em uma forma, use a função Modifier.clip() padrão. Coloque-o depois do 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
)

Caso você precise 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 corte do sharedBounds() pai.

Para manter elementos da 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 elementos compartilhados, 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, a BottomAppBar precisa ser colocada sobre o elemento compartilhado até que a tela não esteja visível. Adicionar o modificador ao elemento combinável o mantém elevado.

Sem Modifier.renderInSharedTransitionScopeOverlay()

Com Modifier.renderInSharedTransitionScopeOverlay()

Às vezes, você pode querer que o elemento combinável não compartilhado seja animado e permaneça sobre outros elementos antes da transição. Nesses casos, use renderInSharedTransitionScopeOverlay().animateEnterExit() para animar o elemento combinável à medida que 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 inferior deslizando para dentro e para fora durante a transição da animação

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

Notificar layouts irmãos de 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 a transição do layout.

Para propagar mudanças de tamanho para o contêiner pai durante a transição, altere 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 na lista se movem para baixo em resposta ao aumento de um item)