Personaliza la transición de elementos compartidos

Para personalizar la forma en que se ejecuta la animación de transición de elementos compartidos, hay algunos parámetros que se pueden usar para cambiar la transición de los elementos compartidos.

Especificación de animación

Para cambiar la especificación de animación que se usa para el movimiento de tamaño y posición, puedes especificar un parámetro boundsTransform diferente en Modifier.sharedElement(). Esto proporciona la posición inicial de Rect y la posición de Rect de destino.

Por ejemplo, para que el texto del ejemplo anterior se mueva con un movimiento de arco, especifica el parámetro boundsTransform para usar una especificación 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
    )
)

Puedes usar cualquier AnimationSpec. En este ejemplo, se usa una especificación keyframes.

Figura 1: Ejemplo que muestra diferentes parámetros de boundsTransform

Modo de cambio de tamaño

Cuando creas animaciones entre dos límites compartidos, puedes configurar el parámetro resizeMode como RemeasureToBounds o ScaleToBounds. Este parámetro determina cómo el elemento compartido pasa entre los dos estados. ScaleToBounds primero mide el diseño secundario con las restricciones de visualización anticipada (o objetivo). Luego, el diseño estable del elemento secundario se escala para adaptarse a los límites compartidos. ScaleToBounds se puede considerar como una "escala gráfica" entre los estados.

Por otro lado, RemeasureToBounds vuelve a medir y rediseñar el diseño secundario de sharedBounds con restricciones fijas animadas en función del tamaño de destino. La nueva medición se activa mediante el cambio de tamaño de los límites, que podría ser en cada fotograma.

Para los elementos componibles Text, se recomienda ScaleToBounds, ya que evitará el rediseño y el reprocesamiento del texto en diferentes líneas. Para los límites que son diferentes relaciones de aspecto y si deseas una continuidad fluida entre los dos elementos compartidos, se recomienda RemeasureToBounds.

La diferencia entre los dos modos de cambio de tamaño se puede ver en los siguientes ejemplos:

ScaleToBounds

RemeasureToBounds

Ir al diseño final

De forma predeterminada, cuando se realiza la transición entre dos diseños, el tamaño del diseño se anima entre su estado inicial y final. Este puede ser un comportamiento no deseado cuando se anima contenido como texto.

En el siguiente ejemplo, se muestra el texto de la descripción "Lorem Ipsum" que se ingresa a la pantalla de dos maneras diferentes. En el primer ejemplo, el texto se reprocesa a medida que ingresa a medida que el contenedor aumenta de tamaño, mientras que en el segundo ejemplo, el texto no se reprocesa a medida que crece. Agregar Modifier.skipToLookaheadSize() evita el reprocesamiento a medida que crece.

No hay Modifier.skipToLookahead(): Observa el reprocesamiento de texto "Lorem Ipsum".

Modifier.skipToLookahead(): Observa que el texto "Lorem Ipsum" mantiene su estado final al inicio de la animación.

Recorte y superposiciones

Un concepto importante a la hora de crear elementos compartidos en Compose es que, para que puedan compartir entre diferentes elementos componibles, la renderización de este elemento se eleva a una superposición de capas cuando comienza la transición con su coincidencia en el destino. El efecto de esto es que escapará los límites del elemento superior y las transformaciones de sus capas (por ejemplo, el valor alfa y la escala).

Se renderizará sobre otros elementos de la IU no compartidos. Una vez que finalice la transición, el elemento se descartará de la superposición y se colocará en su propio DrawScope.

Para recortar un elemento compartido en una forma, usa la función Modifier.clip() estándar. Colócalo después 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
)

Si necesitas asegurarte de que un elemento compartido nunca se renderice fuera de un contenedor superior, puedes configurar clipInOverlayDuringTransition en sharedElement(). De forma predeterminada, para los límites compartidos anidados, clipInOverlayDuringTransition usa la ruta de recorte del sharedBounds() superior.

Para permitir que los elementos específicos de la IU (como una barra inferior o un botón de acción flotante) se mantengan siempre arriba durante una transición de elementos compartidos, usa Modifier.renderInSharedTransitionScopeOverlay(). De forma predeterminada, este modificador mantiene el contenido en la superposición durante el tiempo en que la transición compartida está activa.

Por ejemplo, en Jetsnack, BottomAppBar debe colocarse sobre el elemento compartido hasta el momento en que la pantalla no esté visible. Agregar el modificador al elemento componible lo mantiene elevado.

Sin Modifier.renderInSharedTransitionScopeOverlay()

Con Modifier.renderInSharedTransitionScopeOverlay()

A veces, es posible que quieras que el elemento componible no compartido se anime y permanezca encima de los otros elementos antes de la transición. En esos casos, usa renderInSharedTransitionScopeOverlay().animateEnterExit() para animar el elemento componible a medida que se ejecuta la transición de elementos compartidos:

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

Figura 2: Barra de la app inferior que se desliza hacia adentro y afuera a medida que la animación transiciona

En el caso poco probable de que desees que el elemento compartido no se renderice en una superposición, puedes establecer el renderInOverlayDuringTransition de sharedElement() como falso.

Notificar diseños del mismo nivel en relación con los cambios en el tamaño de los elementos compartidos

De forma predeterminada, sharedBounds() y sharedElement() no notifican al contenedor superior sobre ningún cambio de tamaño durante las transiciones del diseño.

Para propagar los cambios de tamaño al contenedor superior durante la transición, cambia el parámetro placeHolderSize a PlaceHolderSize.animatedSize. Si lo haces, el elemento se agranda o achica. Todos los demás elementos del diseño responden al cambio.

PlaceholderSize.contentSize (predeterminado)

PlaceholderSize.animatedSize

(Observa cómo los otros elementos de la lista se mueven hacia abajo en respuesta al crecimiento del elemento).