I'd like to detect when a user attempts to swipe past the last page of a HorizontalPager. At that point, I'd like it to scroll back to the first page. I don't want an infinite pager, I simply want it to scroll back to the beginning upon swiping past the last page.
I'd imagine this involves setting up some kind of pointer input gesture detector, but I'm not sure how to do this without breaking the built-in gesture detection within HorizonalPager.
You can use Modifier.pointerInput and awaitPointerEvent(pass = PointerEventPass.Initial)
. PointerEventPass.Initial
makes sure that Modifier.pointerInput is invoked before inner one of HorizontalPager via Modifier.scroll. Also since we don't consume any event we do not interfere with pager. You can refer answers below for more details about gestures and event propagation.
https://stackoverflow.com/a/70847531/5457853
Jetpack Compose Intercept pinch/zoom in child layout
However since Pager subcomposes items based on beyondPageSomething, they changed name of this param on 1.7 again, it will animate one or more pages via pager.animateScrollToPage
Result
Demo
@Preview
@Composable
fun PagerScrollSample() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
val pagerState = rememberPagerState {
5
}
var shouldScrollToFirstPage by remember {
mutableStateOf(false)
}
LaunchedEffect(shouldScrollToFirstPage) {
if (shouldScrollToFirstPage) {
delay(100)
pagerState.animateScrollToPage(0)
shouldScrollToFirstPage = false
}
}
Text("shouldScrollToFirstPage: $shouldScrollToFirstPage")
HorizontalPager(
userScrollEnabled = shouldScrollToFirstPage.not(),
modifier = Modifier
.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown()
shouldScrollToFirstPage = false
do {
val event: PointerEvent = awaitPointerEvent(
pass = PointerEventPass.Initial
)
event.changes.forEach {
if (pagerState.currentPage == 4 &&
pagerState.currentPage == pagerState.settledPage &&
// current position of finger
it.position.x < 200f &&
shouldScrollToFirstPage.not()
) {
shouldScrollToFirstPage = true
}
}
} while (event.changes.any { it.pressed })
// User lifts pointer, you can animate here as well
// if (pagerState.currentPage == 4 &&
// pagerState.currentPageOffsetFraction == 0f
// ) {
// shouldScrollToFirstPage = true
// }
}
},
state = pagerState,
pageSpacing = 16.dp,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.LightGray, RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) {
Text(
text = "Page $it",
fontSize = 28.sp
)
}
}
}
}
Another alternative with partially visible and clickable items
@Preview
@Composable
private fun PagerScrollSample2() {
val pagerState = rememberPagerState {
5
}
var shouldScrollToFirstPage by remember {
mutableStateOf(false)
}
val coroutineScope = rememberCoroutineScope()
BoxWithConstraints {
val pageSpacing = 16.dp
val pageWidth = maxWidth - pageSpacing - 32.dp
HorizontalPager(
userScrollEnabled = shouldScrollToFirstPage.not(),
contentPadding = PaddingValues(horizontal = 16.dp),
pageSize = PageSize.Fixed(pageWidth),
modifier = Modifier
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(pass = PointerEventPass.Initial)
shouldScrollToFirstPage = false
val firstTouchX = down.position.x
do {
val event: PointerEvent = awaitPointerEvent(
pass = PointerEventPass.Initial
)
event.changes.forEach {
val diff = firstTouchX - it.position.x
if (pagerState.currentPage == 4 &&
pagerState.currentPage ==
pagerState.settledPage &&
// Scroll if user scrolled 10% from first touch position
// or pointer is at the left of 20% of page
(diff > size.width * .10f ||
it.position.x < size.width * .2f) &&
shouldScrollToFirstPage.not()
) {
coroutineScope.launch {
shouldScrollToFirstPage = true
pagerState.animateScrollToPage(
0,
animationSpec = tween(500)
)
shouldScrollToFirstPage = false
}
}
}
} while (event.changes.any { it.pressed })
}
},
state = pagerState,
pageSpacing = pageSpacing,
) {
val context = LocalContext.current
Box(
modifier = Modifier
.clickable {
Toast.makeText(context, "Clicked $it", Toast.LENGTH_SHORT).show()
}
.fillMaxWidth()
.height(200.dp)
.background(Color.LightGray, RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) {
Text(
text = "Page $it",
fontSize = 28.sp
)
}
}
}
Button(
modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(),
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(0)
}
}
) {
Text("Scroll to first page")
}
}