Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple BottomSheets for one ModalBottomSheetLayout in Jetpack Compose

I want to implement a screen which can show two different bottom sheets. Since ModalBottomSheetLayout only has a slot for one sheet I decided to change the sheetContent of the ModalBottomSheetLayout dynamically using a selected state when I want to show either of the two sheets (full code).

val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)

val (selected, setSelected) = remember(calculation = { mutableStateOf(0) })

ModalBottomSheetLayout(sheetState = sheetState, sheetContent = {
    when (selected) {
       0 -> Layout1()
       1 -> Layout2()
    }
}) {
   Content(sheetState = sheetState, setSelected = setSelected)
}

This works fine for very similar sheets, but as soon as you add more complexity to either of the two sheet layouts the sheet will not show when the button is pressed for the first time, it will only show after the button is pressed twice as you can see here:

2

Here you can find a reproducible example

like image 215
Yannick Avatar asked Mar 01 '21 11:03

Yannick


People also ask

What is mutableStateOf in jetpack compose?

mutableStateOf creates an observable MutableState<T> , which is an observable type integrated with the compose runtime. interface MutableState<T> : State<T> { override var value: T } Any changes to value will schedule recomposition of any composable functions that read value .

What is LazyColumn in jetpack compose?

A LazyColumn is a vertically scrolling list that only composes and lays out the currently visible items. It's similar to a Recyclerview in the classic Android View system.

What is scaffold in jetpack compose?

A Scaffold is a layout which implements the basic material design layout structure. You can add things like a TopBar, BottomBar, FAB or a Drawer.


1 Answers

I had a similar usecase, where I needed to show 2-3 stacked bottomsheets. I ended up copying large part of Compose BottomSheet and added the desired behavior:

enum class BottomSheetValue { SHOWING, HIDDEN }

@Composable
fun BottomSheet(
        parentHeight: Int,
        topOffset: Dp = 0.dp,
        fillMaxHeight: Boolean = false,
        sheetState: SwipeableState<BottomSheetValue>,
        shape: Shape = bottomSheetShape,
        backgroundColor: Color = MaterialTheme.colors.background,
        contentColor: Color = contentColorFor(backgroundColor),
        elevation: Dp = 0.dp,
        content: @Composable () -> Unit
) {
    val topOffsetPx = with(LocalDensity.current) { topOffset.roundToPx() }
    var bottomSheetHeight by remember { mutableStateOf(parentHeight.toFloat())}

    val scrollConnection = sheetState.PreUpPostDownNestedScrollConnection

    BottomSheetLayout(
        maxHeight = parentHeight - topOffsetPx,
        fillMaxHeight = fillMaxHeight
    ) {
        val swipeable = Modifier.swipeable(
            state = sheetState,
            anchors = mapOf(
                parentHeight.toFloat() to BottomSheetValue.HIDDEN,
                parentHeight - bottomSheetHeight to BottomSheetValue.SHOWING
            ),
            orientation = Orientation.Vertical,
            resistance = null
        )

        Surface(
            shape = shape,
            color = backgroundColor,
            contentColor = contentColor,
            elevation = elevation,
            modifier = Modifier
                .nestedScroll(scrollConnection)
                .offset { IntOffset(0, sheetState.offset.value.roundToInt()) }
                .then(swipeable)
                .onGloballyPositioned {
                    bottomSheetHeight = it.size.height.toFloat()
                },
        ) {
            content()
        }
    }
}


@Composable
private fun BottomSheetLayout(
        maxHeight: Int,
        fillMaxHeight: Boolean,
        content: @Composable () -> Unit
) {
    Layout(content = content) { measurables, constraints ->
        val sheetConstraints =
            if (fillMaxHeight) {
                constraints.copy(minHeight = maxHeight, maxHeight = maxHeight)
            } else {
                constraints.copy(maxHeight = maxHeight)
            }

        val placeable = measurables.first().measure(sheetConstraints)

        layout(placeable.width, placeable.height) {
            placeable.placeRelative(0, 0)
        }
    }
}

TopOffset e.g. allows to place the bottomSheet below the AppBar:

BoxWithConstraints {
 BottomSheet(
                parentHeight = constraints.maxHeight,
                topOffset = with(LocalDensity.current) {56.toDp()}
                fillMaxHeight = true,
                sheetState = yourSheetState,
            ) {
                content()
            }
}
like image 77
jns Avatar answered Nov 15 '22 08:11

jns