Paginazione con Compose per Wear OS

La paginazione consente agli utenti di scorrere orizzontalmente o verticalmente tra pagine distinte a schermo intero sui dispositivi Wear OS. I casi d'uso comuni includono lo scorrimento tra le metriche dell'allenamento e i controlli multimediali in un'app di allenamento o la navigazione in flussi di più pagine.

In Compose per Wear OS Material 3, HorizontalPagerScaffold e VerticalPagerScaffold coordinano il layout del pager, posizionando automaticamente l'indicatore di pagina e gestendo le transizioni con TimeText. Ogni pagina è racchiusa in un componente componibile AnimatedPage, che applica effetti di ridimensionamento, morphing degli angoli arrotondati e scrim durante la transizione delle pagine sul display rotondo.

La seguente animazione mostra come HorizontalPagerScaffold e AnimatedPage scalano e animano le pagine durante gli scorrimenti orizzontali:

Struttura della gerarchia per la paginazione

Quando progetti layout di schermate impaginate in Material 3, sia che scorri verso sinistra o destra (HorizontalPager) o verso l'alto o il basso (VerticalPager), utilizza la seguente gerarchia dei componenti dal contenitore esterno ai contenuti interni:

  1. AppScaffold: il contenitore più esterno nella radice dell'app (utilizzane solo uno per app). Ancora l'overlay globale TimeText in modo che l'orologio rimanga fermo nella parte superiore dello schermo durante le transizioni di pagina e i gesti di scorrimento per chiudere.
  2. HorizontalPagerScaffold o VerticalPagerScaffold: inserito all'interno di AppScaffold a livello di paginazione. Coordina le transizioni tra TimeText e HorizontalPageIndicator o VerticalPageIndicator.
  3. HorizontalPager o VerticalPager: il contenitore del pager di base che gestisce i gesti di scorrimento, la fisica di scorrimento rapido e l'input rotativo utilizzando un PagerState condiviso.
  4. AnimatedPage: inserito all'interno della lambda dei contenuti della pagina del pager. Esegue il wrapping di ogni singola pagina per applicare le animazioni di transizione Material 3 (effetti di ridimensionamento e velatura) in base all'offset della pagina in PagerState.
  5. ScreenScaffold: inserito all'interno di AnimatedPage per ogni singola pagina. Poiché ogni pagina può contenere un proprio elenco scorrevole verticalmente (ad esempio un TransformingLazyColumn) o un proprio EdgeButton, l'incorporamento di ScreenScaffold all'interno di ogni pagina garantisce che gli indicatori di scorrimento verticale, i pulsanti sui bordi e contentPadding appartengano a quella pagina specifica e si animino in modo fluido.
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)

Implementare un pager orizzontale

Per implementare un pager orizzontale in Material 3, annida HorizontalPagerScaffold, HorizontalPager, AnimatedPage e ScreenScaffold all'interno di AppScaffold dell'app.

Utilizza PagerScaffoldDefaults.snapWithSpringFlingBehavior per flingBehavior per applicare la fisica del movimento a molla di Material 3 e lo snapping reattivo della pagina (HighSnapPositionalThreshold). Per impostazione predefinita, HorizontalPager disattiva lo scorrimento della pagina rotante (rotaryScrollableBehavior = null), consentendo all'input della corona rotante o della ghiera di scorrere gli elenchi verticali (ad esempio un TransformingLazyColumn) all'interno della pagina attiva.

Il seguente esempio mostra una configurazione HorizontalPagerScaffold completa:

@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") }
                            }
                        }
                    }
                }
            }
        }
    }
}

Se una pagina all'interno del pager orizzontale contiene un TransformingLazyColumn, crea un rememberTransformingLazyColumnState() separato all'interno dell'ambito della pagina e passalo ai ScreenScaffold e TransformingLazyColumn interni:

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

Implementare un pager verticale

Un pager verticale consente agli utenti di scorrere verso l'alto e verso il basso tra le pagine o di sfogliarle utilizzando il pulsante laterale rotante o la ghiera dello smartwatch.

A differenza di HorizontalPager, VerticalPager attiva l'allineamento rotatorio delle pagine per impostazione predefinita (RotaryScrollableDefaults.snapBehavior(state)). Passa PagerScaffoldDefaults.snapWithSpringFlingBehavior a flingBehavior e racchiudi ogni pagina in AnimatedPage e ScreenScaffold:

@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")
                        }
                    }
                }
            }
        }
    }
}

Personalizzare il comportamento del cercapersone

Puoi personalizzare la sensibilità dello snapping di HorizontalPagerScaffold e VerticalPagerScaffold in base alle esigenze di interazione della tua app.

Regolare la sensibilità dello snap per gli allenamenti

Durante gli allenamenti o le attività con movimenti ampi in cui il controllo motorio dell'utente è limitato, tocchi accidentali o leggere rotazioni della corona possono cambiare pagina involontariamente. Per queste schermate, configura il pager con una sensibilità di scatto bassa:

  • Passa PagerDefaults.snapFlingBehavior con maxFlingPages = 0 e snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold pertanto è necessario trascinare deliberatamente su una porzione più ampia dello schermo per girare la pagina.
  • Se attivi lo scorrimento rotatorio delle pagine, imposta snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity in RotaryScrollableDefaults.snapBehavior per richiedere una rotazione maggiore prima di passare alla pagina successiva.
@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
                    }
                }
            }
        }
    }
}

Per implementazioni di esempio complete, consulta Pager.kt nel repository android/snippets e PagerScaffoldSample.kt nel repository AndroidX.