how to implement this Bottomsheet using Jetpack Compose?
2024-01-05 13:25

the default ModalBottomsheet is not give me a flexibility to customize the UI to look like the UI in the screenshot enter image description here



Answer 1 :

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:

The centered button for dismissing the 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") } ) } } } } }

A visual demonstration of the custom modal bottom sheet.

Happy coding! ????????


other answer :

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