Paginación con Compose para Wear OS

La paginación permite a los usuarios deslizar el dedo horizontal o verticalmente entre páginas distintas de pantalla completa en dispositivos Wear OS. Los casos de uso comunes incluyen deslizar el dedo entre las métricas de entrenamiento y los controles multimedia en una app de ejercicios, o bien avanzar por flujos de varias páginas.

En Compose para Wear OS Material 3, HorizontalPagerScaffold y VerticalPagerScaffold coordinan el diseño del paginador, posicionan automáticamente el indicador de página y administran las transiciones con TimeText. Cada página se incluye en un elemento AnimatedPage componible, que aplica efectos de escala, transformación de esquinas redondeadas y scrim a medida que las páginas realizan la transición en la pantalla redonda.

En la siguiente animación, se muestra cómo HorizontalPagerScaffold y AnimatedPage escalan y animan las páginas durante los deslizamientos horizontales:

Jerarquía de andamios para la paginación

Cuando diseñes diseños de pantalla paginados en Material 3, ya sea que deslices el dedo hacia la izquierda o la derecha (HorizontalPager) o hacia arriba o hacia abajo (VerticalPager), usa la siguiente jerarquía de componentes, desde el contenedor exterior hasta el contenido interior:

  1. AppScaffold: Es el contenedor más externo en la raíz de tu app (usa solo uno por app). Ancla la superposición global TimeText para que el reloj permanezca fijo en la parte superior de la pantalla durante las transiciones de página y los gestos de deslizamiento para descartar.
  2. HorizontalPagerScaffold o VerticalPagerScaffold: Se coloca dentro de AppScaffold a nivel del paginador. Coordina las transiciones entre TimeText y HorizontalPageIndicator o VerticalPageIndicator.
  3. HorizontalPager o VerticalPager: Es el contenedor de paginación básico que administra los gestos de deslizamiento, la física de arrastre y el ajuste de entrada rotativa con un PagerState compartido.
  4. AnimatedPage: Se coloca dentro de la expresión lambda del contenido de la página del paginador. Envuelve cada página individual para aplicar animaciones de transición de Material 3 (efectos de ajuste y velado) según el desplazamiento de la página en PagerState.
  5. ScreenScaffold: Se coloca dentro de AnimatedPage para cada página individual. Dado que cada página puede contener su propia lista desplazable verticalmente (como un TransformingLazyColumn) o su propio EdgeButton, anidar ScreenScaffold dentro de cada página garantiza que los indicadores de desplazamiento vertical, los botones de borde y contentPadding pertenezcan a esa página específica y se animen sin problemas con ella.
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)

Implementa un carrusel horizontal

Para implementar un paginador horizontal en Material 3, anida HorizontalPagerScaffold, HorizontalPager, AnimatedPage y ScreenScaffold dentro del AppScaffold de tu app.

Usa PagerScaffoldDefaults.snapWithSpringFlingBehavior para que flingBehavior aplique la física de movimiento de resorte de Material 3 y el ajuste de página responsivo (HighSnapPositionalThreshold). De forma predeterminada, HorizontalPager inhabilita el desplazamiento de página rotatorio (rotaryScrollableBehavior = null), lo que permite que la entrada de la corona o el bisel rotatorio desplace listas verticales (como un TransformingLazyColumn) dentro de la página activa.

En el siguiente ejemplo, se muestra una configuración completa de 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") }
                            }
                        }
                    }
                }
            }
        }
    }
}

Si una página dentro de tu paginador horizontal contiene un TransformingLazyColumn, crea un rememberTransformingLazyColumnState() independiente dentro del alcance de esa página y pásalo a los ScreenScaffold y TransformingLazyColumn internos:

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

Implementa un carrusel vertical

Un localizador vertical permite a los usuarios deslizar el dedo hacia arriba y hacia abajo entre las páginas o recorrerlas con el botón lateral giratorio o el bisel del reloj.

A diferencia de HorizontalPager, VerticalPager habilita el ajuste rotatorio de páginas de forma predeterminada (RotaryScrollableDefaults.snapBehavior(state)). Pasa PagerScaffoldDefaults.snapWithSpringFlingBehavior a flingBehavior y envuelve cada página en AnimatedPage y 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")
                        }
                    }
                }
            }
        }
    }
}

Personaliza el comportamiento del paginador

Puedes personalizar la sensibilidad de ajuste de HorizontalPagerScaffold y VerticalPagerScaffold para que coincida con las necesidades de interacción de tu app.

Cómo ajustar la sensibilidad de los ajustes para los entrenamientos

Durante los entrenamientos o las actividades de alto movimiento en los que el control de la motricidad gruesa del usuario es limitado, los toques accidentales o las rotaciones leves de la corona pueden cambiar de página sin intención. Para estas pantallas, configura el paginador con una sensibilidad de ajuste baja:

  • Pasa PagerDefaults.snapFlingBehavior con maxFlingPages = 0 y snapPositionalThreshold = PagerScaffoldDefaults.LowSnapPositionalThreshold, por lo que se requiere un arrastre deliberado en una porción más grande de la pantalla para pasar la página.
  • Si habilitas el ajuste de página con el selector, configura snapSensitivity = RotaryScrollableDefaults.LowSnapSensitivity en RotaryScrollableDefaults.snapBehavior para requerir más rotación antes de ajustar la página siguiente.
@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
                    }
                }
            }
        }
    }
}

Para ver implementaciones de muestras completas, consulta Pager.kt en el repositorio de android/snippets y PagerScaffoldSample.kt en el repositorio de AndroidX.