Navegación con elementos compartidos

Figura 1. Navegación con elementos compartidos.

Los elementos compartidos hacen que las transiciones entre pantallas sean más fluidas y atractivas, ya que crean una conexión visual que guía al usuario. En esta guía, se muestra cómo usar las APIs de elementos compartidos con las bibliotecas de Jetpack Navigation 3 y Navigation 2.

El siguiente fragmento incluye elementos DetailsScreen y HomeScreen componibles que funcionan como los destinos entre los que los usuarios pueden navegar. Dentro de cada pantalla, el modificador sharedElement se usa tanto en la imagen como en el texto para que cada uno de esos elementos se anime de forma independiente entre las pantallas.

@Composable
fun DetailsScreen(
    id: Int,
    snack: Snack,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope,
    onBackPressed: () -> Unit
) {
    with(sharedTransitionScope) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .clickable { onBackPressed() },
        ) {
            Image(
                painterResource(id = snack.image),
                contentDescription = snack.description,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .sharedElement(
                        sharedTransitionScope.rememberSharedContentState(key = "image-$id"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
                    .aspectRatio(1f)
                    .fillMaxWidth()
            )
            Text(
                text = snack.name,
                fontSize = 18.sp,
                modifier = Modifier
                    .sharedElement(
                        sharedTransitionScope.rememberSharedContentState(key = "text-$id"),
                        animatedVisibilityScope = animatedVisibilityScope
                    )
                    .fillMaxWidth(),
            )
        }
    }
}

@Composable
fun HomeScreen(
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope,
    onItemClick: (Int) -> Unit,
) {
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        itemsIndexed(listSnacks) { index, item ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { onItemClick(index) },
            ) {
                Spacer(modifier = Modifier.width(8.dp))
                with(sharedTransitionScope) {
                    Image(
                        painterResource(id = item.image),
                        contentDescription = item.description,
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .sharedElement(
                                sharedTransitionScope.rememberSharedContentState(key = "image-$index"),
                                animatedVisibilityScope = animatedVisibilityScope
                            )
                            .size(100.dp)
                    )
                    Spacer(modifier = Modifier.width(8.dp))
                    Text(
                        item.name,
                        fontSize = 18.sp,
                        modifier = Modifier
                            .align(Alignment.CenterVertically)
                            .sharedElement(
                                sharedTransitionScope.rememberSharedContentState(key = "text-$index"),
                                animatedVisibilityScope = animatedVisibilityScope,
                            )
                    )
                }
            }
        }
    }
}

Para usar las APIs de elementos compartidos con Navigation 3, primero debes unir el NavDisplay de tu app en un SharedTransitionLayout. Luego, puedes pasar el SharedTransitionScope proporcionado a los elementos componibles de la pantalla.

Para AnimatedVisibilityScope, usa la composición local LocalNavAnimatedContentScope que proporciona AnimatedContentScope desde AnimatedContent que NavDisplay usa internamente para animar entre escenas.

@Composable
fun SharedElement_Nav3() {
    SharedTransitionLayout {
        val backStack = rememberNavBackStack(HomeRoute)

        // Note: NavDisplay accepts a `sharedTransitionScope` parameter, which is used to animate
        // NavEntry instances between scenes. This parameter *isn't* required for shared element
        // or shared bounds transitioning elements between different NavEntry, as demonstrated in
        // this sample.
        // See https://developer.android.com/guide/navigation/navigation-3/animate-destinations#transition-nav-entries
        NavDisplay(
            modifier = Modifier.safeDrawingPadding(),
            backStack = backStack,
            entryProvider = entryProvider {
                entry<HomeRoute> {
                    HomeScreen(
                        sharedTransitionScope = this@SharedTransitionLayout,
                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
                        onItemClick = { backStack.add(DetailsRoute(it)) })
                }
                entry<DetailsRoute> { detailsRoute ->
                    val id = detailsRoute.item
                    val snack = listSnacks[id]

                    DetailsScreen(
                        id = id,
                        snack = snack,
                        sharedTransitionScope = this@SharedTransitionLayout,
                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
                        onBackPressed = {
                            backStack.removeLastOrNull()
                        },
                    )
                }
            })
    }
}

Para usar las APIs de elementos compartidos con Navigation 2, primero debes unir el NavHost de tu app en un SharedTransitionLayout. Luego, puedes pasar el SharedTransitionScope proporcionado a los elementos componibles de la pantalla.

El parámetro content del compilador composable usa AnimatedContentScope como receptor, por lo que puedes usar this@composable para hacer referencia a ese alcance.

@Composable
fun SharedElement_Nav2() {
    SharedTransitionLayout {
        val navController = rememberNavController()
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.safeDrawingPadding()
        ) {
            composable("home") {
                HomeScreen(
                    sharedTransitionScope = this@SharedTransitionLayout,
                    animatedVisibilityScope = this@composable,
                    onItemClick = { navController.navigate("details/$it") })
            }
            composable(
                "details/{item}", arguments = listOf(navArgument("item") { type = NavType.IntType })
            ) { backStackEntry ->
                val id = backStackEntry.arguments?.getInt("item") ?: 0
                val snack = listSnacks[id]
                DetailsScreen(
                    id = id,
                    snack = snack,
                    sharedTransitionScope = this@SharedTransitionLayout,
                    animatedVisibilityScope = this@composable,
                    onBackPressed = {
                        navController.popBackStack()
                    }
                )
            }
        }
    }
}

Atrás predictivo con elementos compartidos

Para usar el atrás predictivo con elementos compartidos, sigue estos pasos:

  1. Todas las versiones de Navigation 3 admiten el gesto atrás predictivo. Para Navigation 2, usa la versión 2.8.0-alpha02 de navigation-compose o una más reciente:

    [versions]
    androidx-navigation = "2.8.0-alpha02" # Or newer
    
    [libraries]
    androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
    
    dependencies {
        implementation(libs.androidx.navigation.compose)
    }
    
  2. Las animaciones de atrás predictivo están habilitadas de forma predeterminada en dispositivos que ejecutan Android 15 (nivel de API 35) o versiones posteriores. En los dispositivos que ejecutan Android 14 (nivel de API 34), debes habilitar el parámetro de configuración de retroceso predictivo en las opciones para desarrolladores.

  3. Si tu app se segmenta para Android 14 o versiones anteriores, debes agregar android:enableOnBackInvokedCallback="true" a los elementos <application> o <activity> específicos en tu archivo AndroidManifest.xml. No necesitas esta marca si tu app se segmenta para Android 15 o versiones posteriores.

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
      <application
          ...
          android:enableOnBackInvokedCallback="true">
      </application>
    </manifest>
    
Figura 2. Elementos compartidos con atrás predictivo.