Reputation: 5912
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:
Here you can find a reproducible example
Upvotes: 14
Views: 13056
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
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
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
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
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