Personaliza la transición de elementos compartidos

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

Especificación de la 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 destino de Rect.

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 de keyframes.

Figura 1. Ejemplo en el que se muestran diferentes parámetros de boundsTransform

Modo de cambio de tamaño

Cuando se anima entre dos límites compartidos, puedes establecer el parámetro resizeMode en RemeasureToBounds o ScaleToBounds. Este parámetro determina cómo se produce la transición del elemento compartido entre los dos estados. ScaleToBounds primero mide el diseño secundario con las restricciones de anticipación (o de destino). Luego, el diseño estable del elemento secundario se escala para que se ajuste a los límites compartidos. ScaleToBounds se puede considerar como una "escala gráfica" entre los estados.

En cambio, RemeasureToBounds vuelve a medir y a diseñar el diseño secundario de sharedBounds con restricciones fijas animadas según el tamaño objetivo. La nueva medición se activa por el cambio de tamaño de los límites, que podría ser cada fotograma.

Para los elementos de componibilidad Text, se recomienda ScaleToBounds, ya que evita que se vuelva a diseñar y redistribuir el texto en diferentes líneas. Se recomienda RemeasureToBounds para los límites que tienen diferentes relaciones de aspecto y si quieres una continuidad fluida entre los dos elementos compartidos.

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

ScaleToBounds

RemeasureToBounds

Habilita y, también, inhabilita de forma dinámica los elementos compartidos

De forma predeterminada, sharedElement() y sharedBounds() están configurados para animar los cambios de diseño cada vez que se encuentra una clave coincidente en el estado de destino. Sin embargo, es posible que quieras inhabilitar esta animación de forma dinámica según condiciones específicas, como la dirección de navegación o el estado actual de la IU.

Para controlar si se produce la transición de elementos compartidos, puedes personalizar el SharedContentConfig que se pasa a rememberSharedContentState(). La propiedad isEnabled determina si el elemento compartido está activo.

En el siguiente ejemplo, se muestra cómo definir una configuración que solo habilite la transición compartida cuando se navega entre pantallas específicas (p.ej., solo de A a B) y la inhabilite para otras.

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
                }
            }
        }
    }
}

De forma predeterminada, si se inhabilita un elemento compartido durante una animación en curso, se completa la animación actual para evitar que se quiten accidentalmente las animaciones en curso. Si necesitas quitar el elemento mientras la animación está en curso, puedes anular shouldKeepEnabledForOngoingAnimation en la interfaz SharedContentConfig para que se devuelva un valor falso.

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 ilustra el texto de descripción "Lorem Ipsum" que ingresa a la pantalla de dos maneras diferentes. En el primer ejemplo, el texto se redistribuye a medida que ingresa, ya que el contenedor aumenta de tamaño. En el segundo ejemplo, el texto no se ajusta a medida que crece. Agregar Modifier.skipToLookaheadSize() evita el reflujo a medida que crece.

No Modifier.skipToLookaheadSize(): Observa cómo se redistribuye el texto "Lorem Ipsum"

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

Clip y superposiciones

Para que los elementos compartidos se compartan entre diferentes elementos componibles, la renderización del elemento componible se eleva a una superposición de capa cuando se inicia la transición a su coincidencia en el destino. El efecto de esto es que escapará de los límites del elemento superior y de las transformaciones de su capa (por ejemplo, el alfa y la escala).

Se renderizará sobre otros elementos de la IU que no se compartan. Una vez que finalice la transición, el elemento se quitará 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 establecer clipInOverlayDuringTransition en sharedElement(). De forma predeterminada, para los límites compartidos anidados, clipInOverlayDuringTransition usa la ruta de recorte del sharedBounds() superior.

Para mantener elementos específicos de la IU, como una barra inferior o un botón de acción flotante, siempre en la parte superior 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, el BottomAppBar debe colocarse sobre el elemento compartido hasta que la pantalla no esté visible. Agregar el modificador al elemento componible lo mantiene elevado.

Sin Modifier.renderInSharedTransitionScopeOverlay()

Con Modifier.renderInSharedTransitionScopeOverlay()

Es posible que desees que tu elemento componible no compartido se anime y permanezca sobre los demás elementos componibles antes de la transición. En esos casos, usa renderInSharedTransitionScopeOverlay().animateEnterExit() para animar el elemento de componibilidad mientras 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. La barra inferior de la app se desliza hacia adentro y hacia afuera a medida que se produce la transición de la animación.

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

Notifica a los diseños hermanos sobre los cambios en el tamaño del elemento compartido

De forma predeterminada, sharedBounds() y sharedElement() no notifican al contenedor superior sobre ningún cambio de tamaño a medida que se produce la transición del diseño.

Para propagar los cambios de tamaño al contenedor superior a medida que se realiza la transición, cambia el parámetro placeholderSize a PlaceholderSize.AnimatedSize. De esta manera, el elemento se agrandará o se achicará. Todos los demás elementos del diseño responden al cambio.

PlaceholderSize.ContentSize (predeterminada)

PlaceholderSize.AnimatedSize

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