On Android using Jetpack Compose, I want to make the HorizontalPager scroll in only one direction, so forward only. I can’t get either pointerInput
or pointerInteropFilter
to correctly handle my gesture detection.
I've tried the following code, but neither option works
// if you use detectDragGesture on the box, then the message is printed, however the event does not bubble up and the horizontalPager will not swipe
// if you put the pointerInput onto the HorizontalPager, then the gesture is not detected and no message is printed
HorizontalPager(state = pagerState) { page ->
// Display each page with a different background color
Box(
modifier = Modifier
.fillMaxSize()
.background(colors[page])
.pointerInput(Unit) {
detectDragGestures { _, _ ->
println("In Here")
}
}
) {
}
}
// the other option is to use pointerInteropFilter
// the problem with this is that when used with the horizontalPager it does not detect the ACTION_MOVE for horizontal scroll
HorizontalPager(state = pagerState) { page ->
Box(
modifier = Modifier
.fillMaxSize()
.background(colors[page])
.pointerInteropFilter { motionEvent ->
when (motionEvent.action)
MotionEvent.ACTION_MOVE -> {
// This is not detected for horizontal scroll
println("ACTION_MOVE")
} }
true
}
) {
Text("Hello")
}
}
If I can correctly detect a backward swipe then I should be able to set userScrollEnabled
on the pager to disable the backward swipe.
Any other approach to disabling backward swiping would also work.
You can do it by gettin disabling user gesture via checking currentPageOffsetFraction
or by using a gesture with PointerEventPass.Initial
you can get gesture before horizontal scroll does as explained here. And by consuming event if pagerState.currentPageOffsetFraction
is below zero you can prevent HorizontalPager getting it.
@Preview
@Composable
fun PagerScrollCancelBackwardScroll() {
Column(
modifier = Modifier.fillMaxSize()
) {
val pagerState = rememberPagerState {
25
}
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = "Current page: ${pagerState.currentPage}\n" +
"settled Page: ${pagerState.settledPage}\n" +
"target Page: ${pagerState.targetPage}\n" +
"currentPageOffsetFraction: ${pagerState.currentPageOffsetFraction}\n" +
"isScrollInProgress: ${pagerState.isScrollInProgress}\n" +
"canScrollForward: ${pagerState.canScrollForward}\n" +
"canScrollBackward: ${pagerState.canScrollBackward}\n" +
"lastScrolledForward: ${pagerState.lastScrolledForward}\n" +
"lastScrolledBackward: ${pagerState.lastScrolledBackward}\n",
fontSize = 18.sp
)
val scrollEnabled by remember {
derivedStateOf {
pagerState.currentPageOffsetFraction >= 0
}
}
HorizontalPager(
userScrollEnabled = scrollEnabled,
state = pagerState,
pageSpacing = 16.dp,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(Color.LightGray, RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) {
Text(
text = "Page $it",
fontSize = 28.sp
)
}
}
}
}
@Preview
@Composable
fun PagerScrollCancelBackwardScrollNoEffect() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
val pagerState = rememberPagerState {
5
}
HorizontalPager(
modifier = Modifier
.pointerInput(Unit) {
awaitEachGesture {
val currentPageOffsetFraction = pagerState.currentPageOffsetFraction
val isBackwardsScroll = currentPageOffsetFraction < 0
val down = awaitFirstDown(
pass = PointerEventPass.Initial
)
if (isBackwardsScroll) {
down.consume()
}
println("currentPageOffsetFraction: $currentPageOffsetFraction")
do {
val event: PointerEvent = awaitPointerEvent(
pass = PointerEventPass.Initial
)
event.changes.forEach {
val diffX = it.position.x - it.previousPosition.x
if (diffX > 0) {
it.consume()
}
}
} while (event.changes.any { it.pressed })
}
},
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
)
}
}
}
}
@Preview
@Composable
fun PagerScrollCancelBackwardNoScrollEffect() {
Column(
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.height(16.dp))
val pagerState = rememberPagerState {
20
}
var userScrollEnabled by remember {
mutableStateOf(true)
}
HorizontalPager(
userScrollEnabled = userScrollEnabled,
modifier = Modifier
.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown(pass = PointerEventPass.Initial)
do {
val event: PointerEvent = awaitPointerEvent(
pass = PointerEventPass.Initial
)
event.changes.forEach {
val diffX = it.position.x - it.previousPosition.x
if (diffX > 0) {
userScrollEnabled = false
} else {
userScrollEnabled = true
}
}
} while (event.changes.any { it.pressed })
}
},
state = pagerState,
pageSpacing = 16.dp,
) { page ->
LazyColumn (
verticalArrangement = Arrangement.spacedBy(8.dp)
){
items(30)
{
Text(
text = "Page $page, item: $it",
fontSize = 28.sp
)
}
}
}
}
}
Edit3 With NestedScrollConnection
You can also pass zero offset from parent to HorizontalPager with NestedScrollConnection
if x is bigger than 0.
@Preview
@Composable
fun PagerScrollCancelBackwardScrollableContent() {
Column(
modifier = Modifier.fillMaxSize()
) {
val pagerState = rememberPagerState {
20
}
Spacer(modifier = Modifier.height(16.dp))
val coroutineScope = rememberCoroutineScope()
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
println("onPreScroll available: $available")
val availableX = available.x
val consumed = if (availableX > 0) availableX else 0f
return Offset(consumed, 0f)
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.settledPage)
}
return super.onPostScroll(consumed, available, source)
}
}
}
Box(modifier = Modifier.fillMaxSize().nestedScroll(nestedScrollConnection)) {
HorizontalPager(
state = pagerState,
pageSpacing = 16.dp,
) { page ->
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(30)
{
Text(
modifier = Modifier
.background(Color.Black)
.fillMaxWidth().padding(16.dp),
text = "Page $page, item: $it",
fontSize = 28.sp,
color = Color.White
)
}
}
}
}
}
}
In this one when you started scrolling you can't scroll back because it's always consumed. By modifying this you can change the amount it can scroll backwards not previous page as well.