Compose LazyColumn scrolling behavior inside of CoordinatorLayout via ComposeView interop
Asked Answered
F

2

16

Problem - scrolling downward causes the bottom sheet to scroll rather than giving scroll priority to the LazyColumn (RecyclerView did not have this problem. It was wrapped by a NestedScrollView)

I've just introduced a Compose LazyColumn replacement of a Recycler inside of a CoordinatorLayout. The Coordinator (implemented as a bottom sheet) can itself scroll freely between peek and expanded states. My issue is when dragging the items area downward in the LazyColumn, the bottom sheet picks up the scrolling rather than the LazyColumn . If I scroll upward first and then downward (without releasing) on the LazyColumn, the scrolling is picked up by the LazyColumn and scrolling priority is given to the LazyColumn (expected behavior.)

BottomSheetFragment
|-CoordinatorLayout
|--ConstraintLayout (BottomSheetBehavior)
|---MyListFragment
|----ComposeView
|-----Theme
|------Surface
|-------Box
|--------LazyColumn

New to Compose, so I'm hoping someone can tell me how to approach correcting this new scroll behavior?

**Edit I'm getting part of the way to having this work by toggling the Coordinator's ^^ BottomSheetBehavior.isDragglable, but it does require that I release the drag rather than smoothly transitioning from the list scroll to the bottom sheet scroll - anyone suggest a fix?:

fun MyUi(listener:Listener) {
    val listState = rememberLazyListState()

    LaunchedEffect(listState) {
        listState.interactionSource.interactions.collect {
            //at the top of the list so allow sheet scrolling
            listener.allowSheetDrag(listState.firstVisibleItemScrollOffset == 0)
        }
    }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                Timber.i("NestedScrollConnection onPreScroll($available: Offset, $source: NestedScrollSource)")
                return super.onPreScroll(available, source)
            }

            override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
                Timber.i("NestedScrollConnection onPostScroll($consumed: Offset, $available: Offset, $source: NestedScrollSource)")
                if (available.y > 0.0 && consumed.y == 0.0f) {
                    //scolling down up but we're already at the top - kick over to sheet scrolling
                    listener.allowSheetDrag(true)
                }
                return super.onPostScroll(consumed, available, source)
            }
        }
    }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        LazyColumn(
            modifier =
            Modifier
                .fillMaxSize()
                .padding(vertical = 12.dp), state = listState
        ) {
            item {
                Row() {}
            }
        }
    }
}

And then in the Fragment:

override fun allowSheetDrag(allowSheetDrag: Boolean) {
    bottomSheetFragment?.bottomSheetBehavior?.isDraggable = allowSheetDrag
}
Flosser answered 29/10, 2021 at 14:42 Comment(10)
If LazyColumn has no items above(i.e its already showing 1st item) and you scroll(drag) downward then it's expected that scroll gets picked up by parent scroll container. This is exactly what you are observing in first case.Whensoever
Elaborate on what behavior you expect. Do you always want scrolling to be consumed entirely by LazyColumn, regardless of whether it has already reached top/bottom?Whensoever
If LazyColumn has items above or below the visible area, scrolling always goes first to the coordinator - this mean that the list items above the visible are can never be scrolled down into view because the coordinator's view starts scrolling when dragging over the visible items.Flosser
Invisible items below can be scrolled into view if the coordinator's state is expanded.Flosser
Have you figured out a solution yet? Stuck with the same problem.... Also asked in Kotlin / Composes Slack channel: kotlinlang.slack.com/archives/CJLTWPH7S/p1636492760204500Selfabnegation
@SteffenFunke I just started looking into it again today - it's really the only issue blocking me from integrating our first Compose ui. I started looking into using a custom bottom sheet behavior for this (I'm mostly unfamiliar with coordinator/sheets etc.)Flosser
@SteffenFunke check out my partial solution. Let me know if you can advance this to smooth scrolling from the list to the bottom sheet.Flosser
@Flosser Thanks for getting back! I'll give it a try as soon as I am back at the project. Keep you posted.Selfabnegation
@Flosser I tinkered together a somehow working solution, based on your idea on setting isDraggable. Needed some extra tweakings for ironing out some edge cases while dragging, and am quite happy with the solution now. Basically observing listStates scrolling state in a LaunchedEffect, and setting some flags depending on where you touch down (list or container). It still requires to lift the finger once, when dragging up, to continue dragging the sheet downwards, but I found out this is actually how other some sheets behave as well, so I consider it ok for my use case. Thanks again!Selfabnegation
For anyone interested, I have pushed an example to GitHub: github.com/sfunke/bottomsheetdialogfragment_compose_interopSelfabnegation
S
28

Wizard Chris Banes just recently published a workaround, which I can confirm it works.

It also transitions nicely without lifting a finger, when being used inside a BottomSheetDialog (scrolling up, then dragging the sheet down in one go)

Example Usage (taken from Chris Banes' example):

setContent {
    Surface(
        // Add this somewhere near the top of your layout, above any scrolling layouts
        modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())
    ) {
        LazyColumn() {
            // blah
        }
    }
}

There is an issue being tracked for nested scrolling with ComposeViews: https://issuetracker.google.com/issues/174348612, and a related SO question which lead me there: AndroidView in Compose loses touch events in NestedScrollView

Selfabnegation answered 2/12, 2021 at 7:39 Comment(0)
V
6

Unfortunately, the interop for scrollability between XML and Compose is not great. There is a method on View called isNestedScrollingEnabled(), and Compose always returns false, so nested scrolling has unpredictable behaviors (which I believe you're describing here).

The way around it that I have found is to ensure that for content that can scroll larger than the full screen, you're placing your BottomSheet in a BottomSheetScaffold or other custom Composable view. As it stands, you will probably need to convert the whole experience to Compose before it will work in the way we would expect.

Compose is always evolving, as well. These comments are accurate for Compose 1.0.4 and Material Design library 1.3.0- it might change in the future.

Velez answered 10/11, 2021 at 4:53 Comment(3)
Thanks for the tip on the nested scroll property - that would almost get me completely there, but I haven't yet found how to seamlessly transfer the scroll from the list as it reaches its end to the bottom sheet w/o releasing and scrolling again. There's probably a way to consume drag events from the sheet and direct them to the LazyColumn until the column has reached the end (or rather top) of its scrolling.Flosser
isNestedScrollingEnabled() seems to do this but changing the enabled value doesn't take effect until a new scroll begins.Flosser
FYI see my comment under @jchristofs question for a hacked together, but working solution on GitHub. Still, you have to lift up the finger once. No luck fiddling around with isNestedScrollingEnabled , unfortunately. But at least, a few sheets on my android device (e.g. app drawer) behave the same, so ¯_(ツ)_/¯Selfabnegation

© 2022 - 2024 — McMap. All rights reserved.