Scroll issue with LazyColumn inside BottomSheetDialogFragment
Asked Answered
A

4

19

I use LazyColumn inside BottomSheetDialogFragment, but if to scroll LazyColumn list UP then Bottom Sheet Dialog scrolls instead of LazyColumn list. Seems like BottomSheetDialogFragment intercepts user touch input.

That's how it looks:

How to properly use LazyColumn inside BottomSheetDialogFragment?

MyBottomSheetDialogFragment.kt:

class MyBottomSheetDialogFragment : BottomSheetDialogFragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Text("Header", color = Color.Black)
                    LazyColumn(
                        Modifier
                            .weight(1f)
                            .fillMaxWidth()) {
                        items(100) {
                            Text("Item $it", Modifier.fillMaxWidth(), Color.Black)
                        }
                    }
                }
            }
        }
    }
}

And show it using this code:

MyBottomSheetDialogFragment().show(activity.supportFragmentManager, null)

When we used the XML RecyclerView list, to fix this issue we had to wrap the RecyclerView list with NestedScrollView like described here, but how to fix it with Jetpack Compose?

Abide answered 20/1, 2022 at 18:29 Comment(0)
A
1

I found an excellent answer to this issue. We can use Jetpack Compose bottom sheet over Android view using Kotlin extension.

More details about how it works are here.

Here is all code we need:

// Extension for Activity
fun Activity.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
    val viewGroup = this.findViewById(android.R.id.content) as ViewGroup
    addContentToView(viewGroup, content)
}

// Extension for Fragment
fun Fragment.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
    val viewGroup = requireActivity().findViewById(android.R.id.content) as ViewGroup
    addContentToView(viewGroup, content)
}

// Helper method
private fun addContentToView(
    viewGroup: ViewGroup,
    content: @Composable (() -> Unit) -> Unit
) {
    viewGroup.addView(
        ComposeView(viewGroup.context).apply {
            setContent {
                BottomSheetWrapper(viewGroup, this, content)
            }
        }
    )
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BottomSheetWrapper(
    parent: ViewGroup,
    composeView: ComposeView,
    content: @Composable (() -> Unit) -> Unit
) {
    val TAG = parent::class.java.simpleName
    val coroutineScope = rememberCoroutineScope()
    val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
    var isSheetOpened by remember { mutableStateOf(false) }

    ModalBottomSheetLayout(
        sheetBackgroundColor = Color.Transparent,
        sheetState = modalBottomSheetState,
        sheetContent = {
            content {
                // Action passed for clicking close button in the content
                coroutineScope.launch {
                    modalBottomSheetState.hide() // will trigger the LaunchedEffect
                }
            }
        }
    ) {}

    BackHandler {
        coroutineScope.launch {
            modalBottomSheetState.hide() // will trigger the LaunchedEffect
        }
    }

    // Take action based on hidden state
    LaunchedEffect(modalBottomSheetState.currentValue) {
        when (modalBottomSheetState.currentValue) {
            ModalBottomSheetValue.Hidden -> {
                when {
                    isSheetOpened -> parent.removeView(composeView)
                    else -> {
                        isSheetOpened = true
                        modalBottomSheetState.show()
                    }
                }
            }
            else -> {
                Log.i(TAG, "Bottom sheet ${modalBottomSheetState.currentValue} state")
            }
        }
    }
}
Abide answered 8/2, 2023 at 13:9 Comment(0)
S
31

Since compose 1.2.0-beta01 problem can be solved by using rememberNestedScrollInteropConnection:

Modifier.nestedScroll(rememberNestedScrollInteropConnection())

In my case BottomSheetDialogFragment is standard View and it has ComposeView with id container. In onViewCreated I do:

binding.container.setContent {
    AppTheme {
        Surface(
            modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())
        ) {
            LazyColumn {
                // ITEMS
            }
        }
    }
}

And now the list is scrolling in a correct way.

Spinescent answered 13/5, 2022 at 9:47 Comment(5)
I have a problem with the top of the bottomsheet not responding when i try to drag downwards to close the bottomsheet, is this the expected behaviour? (I have a Text-composable above the LazyColumn)Becket
@Becket Did you manage to resolve the downwards dragging issue? I am facing the same behavior where the top of the sheet is not responding to drag events.Centrosphere
I faced with the same issue guys @MarcelSchnelle. Asked about it here #75478519Goalie
For me the top dragging issue can be fixed by adding modifier = Modifier.verticalScroll(rememberScrollState()) to the header/title/top composable. Documentation also mentions: "To resolve this, make sure you also set scrollable modifiers to these types of nested composables"Phylloxera
The fix from @EdwardvanRaak helps to solve this issue since the scrolling deltas from the "not Scrolling" elements has also be passed to the NestedScrollConnection provided by rememberNestedScrollInteropConnection. You may ran into a render error Vertically scrollable component was measured with an infinity maximum height... To solve this you may add a hight to those "parent" elements by e.g. using Modifier.height(intrinsicSize = IntrinsicSize.Max)Guessrope
B
6

You might give a try to this https://gist.github.com/chrisbanes/053189c31302269656c1979edf418310.

This is a workaround for https://issuetracker.google.com/issues/174348612, which means that nested scrolling layouts in Compose do not work as nested scrolling children in the view system.

Sample usage in your case :

class MyBottomSheetDialogFragment : BottomSheetDialogFragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                Surface(
                    modifier = Modifier.nestedScroll(rememberViewInteropNestedScrollConnection())
                ){
                    LazyColumn(
                        Modifier
                            .weight(1f)
                            .fillMaxWidth()) {
                        items(100) {
                            Text("Item $it", Modifier.fillMaxWidth(), Color.Black)
                        }
                    }
                }
            }
        }
    }
}
Broaddus answered 24/1, 2022 at 15:5 Comment(0)
D
1

If you’re using compose within the activity that launched the bottom sheet dialog fragment, you might be better off simply sticking with a purely compose implementation and leveraging the compose equivalent bottom sheet component: ModalBottomSheetLayout

https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ModalBottomSheetLayout(kotlin.Function1,androidx.compose.ui.Modifier,androidx.compose.material.ModalBottomSheetState,androidx.compose.ui.graphics.Shape,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function0)

Diplostemonous answered 20/1, 2022 at 18:35 Comment(1)
I would like to use ModalBottomSheetLayout, but so far architecture of the application doesn't allow do implement this easily to the needed place. Seems like a lot of refactoring will be needed. That's why I use BottomSheetDialogFragment. Also, I use Jetpack Compose since new screens in the application we decided to write with this new technology. Anyway, thank you for the answer!Abide
A
1

I found an excellent answer to this issue. We can use Jetpack Compose bottom sheet over Android view using Kotlin extension.

More details about how it works are here.

Here is all code we need:

// Extension for Activity
fun Activity.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
    val viewGroup = this.findViewById(android.R.id.content) as ViewGroup
    addContentToView(viewGroup, content)
}

// Extension for Fragment
fun Fragment.showAsBottomSheet(content: @Composable (() -> Unit) -> Unit) {
    val viewGroup = requireActivity().findViewById(android.R.id.content) as ViewGroup
    addContentToView(viewGroup, content)
}

// Helper method
private fun addContentToView(
    viewGroup: ViewGroup,
    content: @Composable (() -> Unit) -> Unit
) {
    viewGroup.addView(
        ComposeView(viewGroup.context).apply {
            setContent {
                BottomSheetWrapper(viewGroup, this, content)
            }
        }
    )
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BottomSheetWrapper(
    parent: ViewGroup,
    composeView: ComposeView,
    content: @Composable (() -> Unit) -> Unit
) {
    val TAG = parent::class.java.simpleName
    val coroutineScope = rememberCoroutineScope()
    val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
    var isSheetOpened by remember { mutableStateOf(false) }

    ModalBottomSheetLayout(
        sheetBackgroundColor = Color.Transparent,
        sheetState = modalBottomSheetState,
        sheetContent = {
            content {
                // Action passed for clicking close button in the content
                coroutineScope.launch {
                    modalBottomSheetState.hide() // will trigger the LaunchedEffect
                }
            }
        }
    ) {}

    BackHandler {
        coroutineScope.launch {
            modalBottomSheetState.hide() // will trigger the LaunchedEffect
        }
    }

    // Take action based on hidden state
    LaunchedEffect(modalBottomSheetState.currentValue) {
        when (modalBottomSheetState.currentValue) {
            ModalBottomSheetValue.Hidden -> {
                when {
                    isSheetOpened -> parent.removeView(composeView)
                    else -> {
                        isSheetOpened = true
                        modalBottomSheetState.show()
                    }
                }
            }
            else -> {
                Log.i(TAG, "Bottom sheet ${modalBottomSheetState.currentValue} state")
            }
        }
    }
}
Abide answered 8/2, 2023 at 13:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.