공유 요소를 사용한 탐색

그림 1. 공유 요소를 사용한 탐색

공유 요소는 사용자를 안내하는 시각적 연결을 만들어 화면 간 전환을 더 부드럽고 매력적으로 만듭니다. 이 가이드에서는 Navigation 3Navigation 2 Jetpack 라이브러리 모두에서 공유 요소 API를 사용하는 방법을 보여줍니다.

다음 스니펫에는 사용자가 탐색할 수 있는 대상으로 사용되는 DetailsScreenHomeScreen 컴포저블이 포함되어 있습니다. 각 화면 내에서 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,
                            )
                    )
                }
            }
        }
    }
}

Navigation 3에서 공유 요소 API를 사용하려면 먼저 앱의 NavDisplaySharedTransitionLayout로 래핑해야 합니다. 그런 다음 제공된 SharedTransitionScope를 화면 컴포저블에 전달할 수 있습니다.

AnimatedVisibilityScope의 경우 NavDisplay가 내부적으로 사용하여 장면 간에 애니메이션을 적용하는 AnimatedContent에서 AnimatedContentScope를 제공하는 LocalNavAnimatedContentScope 컴포지션 로컬을 사용합니다.

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

Navigation 2에서 공유 요소 API를 사용하려면 먼저 앱의 NavHostSharedTransitionLayout로 래핑해야 합니다. 그런 다음 제공된 SharedTransitionScope를 화면 컴포저블에 전달할 수 있습니다.

composable 빌더의 content 매개변수는 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의 경우 navigation-compose2.8.0-alpha02 출시 이상을 사용하세요.

    [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 이하를 타겟팅하는 경우 AndroidManifest.xml 파일의 <application> 또는 특정 <activity> 요소에 android:enableOnBackInvokedCallback="true"를 추가해야 합니다. 앱이 Android 15 이상을 타겟팅하는 경우 이 플래그가 필요하지 않습니다.

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
      <application
          ...
          android:enableOnBackInvokedCallback="true">
      </application>
    </manifest>
    
그림 2. 뒤로 탐색 예측을 사용한 공유 요소