지원 창 레이아웃 빌드

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

자세한 내용은 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'
  • 적응형: HingeInfoPosture과 같은 하위 수준 빌드 블록

  • 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()

스캐폴드에 탐색기 전달

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

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

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

큰 화면의 경우 BackNavigationBehavior.PopUntilScaffoldValueChange 상수를 전달하여 ThreePaneScaffoldNavigator.navigateBack() 메서드를 사용하여 지원 창을 닫습니다. 이 메서드를 호출하면 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) },
)