22

I want to dynamically enable and disable scrolling programmatically in a LazyColumn.

There don't seem to be any relevant functions on LazyListState or relevant parameters on LazyColumn itself. How can I achieve this in Compose?

Ryan M
  • 18,333
  • 31
  • 67
  • 74
d-feverx
  • 1,424
  • 3
  • 16
  • 31

3 Answers3

30

Since 1.2.0-alpha01 userScrollEnabled was added to LazyColumn, LazyRow, and LazyVerticalGrid


Answer for 1.1.0 and earlier versions:

@Ryan's solution will also disable programmatically-called scrolling.

Here's a solution proposed by a maintainer in this feature request. It'll disable scrolling, allow programmatic scrolling as well as children view touches.

private val VerticalScrollConsumer = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource) = available.copy(x = 0f)
    override suspend fun onPreFling(available: Velocity) = available.copy(x = 0f)
}

private val HorizontalScrollConsumer = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource) = available.copy(y = 0f)
    override suspend fun onPreFling(available: Velocity) = available.copy(y = 0f)
}

fun Modifier.disabledVerticalPointerInputScroll(disabled: Boolean = true) =
    if (disabled) this.nestedScroll(VerticalScrollConsumer) else this

fun Modifier.disabledHorizontalPointerInputScroll(disabled: Boolean = true) =
    if (disabled) this.nestedScroll(HorizontalScrollConsumer) else this

Usage:

LazyColumn(
    modifier = Modifier.disabledVerticalPointerInputScroll()
) {
    // ...
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Currently this disables everything, so child composables don't receive any events? Is there anyway to allow children composables to still consume events so this modifier is a simple pass through? – Mark Nov 23 '21 at 22:01
  • 1
    Ok, found an answer - reading the docs `PointerEventPass.Main` seems to work better - "Up the tree from descendant to ancestor.". When applied to above code it means all unconsumed events from children are consumed by this Modifier, allowing decendent interaction and disabling ancestor - probably best for this feature request to be configurable. This also works well for `Pager`and `PagerState` in Accompanist libs. – Mark Nov 23 '21 at 22:36
  • 1
    @MarkKeen I tested your solution more intensely, and with `PointerEventPass.Main' some touches go through, and occasionally I can still scroll through the list. – Phil Dukhov Nov 28 '21 at 08:40
  • It does seem the lack of control over the gesture/touch system in a consistent and reliable way will be a problem - either I didn't understand the documentation correctly or there it is a bit buggy. I also discovered that child pinch to zoom was still disabled even with `PointerEventPass.Main` - I did read / look at the state diagram in the docs for ansers but it didn't really help with the providing a solution. – Mark Nov 28 '21 at 10:44
  • @Mark do you have a example of this PointerEventPass.Main solution? I need to disable pager swipe, but still have the map in one of it's child views to be touchable. – Peterdk Jun 23 '22 at 11:37
  • @Peterdk If you look at the edit history for the question it was amended to this originally - edit 2 -> 3. Later it was changed as this was deemed not full proof after more manual testing. Why would you not use the proposed solution above as it would seem that this would provide the solution for the mentioned use case. – Mark Jun 23 '22 at 13:24
  • @Mark, I can't swipe my map child view when using this answer, so it seems not to work. Therefor I was looking for alternative solutions. I do see that 1.2 Compose will have a property, but that one is not yet final. – Peterdk Jun 24 '22 at 08:17
  • @Peterdk I believe there's no solution if you need to have a scrollable view inside. The only solution I can think of is updating to 1.2.0 - it's close to the release now – Phil Dukhov Jun 24 '22 at 11:24
26

There's not (currently) a built-in way to do this, which is a reasonable feature request.

However, the scroll API is flexible enough that we can add it ourselves. Basically, we create a never-ending fake scroll at MutatePriority.PreventUserInput to prevent scrolling, and then use a do-nothing scroll at the same priority to cancel the first "scroll" and re-enable scrolling.

Here are two utility functions on LazyListState to disable/re-enable scrolling, and a demo of them both in action (some imports will be required, but Android Studio should suggest them for you).

Note that because we're taking control of scrolling to do this, calling reenableScrolling will also cancel any ongoing scrolls or flings (that is, you should only call it when scrolling is disabled and you want to re-enable it, not just to confirm that it's enabled).

fun LazyListState.disableScrolling(scope: CoroutineScope) {
    scope.launch {
        scroll(scrollPriority = MutatePriority.PreventUserInput) {
            // Await indefinitely, blocking scrolls
            awaitCancellation()
        }
    }
}

fun LazyListState.reenableScrolling(scope: CoroutineScope) {
    scope.launch {
        scroll(scrollPriority = MutatePriority.PreventUserInput) {
            // Do nothing, just cancel the previous indefinite "scroll"
        }
    }
}

@Composable
fun StopScrollDemo() {
    val scope = rememberCoroutineScope()
    val state = rememberLazyListState()
    Column {
        Row {
            Button(onClick = { state.disableScrolling(scope) }) { Text("Disable") }
            Button(onClick = { state.reenableScrolling(scope) }) { Text("Re-enable") }
        }
        LazyColumn(Modifier.fillMaxWidth(), state = state) {
            items((1..100).toList()) {
                Text("$it", fontSize = 24.sp)
            }
        }
    }
}
Ryan M
  • 18,333
  • 31
  • 67
  • 74
  • 7
    It appears this blocks all interactions with child composables when enabled including text fields, drop down menus etc .. assume this is because of `MutatePriority.PreventUserInput` prevents all input and blocks? Reason for asking in Accompanist Pager uses `LazyListState` and `PagerState` as the `ScrollState` delegate - I want to disable user scrolling whilst allowing child pages and composables to still consume events, is there anyway this could just be a passthrough for events, just not comsume scroll events (the pager api currently inflexible and limited)? – Mark Nov 23 '21 at 22:18
5

NestedScrollConnection allows you to consume any scroll applied to a lazy column or row. When true, all of the available scroll is consumed. If false, none is consumed and scrolling happens normally. With this information, you can see how this can be extended for slow/fast scrolls by returning the offset multiple by some factor.

fun Modifier.scrollEnabled(
    enabled: Boolean,
) = nestedScroll(
    connection = object : NestedScrollConnection {
        override fun onPreScroll(
            available: Offset,
            source: NestedScrollSource
        ): Offset = if(enabled) Offset.Zero else available
    }
)

it can be used like this:

LazyColumn(
    modifier = Modifier.scrollEnabled(
        enabled = enabled, //provide a mutable state boolean here
    )
){
    ...

However, this does block programmatic scrolls.

Darryl Johnson
  • 646
  • 6
  • 14