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

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

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

קטע הקוד הבא כולל את רכיבי ה-Composable‏ DetailsScreen ו-HomeScreen שמשמשים כיעדים שהמשתמשים יכולים לנווט ביניהם. בכל מסך, משתמשים במגדיר 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, משתמשים ב-LocalNavAnimatedContentScope composition local שמספק את 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 של בונה 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 תומכות בתכונה 'חזרה חזויה'. בניווט 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. רכיבים משותפים עם חיזוי החזרה.