the default ModalBottomsheet is not give me a flexibility to customize the UI to look like the UI in the screenshot
You can customize the ModalBottomSheet
to match the UI from the picture, and here is an example:
Firstly, add the following parameters to the BottomSheet
composable function:
isBottomSheetVisible: Boolean
- Used for controlling the visibility of the bottom sheet.
sheetState: SheetState
- The state of the bottom sheet.
onDismiss: () -> Unit
- The callback to dismiss the bottom sheet.
Then, wrap the ModalBottomSheet
inside if (isBottomSheetVisible) {}
block and change the following sheet properties:
onDismissRequest
to onDismiss
.
sheetState
to sheetState
.
containerColor
to Color.Transparent
.
contentColor
to your content color e.g. a text color.
shape
to a RectangleShape
.
dragHandle
to null
scrimColor
to the scrim color of the design (e.g. Color.Black.copy(alpha = .5f)
).
And optionally set the windowInsets
to WindowInsets(0, 0, 0, 0)
if you have Edge-To-Edge enabled, to expand the bottom sheet under the system bars.
So far, the BottomSheet
composable should look like this:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheet(
isBottomSheetVisible: Boolean,
sheetState: SheetState,
onDismiss: () -> Unit
) {
if (isBottomSheetVisible) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface,
shape = RectangleShape,
dragHandle = null,
scrimColor = Color.Black.copy(alpha = .5f),
windowInsets = WindowInsets(0, 0, 0, 0)
) {
// Implement the custom layout here ...
}
}
}
Now, you can start implementing the custom layout inside the content
block of the ModalBottomSheet
:
ModalBottomSheet
can be wrapped inside a Box
with a bit of padding and customizing it further:
Box(
modifier = Modifier
.statusBarsPadding()
.fillMaxWidth()
.padding(8.dp),
contentAlignment = Alignment.Center
) {
FilledIconButton(
modifier = Modifier.size(48.dp),
onClick = onDismiss,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.background
)
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = "Dismiss the dialog."
)
}
}
Then, below the Box
add a Column
, and implement the remaining UI components:
Column(
modifier = Modifier
.navigationBarsPadding()
.padding(12.dp) // Outer padding
.clip(shape = RoundedCornerShape(24.dp))
.background(color = MaterialTheme.colorScheme.background)
.fillMaxWidth()
.padding(24.dp) // Inner padding
) {
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = "10th avenue, Some, State",
onValueChange = {},
label = { Text(text = "Delivery Address") },
readOnly = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(24.dp))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = "09090606000",
onValueChange = {},
label = { Text(text = "Number we can call") },
readOnly = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(48.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedButton(
onClick = {},
content = { Text(text = "Pay on delivery") }
)
OutlinedButton(
onClick = {},
content = { Text(text = "Pay with card") }
)
}
}
Finally, wherever you need the bottom sheet, declare the following variables:
scope
- A coroutine scope used to efficiently expand or hide the bottom sheet.
isBottomSheetVisible
- Used for controlling the bottom sheet visibility.
sheetState
- The state of the bottom sheet.
Then, implement the BottomSheet
composable:
val scope = rememberCoroutineScope()
var isBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
BottomSheet(
isBottomSheetVisible = isBottomSheetVisible,
sheetState = sheetState,
onDismiss = {
scope.launch { sheetState.hide() }
.invokeOnCompletion { isBottomSheetVisible = false }
}
)
This is the full code that I've wrote and a visual demonstration:
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val scope = rememberCoroutineScope()
var isBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
// A box with a simple button
// just to show the bottom sheet.
Box(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.padding(64.dp),
contentAlignment = Alignment.BottomCenter
) {
OutlinedButton(
onClick = {
scope.launch {
isBottomSheetVisible = true
sheetState.expand()
}
}
) {
Text(text = "Show")
}
}
BottomSheet(
isBottomSheetVisible = isBottomSheetVisible,
sheetState = sheetState,
onDismiss = {
scope.launch { sheetState.hide() }
.invokeOnCompletion { isBottomSheetVisible = false }
}
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomSheet(
isBottomSheetVisible: Boolean,
sheetState: SheetState,
onDismiss: () -> Unit
) {
if (isBottomSheetVisible) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface,
shape = RectangleShape,
dragHandle = null,
scrimColor = Color.Black.copy(alpha = .5f),
windowInsets = WindowInsets(0, 0, 0, 0)
) {
Box(
modifier = Modifier
.statusBarsPadding()
.fillMaxWidth()
.padding(8.dp),
contentAlignment = Alignment.Center
) {
FilledIconButton(
modifier = Modifier.size(48.dp),
onClick = onDismiss,
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.background
)
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = "Hide the dialog."
)
}
}
Column(
modifier = Modifier
.navigationBarsPadding()
.padding(12.dp) // Outer padding
.clip(shape = RoundedCornerShape(24.dp))
.background(color = MaterialTheme.colorScheme.background)
.fillMaxWidth()
.padding(24.dp) // Inner padding
) {
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = "10th avenue, Some, State",
onValueChange = {},
label = { Text(text = "Delivery Address") },
readOnly = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(24.dp))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = "09090606000",
onValueChange = {},
label = { Text(text = "Number we can call") },
readOnly = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(48.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedButton(
onClick = {},
content = { Text(text = "Pay on delivery") }
)
OutlinedButton(
onClick = {},
content = { Text(text = "Pay with card") }
)
}
}
}
}
}
Happy coding! ????????
To implement a custom BottomSheet in Jetpack Compose with the flexibility to customize the UI, you can create a custom Composable that mimics the behavior of a BottomSheet. Heres a simple example to get you started:
kotlinimport androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.corner.CircleSize
import androidx.compose.material3.*
import androidx.compose.material3.icons.Icons
import androidx.compose.material3.icons.filled.Close
import androidx.compose.material3.icons.filled.Person
import androidx.compose.material3.icons.filled.Phone
import androidx.compose.material3.icons.filled.Place
import androidx.compose.material3.swiperefreshindicator.SwipeRefreshIndicatorDefaults
import androidx.compose.material3.swiperefreshindicator.rememberSwipeRefreshState
import androidx.compose.material3.transition.MaterialSharedAxis
import androidx.compose.material3.utils.addHeaderAdapter
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.layer
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalDensityOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalSoftwareKeyboardControllerProvider
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.LocalViewTreeLifecycleOwner
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.platform.LocalWindowProvider
import androidx.compose.ui.platform.LocalWindowRect
import androidx.compose.ui.platform.LocalWindowSize
import androidx.compose.ui.platform.LocalWindowTimeoutDispatcher
import androidx.compose.ui.platform.LocalWindowTree
import androidx.compose.ui.platform.LocalWindowTreeLifecycleOwner
import androidx.compose.ui.platform.LocalWindowTreePostFrameCallback
import androidx.compose.ui.platform.LocalWindowTreePostFrameCallbackProvider
import androidx.compose.ui.platform.LocalWindowTreeProvider
import androidx.compose.ui.platform.LocalWindowViewModelProvider
import androidx.compose.ui.platform.LocalWindowViewport
import androidx.compose.ui.platform.LocalWindowViewModelProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider
import androidx.compose.ui.platform.LocalWindowViewProvider