지원 창 레이아웃 빌드

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

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

스캐폴드로 지원 창 구현

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

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

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

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

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

    기본 콘텐츠가 디스플레이의 대부분을 차지하고 지원 콘텐츠가 옆에 있습니다.
    그림 1. 지원 창 레이아웃

종속 항목 추가

NavigableSupportingPaneScaffoldMaterial 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: HingeInfoPosture와 같은 하위 수준 템플릿

  • adaptive-layout: ListDetailPaneScaffoldSupportingPaneScaffold와 같은 적응형 레이아웃

  • adaptive-navigation: 창 내에서 그리고 창 간에 탐색하기 위한 컴포저블과 기본적으로 탐색을 지원하는 적응형 레이아웃(NavigableListDetailPaneScaffoldNavigableSupportingPaneScaffold)

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

뒤로 탐색 예측 동작 선택

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

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

탐색기 만들기

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

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

스캐폴드에 탐색기 전달

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

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를 더 쉽게 사용할 수 있습니다 (이전 섹션의 스캐폴드 전체 구현과 비교).

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를 사용하는 것이 좋습니다. 이렇게 하면 PaneScaffoldDirectiveThreePaneScaffoldValue 또는 ThreePaneScaffoldState를 별도로 허용합니다. 이러한 유연성을 통해 창 간격에 관한 맞춤 로직을 구현하고 동시에 표시할 창 수를 결정할 수 있습니다. ThreePaneScaffoldPredictiveBackHandler를 추가하여 뒤로 탐색 예측 지원을 사용 설정할 수도 있습니다.

ThreePaneScaffoldPredictiveBackHandler 추가

스캐폴드 탐색기 인스턴스를 가져오는 뒤로 탐색 예측 핸들러를 연결하고 backBehavior를 지정합니다. 이렇게 하면 뒤로 탐색 중에 백스택에서 대상이 팝되는 방식이 결정됩니다. 그런 다음 scaffoldDirectivescaffoldStateSupportingPaneScaffold에 전달합니다. 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) },
)