diff --git a/pager-indicators/api/current.api b/pager-indicators/api/current.api
index 7044b5576..e4c8947c3 100644
--- a/pager-indicators/api/current.api
+++ b/pager-indicators/api/current.api
@@ -2,8 +2,8 @@
package com.google.accompanist.pager {
public final class PagerIndicatorKt {
- method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void HorizontalPagerIndicator(com.google.accompanist.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor, optional float indicatorWidth, optional float indicatorHeight, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape);
- method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void VerticalPagerIndicator(com.google.accompanist.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor, optional float indicatorHeight, optional float indicatorWidth, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape);
+ method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void HorizontalPagerIndicator(com.google.accompanist.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional int pageCount, optional kotlin.jvm.functions.Function1 super java.lang.Integer,java.lang.Integer> pageIndexMapping, optional long activeColor, optional long inactiveColor, optional float indicatorWidth, optional float indicatorHeight, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape);
+ method @androidx.compose.runtime.Composable @com.google.accompanist.pager.ExperimentalPagerApi public static void VerticalPagerIndicator(com.google.accompanist.pager.PagerState pagerState, optional androidx.compose.ui.Modifier modifier, optional int pageCount, optional kotlin.jvm.functions.Function1 super java.lang.Integer,java.lang.Integer> pageIndexMapping, optional long activeColor, optional long inactiveColor, optional float indicatorHeight, optional float indicatorWidth, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape);
}
public final class PagerTabKt {
diff --git a/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt b/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt
index c051d75dc..60a8ce391 100644
--- a/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt
+++ b/pager-indicators/src/main/java/com/google/accompanist/pager/PagerIndicator.kt
@@ -36,6 +36,8 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import kotlin.math.absoluteValue
+import kotlin.math.sign
/**
* An horizontally laid out indicator for a [HorizontalPager] or [VerticalPager], representing
@@ -48,6 +50,11 @@ import androidx.compose.ui.unit.dp
*
* @param pagerState the state object of your [Pager] to be used to observe the list's state.
* @param modifier the modifier to apply to this layout.
+ * @param pageCount the size of indicators should be displayed, defaults to [PagerState.pageCount].
+ * If you are implementing a looping pager with a much larger [PagerState.pageCount]
+ * than indicators should displayed, e.g. [Int.MAX_VALUE], specify you real size in this param.
+ * @param pageIndexMapping describe how to get the position of active indicator by the giving page
+ * from [PagerState.currentPage], if [pageCount] is not equals to [PagerState.pageCount].
* @param activeColor the color of the active Page indicator
* @param inactiveColor the color of page indicators that are inactive. This defaults to
* [activeColor] with the alpha component set to the [ContentAlpha.disabled].
@@ -61,6 +68,8 @@ import androidx.compose.ui.unit.dp
fun HorizontalPagerIndicator(
pagerState: PagerState,
modifier: Modifier = Modifier,
+ pageCount: Int = pagerState.pageCount,
+ pageIndexMapping: (Int) -> Int = { it },
activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
inactiveColor: Color = activeColor.copy(ContentAlpha.disabled),
indicatorWidth: Dp = 8.dp,
@@ -84,7 +93,7 @@ fun HorizontalPagerIndicator(
.size(width = indicatorWidth, height = indicatorHeight)
.background(color = inactiveColor, shape = indicatorShape)
- repeat(pagerState.pageCount) {
+ repeat(pageCount) {
Box(indicatorModifier)
}
}
@@ -92,8 +101,12 @@ fun HorizontalPagerIndicator(
Box(
Modifier
.offset {
- val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffset)
- .coerceIn(0f, (pagerState.pageCount - 1).coerceAtLeast(0).toFloat())
+ val position = pageIndexMapping(pagerState.currentPage)
+ val offset = pagerState.currentPageOffset
+ val next = pageIndexMapping(pagerState.currentPage + offset.sign.toInt())
+ val scrollPosition = ((next - position) * offset.absoluteValue + position)
+ .coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat())
+
IntOffset(
x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(),
y = 0
@@ -119,6 +132,11 @@ fun HorizontalPagerIndicator(
*
* @param pagerState the state object of your [Pager] to be used to observe the list's state.
* @param modifier the modifier to apply to this layout.
+ * @param pageCount the size of indicators should be displayed, defaults to [PagerState.pageCount].
+ * If you are implementing a looping pager with a much larger [PagerState.pageCount]
+ * than indicators should displayed, e.g. [Int.MAX_VALUE], specify you real size in this param.
+ * @param pageIndexMapping describe how to get the position of active indicator by the giving page
+ * from [PagerState.currentPage], if [pageCount] is not equals to [PagerState.pageCount].
* @param activeColor the color of the active Page indicator
* @param inactiveColor the color of page indicators that are inactive. This defaults to
* [activeColor] with the alpha component set to the [ContentAlpha.disabled].
@@ -132,6 +150,8 @@ fun HorizontalPagerIndicator(
fun VerticalPagerIndicator(
pagerState: PagerState,
modifier: Modifier = Modifier,
+ pageCount: Int = pagerState.pageCount,
+ pageIndexMapping: (Int) -> Int = { it },
activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
inactiveColor: Color = activeColor.copy(ContentAlpha.disabled),
indicatorHeight: Dp = 8.dp,
@@ -155,7 +175,7 @@ fun VerticalPagerIndicator(
.size(width = indicatorWidth, height = indicatorHeight)
.background(color = inactiveColor, shape = indicatorShape)
- repeat(pagerState.pageCount) {
+ repeat(pageCount) {
Box(indicatorModifier)
}
}
@@ -163,8 +183,12 @@ fun VerticalPagerIndicator(
Box(
Modifier
.offset {
- val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffset)
- .coerceIn(0f, (pagerState.pageCount - 1).coerceAtLeast(0).toFloat())
+ val position = pageIndexMapping(pagerState.currentPage)
+ val offset = pagerState.currentPageOffset
+ val next = pageIndexMapping(pagerState.currentPage + offset.sign.toInt())
+ val scrollPosition = ((next - position) * offset.absoluteValue + position)
+ .coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat())
+
IntOffset(
x = 0,
y = ((spacingPx + indicatorHeightPx) * scrollPosition).toInt(),
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index 19c6020e8..0c1872fc8 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -117,6 +117,15 @@
+
+
+
+
+
+
+ Column(
+ Modifier
+ .fillMaxSize()
+ .padding(padding)
+ ) {
+ // Display 10 items
+ val pageCount = 10
+
+ // We start the pager in the middle of the raw number of pages
+ val loopingCount = Int.MAX_VALUE
+ val startIndex = loopingCount / 2
+ val pagerState = rememberPagerState(initialPage = startIndex)
+
+ fun pageMapper(index: Int): Int {
+ return (index - startIndex).floorMod(pageCount)
+ }
+
+ HorizontalPager(
+ // Set the raw page count to a really large number
+ count = loopingCount,
+ state = pagerState,
+ // Add 32.dp horizontal padding to 'center' the pages
+ contentPadding = PaddingValues(horizontal = 32.dp),
+ // Add some horizontal spacing between items
+ itemSpacing = 4.dp,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ ) { index ->
+ // We calculate the page from the given index
+ val page = pageMapper(index)
+ PagerSampleItem(
+ page = page,
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ )
+ }
+ HorizontalPagerIndicator(
+ pagerState = pagerState,
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(16.dp),
+ pageCount = pageCount,
+ pageIndexMapping = ::pageMapper
+ )
+
+ val loopState = remember {
+ mutableStateOf(true)
+ }
+
+ LoopControl(loopState, Modifier.align(Alignment.CenterHorizontally))
+
+ ActionsRow(
+ pagerState = pagerState,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ infiniteLoop = true
+ )
+
+ var underDragging by remember {
+ mutableStateOf(false)
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ pagerState.interactionSource.interactions.collect { interaction ->
+ when (interaction) {
+ is PressInteraction.Press -> underDragging = true
+ is PressInteraction.Release -> underDragging = false
+ is PressInteraction.Cancel -> underDragging = false
+ is DragInteraction.Start -> underDragging = true
+ is DragInteraction.Stop -> underDragging = false
+ is DragInteraction.Cancel -> underDragging = false
+ }
+ }
+ }
+
+ val looping = loopState.value
+ if (underDragging.not() && looping) {
+ LaunchedEffect(key1 = underDragging) {
+ try {
+ while (true) {
+ delay(1000L)
+ val current = pagerState.currentPage
+ val currentPos = pageMapper(current)
+ val nextPage = current + 1
+ if (underDragging.not()) {
+ val toPage = nextPage.takeIf { nextPage < pagerState.pageCount } ?: (currentPos + startIndex + 1)
+ if (toPage > current) {
+ pagerState.animateScrollToPage(toPage)
+ } else {
+ pagerState.scrollToPage(toPage)
+ }
+ }
+ }
+ } catch (e: CancellationException) {
+ Log.i("page", "Launched paging cancelled")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun LoopControl(
+ loopState: MutableState,
+ modifier: Modifier = Modifier,
+) {
+ IconButton(
+ onClick = { loopState.value = loopState.value.not() },
+ modifier = modifier
+ ) {
+ val icon = if (loopState.value) {
+ Icons.Default.PauseCircle
+ } else {
+ Icons.Default.PlayCircle
+ }
+ Icon(imageVector = icon, contentDescription = null)
+ }
+}
+
+private fun Int.floorMod(other: Int): Int = when (other) {
+ 0 -> this
+ else -> this - floorDiv(other) * other
+}
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
index bcd76f806..c28fce9a4 100644
--- a/sample/src/main/res/values/strings.xml
+++ b/sample/src/main/res/values/strings.xml
@@ -27,6 +27,7 @@
Horizontal Pager: Indicator
Horizontal Pager: Transition
Horizontal Pager: Looping
+ Horizontal Pager: Looping with Indicators
Horizontal Pager: Tabs
Horizontal Pager: Scrolling content
Horizontal Pager: Different paddings