폴더블용 TwoPaneLayout Jetpack Compose 구성 요소
중요
이 문서에서 설명하는 기능 및 지침은 공개 미리 보기 상태이며 일반적으로 공급되기 전에 대대적으로 수정될 수 있습니다. Microsoft는 여기에 제공된 정보에 대해 명시적 또는 묵시적 보증을 하지 않습니다.
TwoPaneLayout은 이중 화면, 폴더블 및 큰 화면 디바이스에 대한 UI를 만드는 데 도움이 되는 Jetpack Compose 구성 요소입니다. TwoPaneLayout은 UI의 최상위 수준에서 사용할 수 있는 두 창 레이아웃을 제공합니다. 앱이 이중 화면, 폴더블 및 대형 화면 디바이스에 걸쳐 있을 때 구성 요소는 두 개의 창을 나란히 배치합니다. 그렇지 않으면 하나의 창만 표시됩니다. 이러한 창은 디바이스의 방향과 선택한 paneMode
에 따라 가로 또는 세로일 수 있습니다.
참고
TwoPaneLayout은 너비 창 크기 클래스가 확장될 때 디바이스를 대형 화면으로 간주합니다. 즉, 840dp보다 크다는 의미입니다.
앱이 분리된 세로 힌지 또는 접히는 부분에 걸쳐 있거나 너비가 대형 화면 디바이스의 높이보다 큰 경우, 창 1은 왼쪽에 배치되고 창 2는 오른쪽에 배치됩니다. 디바이스가 회전하거나 앱이 분리된 가로 힌지 또는 접히는 부분에 걸쳐 있거나 너비가 대형 화면 디바이스의 높이보다 작은 경우 창 1은 위에 배치되고 창 2는 아래에 배치됩니다.
종속성 추가
최상위 수준 build.gradle 파일에
mavenCentral()
리포지토리가 있는지 확인합니다.allprojects { repositories { google() mavenCentral() } }
종속성을 모듈 수준 build.gradle 파일에 추가합니다(현재 버전이 여기에 표시된 것과 다를 수 있음).
implementation "com.microsoft.device.dualscreen:twopanelayout:1.0.1-alpha05"
또한 이
compileSdkVersion
API 33으로 설정되어 있고targetSdkVersion
모듈 수준 build.gradle 파일에서 이 API 32 이상으로 설정되어 있는지 확인합니다.android { compileSdkVersion 33 defaultConfig { targetSdkVersion 32 } ... }
TwoPaneLayout
또는TwoPaneLayoutNav
를 사용하여 레이아웃을 빌드합니다.자세한 내용은 TwoPaneLayout 샘플 및 TwoPaneLayoutNav 샘플을 참조하세요.
프로젝트에서 TwoPaneLayout 사용
프로젝트에서 TwoPaneLayout을 사용할 때 고려해야 할 몇 가지 중요한 개념이 있습니다.
-
애플리케이션에 따라 프로젝트의 최상위 수준에서 사용할 수 있는 세 가지 TwoPaneLayout 생성자(기본 TwoPaneLayout, navController가 있는 TwoPaneLayout 및 TwoPaneLayoutNav)가 있습니다.
-
TwoPaneLayout은 창 표시 방법을 사용자 지정하는 두 가지 방법인 가중치 및 창 모드를 제공합니다.
-
TwoPaneLayout은 각 창에 표시되는 콘텐츠를 제어할 수 있는 내부 탐색 메서드도 제공합니다. 사용되는 생성자에 따라 또는
TwoPaneNavScope
메서드에TwoPaneScope
액세스할 수 있습니다. -
또는
TwoPaneNavScope
를 사용하는TwoPaneScope
구성 가능 개체를 테스트하는 데 도움이 되도록 TwoPaneLayout은 UI 테스트에서 사용할 두 범위의 테스트 구현을 제공합니다.
TwoPaneLayout 생성자
TwoPaneLayout은 항상 앱에서 구성 가능한 최상위 수준이어야 창 크기를 올바르게 계산할 수 있습니다. 서로 다른 시나리오에서 사용할 수 있는 세 가지 TwoPaneLayout 생성자가 있습니다. API 참조에 대한 자세한 내용은 TwoPaneLayout README.md를 확인하세요.
기본 TwoPaneLayout
@Composable
fun TwoPaneLayout(
modifier: Modifier = Modifier,
paneMode: TwoPaneMode = TwoPaneMode.TwoPane,
pane1: @Composable TwoPaneScope.() -> Unit,
pane2: @Composable TwoPaneScope.() -> Unit
)
기본 TwoPaneLayout 생성자는 최대 두 개의 콘텐츠 화면만 표시해야 하는 경우에 사용해야 합니다.
pane1
및 pane2
구성 파일 내의 TwoPaneScope 인터페이스에서 제공하는 필드 및 메서드에 액세스할 수 있습니다.
예제 사용법:
TwoPaneLayout(
pane1 = { Pane1Content() },
pane2 = { Pane2Content() }
)
navController가 있는 TwoPaneLayout
@Composable
fun TwoPaneLayout(
modifier: Modifier = Modifier,
paneMode: TwoPaneMode = TwoPaneMode.TwoPane,
navController: NavHostController,
pane1: @Composable TwoPaneScope.() -> Unit,
pane2: @Composable TwoPaneScope.() -> Unit
)
다음과 같은 경우에 navController 생성자가 있는 TwoPaneLayout을 사용해야 합니다.
- 최대 두 개의 콘텐츠 화면만 표시하면 됩니다.
- 앱의 탐색 정보에 액세스해야 합니다.
pane1
및 pane2
구성 파일 내의 TwoPaneScope 인터페이스에서 제공하는 필드 및 메서드에 액세스할 수 있습니다.
예제 사용법:
val navController = rememberNavController()
TwoPaneLayout(
navController = navController,
pane1 = { Pane1Content() },
pane2 = { Pane2Content() }
)
TwoPaneLayoutNav
@Composable
fun TwoPaneLayoutNav(
modifier: Modifier = Modifier,
navController: NavHostController,
paneMode: TwoPaneMode = TwoPaneMode.TwoPane,
singlePaneStartDestination: String,
pane1StartDestination: String,
pane2StartDestination: String,
builder: NavGraphBuilder.() -> Unit
)
TwoPaneLayoutNav 생성자는 두 개가 넘는 콘텐츠 화면을 표시하고 사용자 지정 가능한 탐색 지원이 필요한 경우에 사용해야 합니다. 구성 가능한 각 대상 내의 TwoPaneNavScope 인터페이스에서 제공하는 필드 및 메서드에 액세스할 수 있습니다.
예제 사용법:
val navController = rememberNavController()
TwoPaneLayoutNav(
navController = navController,
singlePaneStartDestination = "A",
pane1StartDestination = "A",
pane2StartDestination = "B"
) {
composable("A") {
ContentA()
}
composable("B") {
ContentB()
}
composable("C") {
ContentC()
}
}
레이아웃 사용자 지정
TwoPaneLayout을 사용자 지정하는 방법에는 다음 두 가지가 있습니다.
-
weight
- 두 창을 비례적으로 배치하는 방법을 결정합니다. -
paneMode
- 이중 화면 모드에서 하나 또는 두 개의 창을 가로 및 세로로 표시할 시기를 결정합니다.
무게
TwoPaneLayout은 TwoPaneNavScope.weight
및 TwoPaneScope.weight
한정자를 사용하여 제공된 가중치에 따라 자식 너비 또는 높이를 할당할 수 있습니다.
예제 사용법:
TwoPaneLayout(
pane1 = { Pane1Content(modifier = Modifier.weight(.3f)) },
pane2 = { Pane2Content(modifier = Modifier.weight(.7f)) }
)
가중치는 다음과 같은 다양한 디바이스에서 레이아웃에 다르게 영향을 줍니다.
- 대형 화면
- 폴더블
대형 화면
가중치가 제공되지 않으면 두 개의 창을 동일하게 나눕니다.
가중치가 제공되면 가중치 비율에 따라 레이아웃이 비례적으로 분할됩니다.
예를 들어 이 스크린샷은 3:7 가중치 비율을 가진 태블릿의 TwoPaneLayout을 보여줍니다.
폴더블
분리 접기가 있는 경우 레이아웃은 가중치 제공 여부에 관계없이 접기의 경계에 따라 분할됩니다.
접기를 구분하지 않는 경우 디바이스는 크기에 따라 큰 화면 또는 단일 화면으로 처리됩니다.
예를 들어 이 이미지는 구분 접이식이 있는 이중 화면 디바이스의 TwoPaneLayout 레이아웃을 보여줍니다.
창 모드
창 모드는 TwoPaneLayout에 대해 두 개의 창이 표시되는 경우에 영향을 줍니다. 기본적으로 구분 접 기 또는 큰 창이 있을 때마다 두 개의 창이 표시되지만 이러한 경우 창 모드를 변경하여 하나의 창만 표시하도록 선택할 수 있습니다.
구분 접기는isSeparating 속성에 대해 true를 반환하는 FoldingFeature가 있음을 의미합니다.
큰 창은 너비 WindowSizeClass가 이 EXPANDED
고 높이 크기 클래스가 적어도 MEDIUM
인 창입니다.
예제 사용법:
TwoPaneLayout(
paneMode = TwoPaneMode.HorizontalSingle,
pane1 = { Pane1Content() },
pane2 = { Pane2Content() }
)
다음과 같은 네 가지 가능한 값이 있습니다 paneMode
.
TwoPane
HorizontalSingle
VerticalSingle
SinglePane
TwoPane
TwoPane
는 기본 창 모드이며 방향에 관계없이 구분 접 기 또는 큰 창이 있는 경우 항상 두 개의 창을 표시합니다.
HorizontalSingle
HorizontalSingle
가로 구분 접기 또는 세로 큰 창(위쪽/아래쪽 창 결합)이 있는 경우 하나의 큰 창을 표시합니다.
VerticalSingle
VerticalSingle
세로 구분 접 기 또는 가로 큰 창 (왼쪽/오른쪽 창 결합)이 있는 경우 하나의 큰 창을 표시합니다.
SinglePane
SinglePane
창 기능 및 방향에 관계없이 항상 하나의 창이 표시됩니다.
창 모드 동작 테이블
요약하자면, 이 표에서는 다른 창 모드 및 디바이스 구성에 대해 하나 🟩 또는 두 개의 🟦🟦 창이 표시되는 시기를 설명합니다.
창 모드 | 접기 구분 없이 작은 창 | 세로 큰 창/가로 구분 접기 | 가로 대형 창/세로 구분 접기 |
---|---|---|---|
TwoPane |
🟩 | 🟦🟦 | 🟦🟦 |
HorizontalSingle |
🟩 | 🟩 | 🟦🟦 |
VerticalSingle |
🟩 | 🟦🟦 | 🟩 |
SinglePane |
🟩 | 🟩 | 🟩 |
TwoPaneLayout 내에서 탐색
TwoPaneLayout은 내부 탐색을 위한 옵션이 있는 두 가지 인터페이스를 제공합니다. 사용하는 생성자에 따라 다른 필드와 메서드에 액세스할 수 있습니다.
-
TwoPaneScope
- 기본 TwoPaneLayout 및 navController가 있는 TwoPaneLayout 생성자와 함께 사용할 수 있습니다. -
TwoPaneNavScope
- TwoPaneLayoutNav 생성자와 함께 사용할 수 있습니다.
TwoPaneScope
interface TwoPaneScope {
...
fun navigateToPane1()
fun navigateToPane2()
val currentSinglePaneDestination: String
...
}
TwoPaneScope
를 사용하면 단일 창 모드에서 창 1과 2 사이를 탐색할 수 있습니다.
Screen.Pane1.route
또는 Screen.Pane2.route
와 같은 현재 단일 창 대상의 경로에 액세스할 수도 있습니다.
예제 사용법:
TwoPaneLayout(
pane1 = { Pane1Content(modifier = Modifier.clickable { navigateToPane2() }) },
pane2 = { Pane2Content(modifier = Modifier.clickable { navigateToPane1() }) }
)
TwoPaneNavScope
interface TwoPaneNavScope {
...
fun NavHostController.navigateTo(
route: String,
launchScreen: Screen,
builder: NavOptionsBuilder.() -> Unit = { }
)
fun NavHostController.navigateBack(): Boolean
val twoPaneBackStack: MutableList<TwoPaneBackStackEntry>
val currentSinglePaneDestination: String
val currentPane1Destination: String
val currentPane2Destination: String
val isSinglePane: Boolean
...
}
TwoPaneNavScope
를 사용하면 단일 및 두 개의 창 모드에서 서로 다른 대상으로 이동할 수 있습니다.
단일 창 대상이든 창 1 및 창 2 대상이든 관계없이 현재 대상의 경로에 액세스할 수도 있습니다. 이러한 값은 TwoPaneLayoutNav
생성자에 전달된 대상의 경로에 따라 달라집니다.
TwoPaneLayoutNav
는 내부 백스택을 유지하므로 1~2개의 창 사이를 전환할 때 탐색 기록이 저장됩니다. 기본 구성 요소 동작은 단일 창 모드에서만 다시 누르기를 지원합니다. 두 창 모드에 누름 처리를 다시 추가하거나 단일 창 모드에서 기본 동작을 재정의하려면 를 호출navigateBack
하는 구성 가능 항목에 사용자 지정 BackHandler를 만듭니다. 이렇게 하면 내부 백스택이 올바르게 유지됩니다. 백스택은 필드가 twoPaneBackStack
있는 인터페이스를 통해서도 노출되므로 필요한 경우 백스택 크기 및 콘텐츠에 액세스할 수 있습니다.
예제 사용법:
val navController = rememberNavController()
TwoPaneLayoutNav(
navController = navController,
singlePaneStartDestination = "A",
pane1StartDestination = "A",
pane2StartDestination = "B"
) {
composable("A") {
ContentA(Modifier.clickable { navController.navigateTo("B", Screen.Pane2) })
}
composable("B") {
ContentB(Modifier.clickable { navController.navigateTo("C", Screen.Pane2) })
}
composable("C") {
ContentC(Modifier.clickable { navController.navigateTo("A", Screen.Pane1) })
}
}
TwoPaneLayout 구성 파일 테스트
TwoPaneLayout 내에서 사용되는 구성 파일에 대한 UI 테스트를 작성할 때 테스트 범위 클래스를 사용하여 테스트를 설정할 수 있습니다. 이러한 클래스 TwoPaneScopeTest
및 TwoPaneNavScopeTest
는 모든 인터페이스 메서드에 대해 빈 구현을 제공하고 클래스 생성자에서 필드 값을 설정할 수 있도록 합니다.
class TwoPaneScopeTest(
currentSinglePaneDestination: String = "",
isSinglePane: Boolean = true
) : TwoPaneScope
class TwoPaneNavScopeTest(
currentSinglePaneDestination: String = "",
currentPane1Destination: String = "",
currentPane2Destination: String = "",
isSinglePane: Boolean = true
) : TwoPaneNavScope
예제 사용법:
// Composable function in app
@Composable
fun TwoPaneScope.Example() {
if (isSinglePane)
Text("single pane")
else
Text("two pane")
}
...
// UI test in androidTest directory
@Test
fun exampleTest() {
composeTestRule.setContent {
val twoPaneScope = TwoPaneScopeTest(isSinglePane = true)
twoPaneScope.Example()
}
composeTestRule.onNodeWithText("single pane").assertIsDisplayed()
composeTestRule.onNodeWithText("two pane").assertDoesNotExist()
}