androidandroid-jetpack-composeandroid-jetpack-compose-gestureandroid-jetpack-compose-pager

How do I make Jetpack Compose HorizontalPager swipe forward only (disable backward swipe)?


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.


Solution

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

    enter image description here

    @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
                    )
                }
            }
        }
    }
    

    Edit1: If you don't want to see overscroll effect

    @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
                    )
                }
            }
        }
    }
    

    Edit2: If you have inner scrollable content

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