Localizador en Compose

Para desplazarte por el contenido de manera izquierda y derecha o hacia arriba y abajo, puedes usar los elementos componibles HorizontalPager y VerticalPager, respectivamente. Esos elementos componibles tienen funciones similares a ViewPager en el sistema de vistas. De forma predeterminada, HorizontalPager ocupa todo el ancho de la pantalla, VerticalPager ocupa todo el alto y los localizadores solo desplazan 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 organizan cuando es necesario. A medida que el usuario se desplaza por las páginas, el elemento componible quita las páginas que ya no son necesarias.

Cargar más páginas fuera de la pantalla

De forma predeterminada, el localizador 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 hasta un elemento en el localizador

Para desplazarte a una página determinada en el localizador, crea un objeto PagerState con rememberPagerState() y pásalo como parámetro state al localizador. 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")
}

Cómo recibir notificaciones sobre los cambios en el 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 del ajuste. De forma predeterminada, la posición del ajuste es al comienzo del diseño.
  • settledPage: Es el número de página cuando no se ejecutan animaciones ni desplazamiento. La diferencia con la propiedad currentPage es que currentPage se actualiza de inmediato si la página está lo suficientemente cerca de la posición del ajuste, pero settledPage permanece igual hasta que terminan de ejecutarse todas las animaciones.
  • targetPage: Es la posición de parada 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 estadísticas 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")
}

Agregar un indicador de página

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

Por ejemplo, si quieres un indicador de círculo simple, puedes repetir la cantidad de círculos y cambiar el color del círculo en función de si la página está seleccionada. Para ello, usa 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)
        )
    }
}

Localizador que muestra un indicador de círculo debajo del contenido
Figura 3: Localizador que muestra un indicador de círculo debajo del contenido

Cómo aplicar 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 localizador. Para saber a qué distancia se encuentra una página de la página seleccionada actualmente, puedes usar PagerState.currentPageOffsetFraction. Luego, puedes aplicar efectos de transformación a tu contenido en función de la distancia desde la página seleccionada.

Figura 4: Aplicación de transformaciones al contenido de Pager

Por ejemplo, para ajustar la opacidad de los elementos según la distancia a la que se encuentran del centro, cambia el alpha usando Modifier.graphicsLayer en un elemento dentro del localizador:

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 todo el alto, respectivamente. Puedes configurar la variable pageSize para que tenga un Fixed, un Fill (predeterminado) o un cálculo de tamañ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 en función del 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 en 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 y la alineación máximos de las páginas.

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

Localizador con padding inicial que muestra el contenido alineado hacia el final

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

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

Localizador con padding de inicio y final que muestra el contenido centrado

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

Cuando se establece el padding de end, las páginas se alinean hacia el comienzo:

Localizador con padding de inicio y fin que muestra el contenido alineado al inicio

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 para VerticalPager. El valor 32.dp solo se usa aquí como ejemplo; puedes configurar cada una de las dimensiones de padding con cualquier valor.

Personaliza el comportamiento de desplazamiento

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

Ajustar distancia

De forma predeterminada, HorizontalPager y VerticalPager establecen la cantidad máxima de páginas por las que un gesto de deslizamiento se puede desplazar y pasar a una página a la 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),
        beyondBoundsPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}