Yannick
Yannick

Reputation: 5912

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

Upvotes: 14

Views: 13056

Answers (5)

Tom
Tom

Reputation: 7824

It doesn't look pretty but you can simply nest ModalBottomSheetLayout. No issues in my app.

 val sheet1State = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
 val sheet2State = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)

 ModalBottomSheetLayout(
     sheetState = sheet1State,
     sheetContent = {
         Sheet1Content(/*...*/)
     },
     content = {
         ModalBottomSheetLayout(
             sheetState = sheet2State,
             sheetContent = {
                 Sheet2Content(/*...*/)
             },
             content = {
                 ScreenContent()
             }
         )
     }
 )

Upvotes: 0

Nojipiz
Nojipiz

Reputation: 63

This solution works with N amount of bottom sheets, and even works if you want to stack them, here is the implementation:

typealias BottomSheetControl = Pair<ModalBottomSheetState, @Composable ColumnScope.() -> Unit> 

@Composable
fun HeaderContentMultipleBottomSheetsTemplate(
    finalContent: @Composable () -> Unit,
    sheetsShape: Shape = ServyUIShapesDefaults.RoundedRectangle,
    vararg sheets: BottomSheetControl
) {
    HeaderContentBottomSheetsTemplateImpl(
        finalContent = finalContent,
        sheetsShape = sheetsShape,
        sheets = sheets,
    )
}

@Composable
private fun HeaderContentBottomSheetsTemplateImpl(
    modifier: Modifier = Modifier,
    finalContent: @Composable () -> Unit,
    scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
    sheetsShape: Shape = ServyUIShapesDefaults.RoundedRectangle,
    vararg sheets: BottomSheetControl
) {
    if (sheets.isNotEmpty()) {
        ModalBottomSheetLayout(
            modifier = modifier,
            sheetState = sheets[0].first,
            sheetContent = sheets[0].second,
            sheetShape = sheetsShape,
            scrimColor = scrimColor
        ) {
            HeaderContentBottomSheetsTemplateImpl(
                finalContent = finalContent,
                sheets = sheets.sliceArray(1 until sheets.size)
            )
        }
    } else {
        finalContent() // Here is your Screen Content
    }
}

For the implementation you need to have this :

// The sheet state, you can manipulate it whatever you want.
val testSheetState= rememberModalBottomSheetState(
        initialValue = ModalBottomSheetValue.Hidden,
        animationSpec = tween(durationMillis = 100),
        confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded },
        skipHalfExpanded = true,
    )

// The sheet content.
@Composable
fun TestSheet(): @Composable ColumnScope.() -> Unit = {
    Text(text = "LOLLL")
}

// The Pair of state and content 
val test: BottomSheetControl = Pair(testSheetState, TestSheet())

And finally you can just:

HeaderContentMultipleBottomSheetsTemplate(
    finalContent = {
        // Here the content of your screen (in ColumnScope)
    },
    sheets = arrayOf(test)
)

Pd: The order of the items in the vararg argument "sheets" define the level of each one, if you want to stack them, please write in nested order.

Upvotes: 0

Tim Yumalin
Tim Yumalin

Reputation: 520

I implemented it like this. It looks pretty simple, but I still could not figure out how to pass the argument to "mutableStateOf ()" directly, I had to create a variable "content"

fun Screen() {
    val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
    val scope = rememberCoroutineScope()
    val content: @Composable (() -> Unit) = { Text("NULL") }
    var customSheetContent by remember { mutableStateOf(content) }
    ModalBottomSheetLayout(
        sheetState = bottomSheetState,
        sheetContent = {
            customSheetContent()
        }
    ) {
        Column {
        Button(
            onClick = {
                customSheetContent = { SomeComposable1() }
                scope.launch { bottomSheetState.show() }
        }) {
         Text("First Button")
        }

        Button(
            onClick = {
                customSheetContent = { SomeComposable2() }
                scope.launch { bottomSheetState.show() }
        }) {
         Text("Second Button")
        }
    }
    }

Upvotes: 6

jns
jns

Reputation: 6952

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

Upvotes: 7

David Ibrahim
David Ibrahim

Reputation: 3227

I wanted to implement the same thing and because of the big soln, I wrote a post on dev.to that solves this problem, Here is the link

Upvotes: 3

Related Questions