Nawigacja z elementami współdzielonymi

Rysunek 1. Nawigacja z elementami współdzielonymi.

Wspólne elementy sprawiają, że przejścia między ekranami są płynniejsze i bardziej angażujące, ponieważ tworzą wizualne połączenie, które prowadzi użytkownika. Z tego przewodnika dowiesz się, jak używać interfejsów API elementów wspólnych w bibliotekach Jetpack Navigation 3Navigation 2.

Poniższy fragment kodu zawiera funkcje kompozycyjne DetailsScreenHomeScreen, które służą jako miejsca docelowe, między którymi mogą się poruszać użytkownicy. Na każdym ekranie modyfikator sharedElement jest używany zarówno w przypadku obrazu, jak i tekstu, dzięki czemu każdy z tych elementów jest animowany niezależnie między ekranami.

@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,
                            )
                    )
                }
            }
        }
    }
}

Aby korzystać z interfejsów API elementów udostępnionych w przypadku Navigation 3, musisz najpierw umieścić element NavDisplay aplikacji w elemencie SharedTransitionLayout. Następnie możesz przekazać podany SharedTransitionScope do funkcji kompozycyjnych ekranu.

W przypadku AnimatedVisibilityScope użyj lokalnego komponentu LocalNavAnimatedContentScope, który udostępnia AnimatedContentScope z komponentu AnimatedContent, który NavDisplay jest używany wewnętrznie do animowania przejść między scenami.

@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()
                        },
                    )
                }
            })
    }
}

Aby korzystać z interfejsów API elementów udostępnionych w Navigation 2, musisz najpierw umieścić element NavHost aplikacji w elemencie SharedTransitionLayout. Następnie możesz przekazać podany element SharedTransitionScope do funkcji kompozycyjnych ekranu.

Parametr content kreatora composable używa AnimatedContentScope jako odbiorcy, więc możesz użyć this@composable, aby odwołać się do tego zakresu.

@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()
                    }
                )
            }
        }
    }
}

Przewidywane przejście wstecz ze wspólnymi elementami

Aby używać przewidywanego przejścia wstecz ze wspólnymi elementami, wykonaj te czynności:

  1. Wszystkie wersje Navigation 3 obsługują przewidywane przejście wstecz. W przypadku Navigation 2 użyj wersji 2.8.0-alpha02 navigation-compose lub nowszej:

    [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. Animacje przewidywanego przejścia wstecz są domyślnie włączone na urządzeniach z Androidem 15 (poziom interfejsu API 35) lub nowszym. W przypadku urządzeń z Androidem 14 (poziom interfejsu API 34) musisz włączyć ustawienie przewidywanego przejścia wstecz w Opcjach programisty.

  3. Jeśli Twoja aplikacja jest kierowana na Androida 14 lub starszego, musisz dodać element android:enableOnBackInvokedCallback="true" do elementu <application> lub konkretnych elementów <activity> w pliku AndroidManifest.xml. Jeśli Twoja aplikacja jest kierowana na Androida 15 lub nowszego, nie potrzebujesz tej flagi.

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
      <application
          ...
          android:enableOnBackInvokedCallback="true">
      </application>
    </manifest>
    
Rysunek 2. Wspólne elementy z przewidywanym przejściem wstecz.