Localizador en Compose

Para desplazarte por el contenido de forma horizontal o vertical, puedes usar los elementos componibles HorizontalPager y VerticalPager. Estas tienen funciones similares a ViewPager en el sistema de vistas. De forma predeterminada, HorizontalPager ocupa todo el ancho de la pantalla y VerticalPager ocupa toda la altura. Los paginadores también solo arrojan una página a la vez. Todos estos valores predeterminados se pueden configurar.

HorizontalPager

Para crear un localizador que se desplace horizontalmente hacia la izquierda y la derecha, usa HorizontalPager:

Figura 1: Demostración de HorizontalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

VerticalPager

Para crear un localizador que se desplace hacia arriba y hacia abajo, usa VerticalPager:

Figura 2. Demostración de VerticalPager

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

Creación diferida

Las páginas de HorizontalPager y VerticalPager se componen de forma diferida y se disponen cuando es necesario. A medida que el usuario se desplaza por las páginas, el elemento de componibilidad quita las páginas que ya no son necesarias.

Carga más páginas fuera de la pantalla

De forma predeterminada, el paginador solo carga las páginas visibles en la pantalla. Para cargar más páginas fuera de la pantalla, establece beyondBoundsPageCount en un valor superior a cero.

Desplázate a un elemento en el paginador

Para desplazarte a una página determinada en el paginador, crea un objeto PagerState con rememberPagerState() y pásalo como parámetro state al paginador. Puedes llamar a PagerState#scrollToPage() en este estado, dentro de un CoroutineScope:

val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.scrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

Si quieres animar la página, usa la función PagerState#animateScrollToPage():

val pagerState = rememberPagerState(pageCount = {
    10
})

HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.animateScrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

Recibe notificaciones sobre los cambios de estado de la página

PagerState tiene tres propiedades con información sobre las páginas: currentPage, settledPage y targetPage.

  • currentPage: Es la página más cercana a la posición de ajuste. De forma predeterminada, la posición de ajuste se encuentra al principio del diseño.
  • settledPage: Es el número de página cuando no se está ejecutando ninguna animación ni desplazamiento. Esto es diferente de la propiedad currentPage, ya que currentPage se actualiza de inmediato si la página está lo suficientemente cerca de la posición de ajuste, pero settledPage sigue siendo el mismo hasta que se terminen de ejecutar todas las animaciones.
  • targetPage: Es la posición de detención propuesta para un movimiento de desplazamiento.

Puedes usar la función snapshotFlow para observar los cambios en estas variables y reaccionar a ellos. Por ejemplo, para enviar un evento de Analytics en cada cambio de página, puedes hacer lo siguiente:

val pagerState = rememberPagerState(pageCount = {
    10
})

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

VerticalPager(
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}

Agrega un indicador de página

Para agregar un indicador a una página, usa el objeto PagerState para obtener información sobre qué página se selecciona de la cantidad de páginas y dibuja tu indicador personalizado.

Por ejemplo, para crear un indicador circular, puedes repetir la cantidad de círculos y cambiar el color del círculo según si la página está seleccionada, con pagerState.currentPage:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    modifier = Modifier.fillMaxSize()
) { page ->
    // Our page content
    Text(
        text = "Page: $page",
    )
}
Row(
    Modifier
        .wrapContentHeight()
        .fillMaxWidth()
        .align(Alignment.BottomCenter)
        .padding(bottom = 8.dp),
    horizontalArrangement = Arrangement.Center
) {
    repeat(pagerState.pageCount) { iteration ->
        val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
        Box(
            modifier = Modifier
                .padding(2.dp)
                .clip(CircleShape)
                .background(color)
                .size(16.dp)
        )
    }
}

Pager que muestra un indicador circular debajo del contenido
Figura 3: Pager que muestra un indicador circular debajo del contenido

Aplica efectos de desplazamiento de elementos al contenido

Un caso de uso común es usar la posición de desplazamiento para aplicar efectos a los elementos del buscapersonas. Para averiguar qué tan lejos está una página de la página seleccionada, puedes usar PagerState.currentPageOffsetFraction. Luego, puedes aplicar efectos de transformación a tu contenido según la distancia desde la página seleccionada.

Figura 4: Aplica transformaciones al contenido de Pager

Por ejemplo, para ajustar la opacidad de los elementos según la distancia que tengan con respecto al centro, cambia el alpha con Modifier.graphicsLayer en un elemento dentro del buscapersonas:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(state = pagerState) { page ->
    Card(
        Modifier
            .size(200.dp)
            .graphicsLayer {
                // Calculate the absolute offset for the current page from the
                // scroll position. We use the absolute value which allows us to mirror
                // any effects for both directions
                val pageOffset = (
                    (pagerState.currentPage - page) + pagerState
                        .currentPageOffsetFraction
                    ).absoluteValue

                // We animate the alpha, between 50% and 100%
                alpha = lerp(
                    start = 0.5f,
                    stop = 1f,
                    fraction = 1f - pageOffset.coerceIn(0f, 1f)
                )
            }
    ) {
        // Card content
    }
}

Tamaños de página personalizados

De forma predeterminada, HorizontalPager y VerticalPager ocupan todo el ancho o toda la altura, respectivamente. Puedes configurar la variable pageSize para que tenga un cálculo de tamaño Fixed, Fill (predeterminado) o personalizado.

Por ejemplo, para establecer una página de ancho fijo de 100.dp, haz lo siguiente:

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) { page ->
    // page content
}

Para ajustar el tamaño de las páginas según el tamaño del viewport, usa un cálculo personalizado del tamaño de la página. Crea un objeto PageSize personalizado y divide el availableSpace por tres, teniendo en cuenta el espaciado entre los elementos:

private val threePagesPerViewport = object : PageSize {
    override fun Density.calculateMainAxisPageSize(
        availableSpace: Int,
        pageSpacing: Int
    ): Int {
        return (availableSpace - 2 * pageSpacing) / 3
    }
}

Padding del contenido

HorizontalPager y VerticalPager admiten el cambio del padding del contenido, lo que te permite influir en el tamaño máximo y la alineación de las páginas.

Por ejemplo, si configuras el padding start, las páginas se alinearán hacia el final:

Pager con padding de inicio que muestra el contenido alineado hacia el final
Figura 5: Pager con padding de inicio.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(start = 64.dp),
) { page ->
    // page content
}

Si configuras el padding de start y end con el mismo valor, el elemento se centrará de forma horizontal:

Pager con padding al inicio y al final que muestra el contenido centrado
Figura 6: Pager con padding horizontal.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(horizontal = 32.dp),
) { page ->
    // page content
}

Si configuras el padding end, las páginas se alinearán hacia el principio:

Pager con padding al inicio y al final que muestra el contenido alineado al inicio
Figura 7: Pager con padding final.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(end = 64.dp),
) { page ->
    // page content
}

Puedes establecer los valores top y bottom para lograr efectos similares en VerticalPager. El valor 32.dp solo se usa aquí como ejemplo; puedes establecer cada una de las dimensiones de padding en cualquier valor.

Personaliza el comportamiento de desplazamiento

Los elementos componibles predeterminados HorizontalPager y VerticalPager especifican cómo funcionan los gestos de desplazamiento con el paginador. Sin embargo, puedes personalizar y cambiar los valores predeterminados, como pagerSnapDistance o flingBehavior.

Distancia de ajuste

De forma predeterminada, HorizontalPager y VerticalPager establecen la cantidad máxima de páginas por las que se puede desplazar un gesto de deslizamiento rápido a una página por vez. Para cambiar esto, configura pagerSnapDistance en flingBehavior:

val pagerState = rememberPagerState(pageCount = { 10 })

val fling = PagerDefaults.flingBehavior(
    state = pagerState,
    pagerSnapDistance = PagerSnapDistance.atMost(10)
)

Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        state = pagerState,
        pageSize = PageSize.Fixed(200.dp),
        beyondViewportPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}

Crea un buscapersonas con avance automático

En esta sección, se describe cómo crear un buscapersonas de avance automático con indicadores de página en Compose. La colección de elementos se desplaza automáticamente de forma horizontal, pero los usuarios también pueden deslizar el dedo entre los elementos de forma manual. Si un usuario interactúa con el buscapersonas, se detiene el progreso automático.

Ejemplo básico

Juntos, los siguientes fragmentos crean una implementación básica de un buscapersonas con avance automático con un indicador visual, en el que cada página se renderiza con un color diferente:

@Composable
fun AutoAdvancePager(pageItems: List<Color>, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        val pagerState = rememberPagerState(pageCount = { pageItems.size })
        val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()

        val pageInteractionSource = remember { MutableInteractionSource() }
        val pageIsPressed by pageInteractionSource.collectIsPressedAsState()

        // Stop auto-advancing when pager is dragged or one of the pages is pressed
        val autoAdvance = !pagerIsDragged && !pageIsPressed

        if (autoAdvance) {
            LaunchedEffect(pagerState, pageInteractionSource) {
                while (true) {
                    delay(2000)
                    val nextPage = (pagerState.currentPage + 1) % pageItems.size
                    pagerState.animateScrollToPage(nextPage)
                }
            }
        }

        HorizontalPager(
            state = pagerState
        ) { page ->
            Text(
                text = "Page: $page",
                textAlign = TextAlign.Center,
                modifier = modifier
                    .fillMaxSize()
                    .background(pageItems[page])
                    .clickable(
                        interactionSource = pageInteractionSource,
                        indication = LocalIndication.current
                    ) {
                        // Handle page click
                    }
                    .wrapContentSize(align = Alignment.Center)
            )
        }

        PagerIndicator(pageItems.size, pagerState.currentPage)
    }
}

Puntos clave sobre el código

  • La función AutoAdvancePager crea una vista de paginación horizontal con avance automático. Toma una lista de objetos Color como entrada, que se usan como colores de fondo para cada página.
  • pagerState se crea con rememberPagerState, que contiene el estado del paginador.
  • pagerIsDragged y pageIsPressed hacen un seguimiento de la interacción del usuario.
  • El LaunchedEffect avanza automáticamente el paginador cada dos segundos, a menos que el usuario lo arrastre o presione una de las páginas.
  • HorizontalPager muestra una lista de páginas, cada una con un elemento de componibilidad Text que muestra el número de página. El modificador llena la página, establece el color de fondo desde pageItems y hace que se pueda hacer clic en la página.

@Composable
fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        Row(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            repeat(pageCount) { iteration ->
                val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray
                Box(
                    modifier = modifier
                        .padding(2.dp)
                        .clip(CircleShape)
                        .background(color)
                        .size(16.dp)
                )
            }
        }
    }
}

Puntos clave sobre el código

  • Un elemento Box componible actúa como el elemento raíz y contiene un Row para organizar los indicadores de página de forma horizontal.
  • Un indicador de página personalizado se muestra como una fila de círculos, en la que cada Box recortado en un CircleShape representa una página.
  • El círculo de la página actual tiene el color DarkGray, mientras que los otros círculos tienen el color LightGray. El parámetro currentPageIndex determina qué círculo se renderiza en gris oscuro.

Resultado

En este video, se muestra el buscapersonas básico de avance automático de los fragmentos anteriores:

Figura 8. Un buscapersonas con avance automático y un retraso de dos segundos entre cada progresión de página.

Recursos adicionales