I'm trying to implement a BottomSheet
in Android (using either BottomSheetDialogFragment
or Jetpack's ModalBottomSheetLayout
) that vertically centers its content depending on whether the sheet is fully or partially expanded. In both implementations, the sheet itself seems to descend past the bottom edge of the screen, so any content visible there will be hidden. The "halfway" mark on the sheet is therefore static.
In my case, I want the content to remain centered within the visible portion of the sheet, not taking into account the invisible portion (i.e., between the top of the sheet and the bottom edge of the screen). If the content is too long, it would scroll via a ScrollView
or scrolling WebView
, etc. This seems to be easy to accomplish in SwiftUI
in iOS
, but so far I haven't been able to replicate this behavior on Android
. It seems like the layouts are simply not aware of how they're positioned within the screen's frame.
Here's an example using Jetpack that replicates the problem:
ModalBottomSheetLayout(
sheetState = bottomSheetState,
sheetShape = RoundedCornerShape(16.dp),
sheetContent = {
Column(modifier = Modifier
.fillMaxWidth()
.background(Color(0XFF0F9D58))) {
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Hello world!", fontSize = 20.sp, color = Color.White)
}
}
}
) {
Text("")
}
Perhaps I need to "manually" measure the halfway point, but I'm not quite sure how to do that. Any help would be appreciated.
I hope this helps, below is a simple attempt on how to make sure the content stay in the middle as the Sheet
is being dragged open/close, based on a Parallax
effect.
val configuration = LocalConfiguration.current
val coroutineScope = rememberCoroutineScope()
val density = LocalContext.current.resources.displayMetrics.density
val shDp = configuration.screenHeightDp.dp
val shPx = with(LocalDensity.current) { shDp.toPx() }
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
)
val sheetOffset = sheetState.offset.value
// weird parallax computation
val p1 = shPx / density
val parallax = (p1 / shPx) / 2
// just a button to show the bottom sheet
Button(onClick = {
if (!sheetState.isVisible) {
coroutineScope.launch {
sheetState.animateTo(ModalBottomSheetValue.HalfExpanded)
}
}
}) {}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(16.dp),
sheetContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color(0XFF0F9D58))
) {
Column(
modifier = Modifier
.height(shDp)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
var toOffset by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.size(50.dp)
.onGloballyPositioned {
val posInParent = it.positionInParent().y
toOffset = posInParent - (posInParent + sheetOffset)
}
.offset(y = (toOffset * parallax).dp)
.background(Color.Red)
)
}
}
}
) {
Text("")
}
What we're aiming here is a negative value that will continuously offset the Box
keeping it centered, so
I specified the Screen
height
to the inner most Column
composable using the Screen's
full height
Have a mutable state Float
offset
Computed Parallax
value using the following
p1 = shPx / density
parallax = (p1 / shPx) / 2
where shPx
is Screen height in pixels.
Observe Box's
layout coordinates via its
onGloballyPositioned {...}
callback, and get its y
position from its parent via .positionInParent().y
Continuously compute the offset
for the Box
while the sheet
is being dragged with the folowing
BoxOffset = (Box Y in Parent) - (Box Y in Parent + sheetOffset)
as .dp
with the following
BoxOffset * parallax
However I'm not sure if this is the better way to achieve this effect, I suspect that in every offset change, any composable starting from the Box
down to all the composable it would contain will re-compose
, I'm not sure though, I haven't tested this yet with such use-case and observe the layout inspector.