다음을 통해 공유


폴더블용 TwoPaneLayout Jetpack Compose 구성 요소

중요

이 문서에서 설명하는 기능 및 지침은 공개 미리 보기 상태이며 일반적으로 공급되기 전에 대대적으로 수정될 수 있습니다. Microsoft는 여기에 제공된 정보에 대해 명시적 또는 묵시적 보증을 하지 않습니다.

TwoPaneLayout은 이중 화면, 폴더블 및 큰 화면 디바이스에 대한 UI를 만드는 데 도움이 되는 Jetpack Compose 구성 요소입니다. TwoPaneLayout은 UI의 최상위 수준에서 사용할 수 있는 두 창 레이아웃을 제공합니다. 앱이 이중 화면, 폴더블 및 대형 화면 디바이스에 걸쳐 있을 때 구성 요소는 두 개의 창을 나란히 배치합니다. 그렇지 않으면 하나의 창만 표시됩니다. 이러한 창은 디바이스의 방향과 선택한 paneMode에 따라 가로 또는 세로일 수 있습니다.

참고

TwoPaneLayout은 너비 창 크기 클래스가 확장될 때 디바이스를 대형 화면으로 간주합니다. 즉, 840dp보다 크다는 의미입니다.

앱이 분리된 세로 힌지 또는 접히는 부분에 걸쳐 있거나 너비가 대형 화면 디바이스의 높이보다 큰 경우, 창 1은 왼쪽에 배치되고 창 2는 오른쪽에 배치됩니다. 디바이스가 회전하거나 앱이 분리된 가로 힌지 또는 접히는 부분에 걸쳐 있거나 너비가 대형 화면 디바이스의 높이보다 작은 경우 창 1은 위에 배치되고 창 2는 아래에 배치됩니다.

종속성 추가

  1. 최상위 수준 build.gradle 파일에 mavenCentral() 리포지토리가 있는지 확인합니다.

    allprojects {
        repositories {
            google()
            mavenCentral()
         }
    }
    
  2. 종속성을 모듈 수준 build.gradle 파일에 추가합니다(현재 버전이 여기에 표시된 것과 다를 수 있음).

    implementation "com.microsoft.device.dualscreen:twopanelayout:1.0.1-alpha05"
    
  3. 또한 이 compileSdkVersion API 33으로 설정되어 있고 targetSdkVersion 모듈 수준 build.gradle 파일에서 이 API 32 이상으로 설정되어 있는지 확인합니다.

    android { 
        compileSdkVersion 33
    
        defaultConfig { 
            targetSdkVersion 32
        } 
        ... 
    }
    
  4. TwoPaneLayout 또는 TwoPaneLayoutNav를 사용하여 레이아웃을 빌드합니다.

    자세한 내용은 TwoPaneLayout 샘플TwoPaneLayoutNav 샘플을 참조하세요.

프로젝트에서 TwoPaneLayout 사용

프로젝트에서 TwoPaneLayout을 사용할 때 고려해야 할 몇 가지 중요한 개념이 있습니다.

  • TwoPaneLayout 생성자

    애플리케이션에 따라 프로젝트의 최상위 수준에서 사용할 수 있는 세 가지 TwoPaneLayout 생성자(기본 TwoPaneLayout, navController가 있는 TwoPaneLayout 및 TwoPaneLayoutNav)가 있습니다.

  • 레이아웃 사용자 지정

    TwoPaneLayout은 창 표시 방법을 사용자 지정하는 두 가지 방법인 가중치 및 창 모드를 제공합니다.

  • TwoPaneLayout으로 탐색

    TwoPaneLayout은 각 창에 표시되는 콘텐츠를 제어할 수 있는 내부 탐색 메서드도 제공합니다. 사용되는 생성자에 따라 또는 TwoPaneNavScope 메서드에 TwoPaneScope 액세스할 수 있습니다.

  • TwoPaneLayout 구성 파일 테스트

    또는 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 생성자는 최대 두 개의 콘텐츠 화면만 표시해야 하는 경우에 사용해야 합니다. pane1pane2 구성 파일 내의 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을 사용해야 합니다.

  • 최대 두 개의 콘텐츠 화면만 표시하면 됩니다.
  • 앱의 탐색 정보에 액세스해야 합니다.

pane1pane2 구성 파일 내의 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.weightTwoPaneScope.weight 한정자를 사용하여 제공된 가중치에 따라 자식 너비 또는 높이를 할당할 수 있습니다.

예제 사용법:

TwoPaneLayout(
    pane1 = { Pane1Content(modifier = Modifier.weight(.3f)) },
    pane2 = { Pane2Content(modifier = Modifier.weight(.7f)) }
)

가중치는 다음과 같은 다양한 디바이스에서 레이아웃에 다르게 영향을 줍니다.

  • 대형 화면
  • 폴더블

대형 화면

가중치가 제공되지 않으면 두 개의 창을 동일하게 나눕니다.

가중치가 제공되면 가중치 비율에 따라 레이아웃이 비례적으로 분할됩니다.

예를 들어 이 스크린샷은 3:7 가중치 비율을 가진 태블릿의 TwoPaneLayout을 보여줍니다.

0.3 및 0.7의 가중치 수정자를 사용하여 창이 3:7 비율로 분할되는 태블릿/대형 화면 디바이스의 TwoPaneLayout

폴더블

분리 접기가 있는 경우 레이아웃은 가중치 제공 여부에 관계없이 접기의 경계에 따라 분할됩니다.

접기를 구분하지 않는 경우 디바이스는 크기에 따라 큰 화면 또는 단일 화면으로 처리됩니다.

예를 들어 이 이미지는 구분 접이식이 있는 이중 화면 디바이스의 TwoPaneLayout 레이아웃을 보여줍니다.

가중치의 관계없이 창이 접기 경계에 따라 분할되는 듀얼 화면 디바이스(Surface Duo)의 TwoPaneLayout

창 모드

창 모드는 TwoPaneLayout에 대해 두 개의 창이 표시되는 경우에 영향을 줍니다. 기본적으로 구분 접 기 또는 큰 창이 있을 때마다 두 개의 창이 표시되지만 이러한 경우 창 모드를 변경하여 하나의 창만 표시하도록 선택할 수 있습니다.

구분 접기는isSeparating 속성에 대해 true를 반환하는 FoldingFeature가 있음을 의미합니다.

큰 창은 너비 WindowSizeClass가 이 EXPANDED 고 높이 크기 클래스가 적어도 MEDIUM인 창입니다.

예제 사용법:

TwoPaneLayout(
    paneMode = TwoPaneMode.HorizontalSingle,
    pane1 = { Pane1Content() },
    pane2 = { Pane2Content() }
)

다음과 같은 네 가지 가능한 값이 있습니다 paneMode .

  • TwoPane
  • HorizontalSingle
  • VerticalSingle
  • SinglePane

TwoPane

TwoPane 는 기본 창 모드이며 방향에 관계없이 구분 접 기 또는 큰 창이 있는 경우 항상 두 개의 창을 표시합니다.

폴더블 디바이스의 TwoPane 창 모드

HorizontalSingle

HorizontalSingle 가로 구분 접기 또는 세로 큰 창(위쪽/아래쪽 창 결합)이 있는 경우 하나의 큰 창을 표시합니다.

이중 화면 디바이스의 HorizontalSingle 창 모드

VerticalSingle

VerticalSingle 세로 구분 접 기 또는 가로 큰 창 (왼쪽/오른쪽 창 결합)이 있는 경우 하나의 큰 창을 표시합니다.

폴더블 디바이스의 VerticalSingle 창 모드

SinglePane

SinglePane 창 기능 및 방향에 관계없이 항상 하나의 창이 표시됩니다.

창 모드 동작 테이블

요약하자면, 이 표에서는 다른 창 모드 및 디바이스 구성에 대해 하나 🟩 또는 두 개의 🟦🟦 창이 표시되는 시기를 설명합니다.

창 모드 접기 구분 없이 작은 창 세로 큰 창/가로 구분 접기 가로 대형 창/세로 구분 접기
TwoPane 🟩 🟦🟦 🟦🟦
HorizontalSingle 🟩 🟩 🟦🟦
VerticalSingle 🟩 🟦🟦 🟩
SinglePane 🟩 🟩 🟩

TwoPaneLayout은 내부 탐색을 위한 옵션이 있는 두 가지 인터페이스를 제공합니다. 사용하는 생성자에 따라 다른 필드와 메서드에 액세스할 수 있습니다.

TwoPaneScope

interface TwoPaneScope {
    ...

    fun navigateToPane1()

    fun navigateToPane2()

    val currentSinglePaneDestination: String

    ...
}

TwoPaneScope를 사용하면 단일 창 모드에서 창 1과 2 사이를 탐색할 수 있습니다.

실행 중인 navigateToPane1 및 navigateToPane2 메서드를 보여주는 애니메이션

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를 사용하면 단일 및 두 개의 창 모드에서 서로 다른 대상으로 이동할 수 있습니다.

TwoPaneLayoutNav를 사용하여 단일 및 두 개의 창 모드에서 두 개를 초과하는 대상 사이를 탐색하는 방법을 보여주는 애니메이션.

단일 창 대상이든 창 1 및 창 2 대상이든 관계없이 현재 대상의 경로에 액세스할 수도 있습니다. 이러한 값은 TwoPaneLayoutNav 생성자에 전달된 대상의 경로에 따라 달라집니다.

TwoPaneLayoutNav 는 내부 백스택을 유지하므로 1~2개의 창 사이를 전환할 때 탐색 기록이 저장됩니다. 기본 구성 요소 동작은 단일 창 모드에서만 다시 누르기를 지원합니다. 두 창 모드에 누름 처리를 다시 추가하거나 단일 창 모드에서 기본 동작을 재정의하려면 를 호출navigateBack하는 구성 가능 항목에 사용자 지정 BackHandler를 만듭니다. 이렇게 하면 내부 백스택이 올바르게 유지됩니다. 백스택은 필드가 twoPaneBackStack 있는 인터페이스를 통해서도 노출되므로 필요한 경우 백스택 크기 및 콘텐츠에 액세스할 수 있습니다.

TwoPaneLayoutNav가 백스택을 유지하고 단일 창 모드에서 다시 누르기 동작을 지원하는 방법을 보여 주는 애니메이션.

예제 사용법:

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 테스트를 작성할 때 테스트 범위 클래스를 사용하여 테스트를 설정할 수 있습니다. 이러한 클래스 TwoPaneScopeTestTwoPaneNavScopeTest는 모든 인터페이스 메서드에 대해 빈 구현을 제공하고 클래스 생성자에서 필드 값을 설정할 수 있도록 합니다.

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