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
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 .
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.
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.
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()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With