Stronicowanie za pomocą Compose na Wear OS

Stronicowanie umożliwia użytkownikom przesuwanie palcem w poziomie lub pionie między różnymi stronami na pełnym ekranie na urządzeniach z Wear OS. Typowe zastosowania to m.in. przełączanie się między danymi treningowymi a elementami sterującymi multimediami w aplikacji do ćwiczeń lub przechodzenie przez wielostronicowe procesy.

W Compose for Wear OS Material 3 komponenty HorizontalPagerScaffoldVerticalPagerScaffold koordynują układ stronicowania, automatycznie pozycjonują wskaźnik strony i zarządzają przejściami za pomocą TimeText. Każda strona jest opakowana w komponent AnimatedPage, który stosuje skalowanie, zaokrąglanie rogów i efekty przyciemnienia podczas przechodzenia stron na okrągłym wyświetlaczu.

Poniższa animacja pokazuje, jak HorizontalPagerScaffoldAnimatedPage skalują i animują strony podczas przesuwania w poziomie:

Hierarchia szkieletowa na potrzeby stronicowania

Podczas projektowania układów ekranów z podziałem na strony w Material 3 – niezależnie od tego, czy przewijasz w lewo lub w prawo (HorizontalPager), czy w górę lub w dół (VerticalPager) – używaj tej hierarchii komponentów od kontenera zewnętrznego do treści wewnętrznej:

  1. AppScaffold: najbardziej zewnętrzny kontener w katalogu głównym aplikacji (używaj tylko jednego na aplikację). Przytrzymuje globalną nakładkę TimeText, dzięki czemu zegar pozostaje nieruchomy u góry ekranu podczas przejść między stronami i gestów odrzucania przez przesunięcie.
  2. HorizontalPagerScaffold lub VerticalPagerScaffold: umieszczony w AppScaffold na poziomie pagera. Koordynuje przejścia między TimeTextHorizontalPageIndicator lub VerticalPageIndicator.
  3. HorizontalPager lub VerticalPager: kontener stronicowania podstawowego, który zarządza gestami przesuwania, fizyką rzucania i przyciąganiem danych wejściowych z pokrętła za pomocą udostępnionego elementu PagerState.
  4. AnimatedPage: umieszczony w funkcji lambda treści strony pagera. Zawiera każdą stronę, aby zastosować animacje przejścia Material 3 (efekty skalowania i przesłony) na podstawie przesunięcia strony w PagerState.
  5. ScreenScaffold: umieszczony wewnątrz AnimatedPage na każdej stronie. Każda strona może zawierać własną listę z możliwością przewijania w pionie (np. TransformingLazyColumn) lub własny EdgeButton. Zagnieżdżenie ScreenScaffold na każdej stronie zapewnia, że wskaźniki przewijania w pionie, przyciski krawędziowe i contentPadding należą do tej konkretnej strony i płynnie się z nią animują.
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)

Wdrażanie podziału na strony w poziomie

Aby zaimplementować poziomy pager w Material 3, zagnieźdź HorizontalPagerScaffold, HorizontalPager, AnimatedPageScreenScaffoldAppScaffold aplikacji.

Użyj PagerScaffoldDefaults.snapWithSpringFlingBehavior dla flingBehavior, aby zastosować fizykę ruchu sprężynowego Material 3 i elastyczne przyciąganie strony (HighSnapPositionalThreshold). Domyślnie HorizontalPager wyłącza przewijanie strony za pomocą obrotu (rotaryScrollableBehavior = null), umożliwiając przewijanie list pionowych (np. TransformingLazyColumn) na aktywnej stronie za pomocą obrotowego pokrętła lub ramki.

Poniższy przykład pokazuje kompletną konfigurację 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") }
                            }
                        }
                    }
                }
            }
        }
    }
}

Jeśli strona w pagerze poziomym zawiera element TransformingLazyColumn, utwórz osobny element rememberTransformingLazyColumnState() w zakresie tej strony i przekaż go do wewnętrznych elementów 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")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Wdrażanie miejsca podziału na strony w pionie

Pionowy pager umożliwia użytkownikom przesuwanie palcem w górę i w dół między stronami lub przechodzenie między nimi za pomocą obrotowego przycisku bocznego lub ramki zegarka.

W przeciwieństwie do HorizontalPager, VerticalPager domyślnie włącza przyciąganie obrotowe strony (RotaryScrollableDefaults.snapBehavior(state)). Przekaż PagerScaffoldDefaults.snapWithSpringFlingBehavior do flingBehavior i otocz każdą stronę tagami 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")
                        }
                    }
                }
            }
        }
    }
}

Dostosowywanie działania pagera

Możesz dostosować czułość przyciągania HorizontalPagerScaffoldVerticalPagerScaffold do potrzeb interakcji w aplikacji.

Dostosowywanie czułości przyciągania podczas treningów

Podczas treningów lub aktywności o dużej intensywności, gdy użytkownik ma ograniczoną kontrolę nad ruchami dużymi, przypadkowe dotknięcia lub niewielkie obroty pokrętła mogą nieumyślnie przełączać strony. W przypadku tych ekranów skonfiguruj pager z niską czułością przyciągania:

  • Przesuń PagerDefaults.snapFlingBehaviormaxFlingPages = 0snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold, aby przewrócić stronę. Wymaga to celowego przeciągnięcia po większej części ekranu.
  • Jeśli włączysz przyciąganie strony podczas obracania, ustaw wartość snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity w polu RotaryScrollableDefaults.snapBehavior, aby wymagać większego obrotu przed przyciągnięciem do następnej strony.
@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
                    }
                }
            }
        }
    }
}

Pełne przykłady implementacji znajdziesz w Pager.kt w repozytorium android/snippetsPagerScaffoldSample.kt w repozytorium AndroidX.