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 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 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