ניווט עם אלמנטים משותפים

איור 1. ניווט עם רכיבים משותפים.

רכיבים משותפים הופכים את המעברים בין המסכים לחלקים ומושכים יותר, על ידי יצירת חיבור חזותי שמנחה את המשתמש. במדריך הזה מוסבר איך להשתמש בממשקי ה-API של הרכיבים המשותפים עם ספריות Jetpack של Navigation 3 ו-Navigation 2.

קטע הקוד הבא כולל את הפונקציות הקומפוזביליות DetailsScreen ו-HomeScreen, שמשמשות כיעדים שהמשתמשים יכולים לנווט ביניהם. בכל מסך, משתמשים ב-modifier‏ sharedElement גם בתמונה וגם בטקסט, כדי שכל אחד מהרכיבים האלה יציג אנימציה באופן עצמאי בין המסכים.

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

כדי להשתמש בממשקי ה-API של הרכיבים המשותפים עם Navigation 3, קודם צריך לעטוף את ה-NavDisplay של האפליקציה ב-SharedTransitionLayout. לאחר מכן אפשר להעביר את ה-SharedTransitionScope שסופק לרכיבי ה-Composable של המסך.

ב-AnimatedVisibilityScope, משתמשים ב-CompositionLocal של LocalNavAnimatedContentScope שמספק את AnimatedContentScope מ-AnimatedContent ש-NavDisplay משתמש בו באופן פנימי כדי להוסיף אנימציה בין סצנות.

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

כדי להשתמש בממשקי ה-API של הרכיבים המשותפים עם Navigation 2, קודם צריך לעטוף את ה-NavHost של האפליקציה ב-SharedTransitionLayout. לאחר מכן, אפשר להעביר את ה-SharedTransitionScope שסופק לפונקציות ה-composable של המסך.

הפרמטר content של ה-builder‏ composable משתמש ב-AnimatedContentScope כמקבל, כך שאפשר להשתמש ב-this@composable כדי להפנות להיקף הזה.

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

חיזוי החזרה עם רכיבים משותפים

כדי להשתמש בחיזוי החזרה עם רכיבים משותפים, פועלים לפי השלבים הבאים:

  1. כל הגרסאות של Navigation 3 תומכות בחיזוי החזרה. ב-Navigation 2, צריך להשתמש בגרסה 2.8.0-alpha02 של navigation-compose או בגרסה חדשה יותר:

    [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. האנימציות של חיזוי החזרה מופעלות כברירת מחדל במכשירים עם Android בגרסה 15 (רמת API‏ 35) ומעלה. במכשירים עם Android 14 (רמת API‏ 34), צריך להפעיל את ההגדרה "חיזוי החזרה" באפשרויות למפתחים.

  3. אם האפליקציה שלכם מטרגטת את Android מגרסה 14 ומטה, אתם צריכים להוסיף את android:enableOnBackInvokedCallback="true" לרכיבים <application> או לרכיבים ספציפיים של <activity> בקובץ AndroidManifest.xml. אתם לא צריכים את הדגל הזה אם האפליקציה שלכם מטרגטת את Android מגרסה 15 ואילך.

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
      <application
          ...
          android:enableOnBackInvokedCallback="true">
      </application>
    </manifest>
    
איור 2. רכיבים משותפים עם חיזוי החזרה.