지원 창 레이아웃 빌드

지원 창 레이아웃은 사용자가 앱의 기본 콘텐츠에 집중할 수 있도록 하면서 관련 지원 정보를 표시합니다. 예를 들어 기본 창에는 영화에 관한 세부정보가 표시되고 보조 창에는 유사한 영화, 동일한 감독의 영화 또는 동일한 배우가 출연한 작품이 표시될 수 있습니다.

자세한 내용은 Material 3 지원 창 가이드라인을 참고하세요.

스캐폴드로 지원 창 구현

NavigableSupportingPaneScaffold는 Jetpack Compose에서 지원 창 레이아웃 구현을 간소화하는 컴포저블입니다. SupportingPaneScaffold를 래핑하고 기본 제공 탐색 및 뒤로 탐색 예측 처리를 추가합니다.

지원 창 스캐폴드는 최대 3개의 창을 지원합니다.

  • 기본 창: 기본 콘텐츠를 표시합니다.
  • 지원 창: 기본 창과 관련된 추가 컨텍스트 또는 도구를 제공합니다.
  • 추가 창 (선택사항): 필요한 경우 보조 콘텐츠에 사용됩니다.

스캐폴드는 창 크기에 따라 조정됩니다.

  • 큰 창에서는 기본 창과 지원 창이 나란히 표시됩니다.
  • 작은 창에서는 한 번에 하나의 창만 표시되며 사용자가 탐색할 때 전환됩니다.

    기본 콘텐츠가 디스플레이의 대부분을 차지하고 지원 콘텐츠가 함께 표시됩니다.
    그림 1. 지원 창 레이아웃

종속 항목 추가

NavigableSupportingPaneScaffold은 Material 3 적응형 레이아웃 라이브러리의 일부입니다.

앱 또는 모듈의 build.gradle 파일에 다음과 같은 세 가지 관련 종속 항목을 추가합니다.

Kotlin

implementation("androidx.compose.material3.adaptive:adaptive")
implementation("androidx.compose.material3.adaptive:adaptive-layout")
implementation("androidx.compose.material3.adaptive:adaptive-navigation")

Groovy

implementation 'androidx.compose.material3.adaptive:adaptive'
implementation 'androidx.compose.material3.adaptive:adaptive-layout'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation'
  • adaptive: HingeInfo 및 Posture와 같은 하위 수준 구성요소

  • adaptive-layout: ListDetailPaneScaffold 및 SupportingPaneScaffold와 같은 적응형 레이아웃

  • adaptive-navigation: 창 내부 및 창 간 탐색을 위한 컴포저블과 기본적으로 탐색을 지원하는 적응형 레이아웃(예: NavigableListDetailPaneScaffold 및 NavigableSupportingPaneScaffold)

프로젝트에 compose-material3-adaptive 버전 1.1.0-beta1 이상이 포함되어 있는지 확인합니다.

뒤로 탐색 예측 동작 선택

Android 15 이하에서 뒤로 탐색 예측 애니메이션을 사용 설정하려면 뒤로 탐색 예측 동작을 지원하도록 선택해야 합니다. 선택하려면 AndroidManifest.xml 파일 내의 <application> 태그 또는 개별 <activity> 태그에 android:enableOnBackInvokedCallback="true"를 추가합니다.

앱이 Android 16 (API 수준 36) 이상을 타겟팅하면 뒤로 탐색 예측이 기본적으로 사용 설정됩니다.

내비게이터 만들기

작은 창에서는 한 번에 하나의 창만 표시되므로 ThreePaneScaffoldNavigator를 사용하여 창 간에 이동합니다. rememberSupportingPaneScaffoldNavigator를 사용하여 내비게이터 인스턴스를 만듭니다.

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

scaffold에 탐색기 전달

스캐폴드에는 스캐폴드의 상태를 나타내는 인터페이스인 ThreePaneScaffoldNavigator, ThreePaneScaffoldValue, PaneScaffoldDirective가 필요합니다.

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = { /*...*/ },
    supportingPane = { /*...*/ },
)

기본 창과 지원 창은 콘텐츠가 포함된 컴포저블입니다. AnimatedPane를 사용하여 탐색 중에 기본 창 애니메이션을 적용합니다. 스캐폴드 값을 사용하여 지원 창이 숨겨져 있는지 확인합니다. 숨겨져 있다면 navigateTo(SupportingPaneScaffoldRole.Supporting)를 호출하는 버튼을 표시하여 지원 창을 표시합니다.

대형 화면의 경우 ThreePaneScaffoldNavigator.navigateBack() 메서드를 사용하여 지원 창을 닫고 BackNavigationBehavior.PopUntilScaffoldValueChange 상수를 전달합니다. 이 메서드를 호출하면 NavigableSupportingPaneScaffold의 재구성이 강제됩니다. 리컴포지션 중에 ThreePaneScaffoldNavigator.currentDestination 속성을 확인하여 지원 창을 표시할지 결정합니다.

다음은 스캐폴드의 전체 구현입니다.

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()
val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        AnimatedPane(
            modifier = Modifier
                .safeContentPadding()
                .background(Color.Red)
        ) {
            if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden) {
                Button(
                    modifier = Modifier
                        .wrapContentSize(),
                    onClick = {
                        scope.launch {
                            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting)
                        }
                    }
                ) {
                    Text("Show supporting pane")
                }
            } else {
                Text("Supporting pane is shown")
            }
        }
    },
    supportingPane = {
        AnimatedPane(modifier = Modifier.safeContentPadding()) {
            Column {
                // Allow users to dismiss the supporting pane. Use back navigation to
                // hide an expanded supporting pane.
                if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
                    // Material design principles promote the usage of a right-aligned
                    // close (X) button.
                    IconButton(
                        modifier =  Modifier.align(Alignment.End).padding(16.dp),
                        onClick = {
                            scope.launch {
                                scaffoldNavigator.navigateBack(backNavigationBehavior)
                            }
                        }
                    ) {
                        Icon(Icons.Default.Close, contentDescription = "Close")
                    }
                }
                Text("Supporting pane")
            }

        }
    }
)

창 컴포저블 추출

SupportingPaneScaffold의 개별 창을 자체 컴포저블로 추출하여 재사용 및 테스트 가능하게 만듭니다. 기본 애니메이션을 원하는 경우 ThreePaneScaffoldScope를 사용하여 AnimatedPane에 액세스합니다.

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.MainPane(
    shouldShowSupportingPaneButton: Boolean,
    onNavigateToSupportingPane: () -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedPane(
        modifier = modifier.safeContentPadding()
    ) {
        // Main pane content
        if (shouldShowSupportingPaneButton) {
            Button(onClick = onNavigateToSupportingPane) {
                Text("Show supporting pane")
            }
        } else {
            Text("Supporting pane is shown")
        }
    }
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.SupportingPane(
    scaffoldNavigator: ThreePaneScaffoldNavigator<Any>,
    modifier: Modifier = Modifier,
    backNavigationBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange,
) {
    val scope = rememberCoroutineScope()
    AnimatedPane(modifier = Modifier.safeContentPadding()) {
        Column {
            // Allow users to dismiss the supporting pane. Use back navigation to
            // hide an expanded supporting pane.
            if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
                // Material design principles promote the usage of a right-aligned
                // close (X) button.
                IconButton(
                    modifier =  modifier.align(Alignment.End).padding(16.dp),
                    onClick = {
                        scope.launch {
                            scaffoldNavigator.navigateBack(backNavigationBehavior)
                        }
                    }
                ) {
                    Icon(Icons.Default.Close, contentDescription = "Close")
                }
            }
            Text("Supporting pane")
        }

    }
}

창을 컴포저블로 추출하면 SupportingPaneScaffold 사용이 간소화됩니다 (다음과 이전 섹션의 Scaffold 전체 구현 비교).

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)

스캐폴드의 특정 측면을 더 세부적으로 제어해야 하는 경우 NavigableSupportingPaneScaffold 대신 SupportingPaneScaffold을 사용하는 것이 좋습니다. PaneScaffoldDirective 및 ThreePaneScaffoldValue 또는 ThreePaneScaffoldState를 별도로 허용합니다. 이러한 유연성 덕분에 창 간격에 관한 맞춤 로직을 구현하고 동시에 표시할 창 수를 결정할 수 있습니다. ThreePaneScaffoldPredictiveBackHandler를 추가하여 뒤로 탐색 예측 지원을 사용 설정할 수도 있습니다.

ThreePaneScaffoldPredictiveBackHandler 추가

scaffold 탐색기 인스턴스를 사용하는 뒤로 탐색 예측 핸들러를 연결하고 backBehavior를 지정합니다. 이는 뒤로 탐색 중에 백 스택에서 대상이 팝되는 방식을 결정합니다. 그런 다음 scaffoldDirective 및 scaffoldState를 SupportingPaneScaffold에 전달합니다. ThreePaneScaffoldState를 허용하는 오버로드를 사용하여 scaffoldNavigator.scaffoldState를 전달합니다.

SupportingPaneScaffold 내에서 기본 창과 지원 창을 정의합니다. 기본 창 애니메이션에는 AnimatedPane를 사용합니다.

위 단계를 구현하면 코드가 다음과 같아집니다.

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

ThreePaneScaffoldPredictiveBackHandler(
    navigator = scaffoldNavigator,
    backBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
)

SupportingPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    scaffoldState = scaffoldNavigator.scaffoldState,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)