Wear OS용 Compose를 사용한 페이지로 나누기

페이징을 사용하면 사용자가 Wear OS 기기에서 서로 다른 전체 화면 페이지 간에 가로 또는 세로로 스와이프할 수 있습니다. 일반적인 사용 사례로는 운동 앱에서 운동 측정항목과 미디어 컨트롤 간에 스와이프하거나 다중 페이지 흐름을 단계별로 진행하는 것이 있습니다.

Wear OS Material 3용 Compose에서 HorizontalPagerScaffoldVerticalPagerScaffold는 페이지 표시기를 자동으로 배치하고 TimeText로 전환을 관리하여 페이지 탐색기 레이아웃을 조정합니다. 각 페이지는 AnimatedPage 컴포저블로 래핑됩니다. 이 컴포저블은 페이지가 원형 디스플레이를 통해 전환될 때 크기 조절, 둥근 모서리 모핑, 스크림 효과를 적용합니다.

다음 애니메이션은 가로 스와이프 중에 HorizontalPagerScaffoldAnimatedPage이 페이지를 확장하고 애니메이션을 적용하는 방법을 보여줍니다.

페이지로 나누기를 위한 스캐폴드 계층 구조

Material 3에서 페이지로 나뉜 화면 레이아웃을 디자인할 때는 왼쪽이나 오른쪽 (HorizontalPager) 또는 위나 아래 (VerticalPager)로 스와이프하는지 여부에 관계없이 외부 컨테이너에서 내부 콘텐츠까지 다음 구성요소 계층 구조를 사용하세요.

  1. AppScaffold: 앱의 루트에 있는 가장 바깥쪽 컨테이너입니다(앱당 하나만 사용). 페이지 전환 및 스와이프하여 닫기 동작 중에 시계가 화면 상단에 고정되도록 전역 TimeText 오버레이를 고정합니다.
  2. HorizontalPagerScaffold 또는 VerticalPagerScaffold: 페이지 수준에서 AppScaffold 내부에 배치됩니다. TimeTextHorizontalPageIndicator 또는 VerticalPageIndicator 간 전환을 조정합니다.
  3. HorizontalPager 또는 VerticalPager: 공유 PagerState를 사용하여 스와이프 동작, 플링 물리학, 로터리 입력 맞추기를 관리하는 기본 페이저 컨테이너입니다.
  4. AnimatedPage: 페이지로 나누기의 페이지 콘텐츠 람다 내에 배치됩니다. PagerState의 페이지 오프셋을 기반으로 Material 3 전환 애니메이션(스케일링 및 스크림 효과)을 적용하기 위해 각 개별 페이지를 래핑합니다.
  5. ScreenScaffold: 각 개별 페이지의 AnimatedPage 내부에 배치됩니다. 각 페이지에는 자체적으로 세로로 스크롤 가능한 목록 (예: TransformingLazyColumn) 또는 자체 EdgeButton이 포함될 수 있으므로 각 페이지 내에 ScreenScaffold을 중첩하면 세로 스크롤 표시기, 가장자리 버튼, contentPadding이 해당 페이지에 속하고 페이지와 함께 원활하게 애니메이션 처리됩니다.
AppScaffold (1 per app: anchors global TimeText)
 └── HorizontalPagerScaffold / VerticalPagerScaffold (manages PageIndicator)
      └── HorizontalPager / VerticalPager (manages PagerState & fling behavior)
           └── AnimatedPage (applies scaling & scrim transitions per page)
                └── ScreenScaffold (1 per page: ScrollIndicator & EdgeButton)
                     └── Page Content (Column or TransformingLazyColumn)

수평 페이저 구현

Material 3에서 가로 페이저를 구현하려면 앱의 AppScaffold 내부에 HorizontalPagerScaffold, HorizontalPager, AnimatedPage, ScreenScaffold를 중첩하세요.

flingBehaviorPagerScaffoldDefaults.snapWithSpringFlingBehavior를 사용하여 Material 3 스프링 모션 물리학 및 반응형 페이지 스냅 (HighSnapPositionalThreshold)을 적용합니다. 기본적으로 HorizontalPager는 로터리 페이지 스크롤 (rotaryScrollableBehavior = null)을 사용 중지하므로 회전하는 크라운 또는 베젤 입력으로 활성 페이지 내에서 세로 목록 (예: TransformingLazyColumn)을 스크롤할 수 있습니다.

다음 샘플은 완전한 HorizontalPagerScaffold 설정을 보여줍니다.

@Composable
fun HorizontalPagerScaffoldSample(navigateBack: () -> Unit) {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 10 })

        HorizontalPagerScaffold(pagerState = pagerState) {
            HorizontalPager(
                state = pagerState,
                flingBehavior =
                    PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                        state = pagerState
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        Column(
                            modifier = Modifier.fillMaxSize(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                        ) {
                            Text(text = "Page #$page")
                            Spacer(modifier = Modifier.height(8.dp))
                            Text(text = "Swipe left and right")
                            if (page == 0) {
                                Spacer(modifier = Modifier.height(16.dp))
                                Button(onClick = navigateBack) { Text("Exit") }
                            }
                        }
                    }
                }
            }
        }
    }
}

가로 페이저 내 페이지에 TransformingLazyColumn가 포함된 경우 해당 페이지의 범위 내에 별도의 rememberTransformingLazyColumnState()를 만들어 내부 ScreenScaffoldTransformingLazyColumn에 전달합니다.

AppScaffold {
    val pagerState = rememberPagerState(pageCount = { 10 })

    HorizontalPagerScaffold(pagerState = pagerState) {
        HorizontalPager(
            state = pagerState,
            flingBehavior =
                PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                    state = pagerState
                ),
        ) { page ->
            AnimatedPage(pageIndex = page, pagerState = pagerState) {
                val columnState = rememberTransformingLazyColumnState()
                val transformationSpec = rememberTransformationSpec()

                ScreenScaffold(
                    scrollState = columnState,
                ) { contentPadding ->
                    TransformingLazyColumn(
                        state = columnState,
                        contentPadding = contentPadding,
                    ) {
                        item {
                            ListHeader(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .transformedHeight(this, transformationSpec)
                                    .minimumVerticalContentPadding(
                                        ListHeaderDefaults.minimumTopListContentPadding
                                    ),
                                transformation = SurfaceTransformation(transformationSpec),
                            ) {
                                Text(text = "Pager sample")
                            }
                        }
                        item {
                            Card(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .transformedHeight(this, transformationSpec)
                                    .minimumVerticalContentPadding(
                                        CardDefaults.minimumVerticalListContentPadding
                                    ),
                                transformation = SurfaceTransformation(transformationSpec),
                            ) {
                                if (page == 0) {
                                    Text(text = "Page #$page. Swipe right")
                                } else {
                                    Text(text = "Page #$page. Swipe left and right")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

세로 페이저 구현

세로 페이저를 사용하면 사용자가 페이지 간에 위아래로 스와이프하거나 시계의 회전식 측면 버튼이나 베젤을 사용하여 페이지를 단계별로 탐색할 수 있습니다.

HorizontalPager와 달리 VerticalPager는 기본적으로 회전 페이지 스냅을 사용 설정합니다 (RotaryScrollableDefaults.snapBehavior(state)). PagerScaffoldDefaults.snapWithSpringFlingBehaviorflingBehavior에 전달하고 각 페이지를 AnimatedPageScreenScaffold로 래핑합니다.

@Composable
fun VerticalPagerScaffoldSample() {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 10 })

        VerticalPagerScaffold(pagerState = pagerState) {
            VerticalPager(
                state = pagerState,
                flingBehavior =
                    PagerScaffoldDefaults.snapWithSpringFlingBehavior(
                        state = pagerState
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        Column(
                            modifier = Modifier.fillMaxSize(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                        ) {
                            Text(text = "Page #$page")
                            Spacer(modifier = Modifier.height(8.dp))
                            Text(text = "Swipe up and down")
                        }
                    }
                }
            }
        }
    }
}

페이지로 나누기 동작 맞춤설정

앱의 상호작용 요구사항에 맞게 HorizontalPagerScaffoldVerticalPagerScaffold의 스냅 감도를 맞춤설정할 수 있습니다.

운동의 스냅 민감도 조정하기

운동이나 사용자의 대근육 운동 제어가 제한되는 움직임이 많은 활동 중에 실수로 터치하거나 용두를 약간 회전하면 의도치 않게 페이지가 전환될 수 있습니다. 이러한 화면의 경우 스냅 감도를 낮게 설정하여 페이지를 구성하세요.

  • maxFlingPages = 0snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold와 함께 PagerDefaults.snapFlingBehavior를 전달하므로 페이지를 넘기려면 화면의 더 큰 부분을 의도적으로 드래그해야 합니다.
  • 로터리 페이지 스냅을 사용 설정하는 경우 RotaryScrollableDefaults.snapBehavior에서 snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity를 설정하여 다음 페이지로 스냅하기 전에 더 많은 회전이 필요하도록 합니다.
@Composable
fun HorizontalPagerScaffoldWithLowSensitivitySample(navigateBack: () -> Unit) {
    AppScaffold {
        val pagerState = rememberPagerState(pageCount = { 3 })

        HorizontalPagerScaffold(pagerState = pagerState) {
            HorizontalPager(
                state = pagerState,
                flingBehavior =
                    PagerDefaults.snapFlingBehavior(
                        state = pagerState,
                        maxFlingPages = 0,
                        snapPositionalThreshold =
                            PagerScaffoldDefaults.LowSnapPositionalThreshold,
                    ),
                rotaryScrollableBehavior =
                    RotaryScrollableDefaults.snapBehavior(
                        pagerState = pagerState,
                        snapSensitivity =
                            RotaryScrollableDefaults.LowSnapSensitivity,
                    ),
            ) { page ->
                AnimatedPage(pageIndex = page, pagerState = pagerState) {
                    ScreenScaffold {
                        // Page content
                    }
                }
            }
        }
    }
}

전체 샘플 구현은 android/snippets 저장소의 Pager.kt과 AndroidX 저장소의 PagerScaffoldSample.kt을 참고하세요.