Bottom Sheet
Hướng dẫn sử dụng Bottom Sheet trong Compose Multiplatform.
Modal Bottom Sheet
Phần tiêu đề “Modal Bottom Sheet”@OptIn(ExperimentalMaterial3Api::class)@Composablefun ModalBottomSheetExample() { var showBottomSheet by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState()
Scaffold { padding -> Box(modifier = Modifier.padding(padding)) { Button(onClick = { showBottomSheet = true }) { Text("Show Bottom Sheet") } } }
if (showBottomSheet) { ModalBottomSheet( onDismissRequest = { showBottomSheet = false }, sheetState = sheetState ) { BottomSheetContent( onDismiss = { showBottomSheet = false } ) } }}
@Composablefun BottomSheetContent(onDismiss: () -> Unit) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text( "Bottom Sheet", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold )
Spacer(Modifier.height(16.dp))
Text("This is the content of the bottom sheet.")
Spacer(Modifier.height(24.dp))
Button( onClick = onDismiss, modifier = Modifier.fillMaxWidth() ) { Text("Close") }
Spacer(Modifier.height(32.dp)) // Padding cho navigation bar }}Bottom Sheet với List
Phần tiêu đề “Bottom Sheet với List”@OptIn(ExperimentalMaterial3Api::class)@Composablefun OptionsBottomSheet( show: Boolean, onDismiss: () -> Unit, onOptionSelected: (String) -> Unit) { if (show) { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState() ) { Column(modifier = Modifier.padding(bottom = 32.dp)) { Text( "Select an option", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(16.dp) )
listOf( "Option 1" to Icons.Default.Star, "Option 2" to Icons.Default.Favorite, "Option 3" to Icons.Default.Share, "Cancel" to Icons.Default.Close ).forEach { (option, icon) -> ListItem( headlineContent = { Text(option) }, leadingContent = { Icon(icon, contentDescription = null) }, modifier = Modifier.clickable { onOptionSelected(option) onDismiss() } ) } } } }}Share Bottom Sheet
Phần tiêu đề “Share Bottom Sheet”@OptIn(ExperimentalMaterial3Api::class)@Composablefun ShareBottomSheet( show: Boolean, onDismiss: () -> Unit, onShare: (ShareOption) -> Unit) { if (show) { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState() ) { Column(modifier = Modifier.padding(16.dp)) { Text( "Share via", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold )
Spacer(Modifier.height(16.dp))
Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { ShareOption.entries.forEach { option -> ShareOptionItem( option = option, onClick = { onShare(option) onDismiss() } ) } }
Spacer(Modifier.height(32.dp)) } } }}
enum class ShareOption(val label: String, val icon: ImageVector) { COPY("Copy", Icons.Default.ContentCopy), MESSAGE("Message", Icons.Default.Message), EMAIL("Email", Icons.Default.Email), MORE("More", Icons.Default.MoreHoriz)}
@Composablefun ShareOptionItem( option: ShareOption, onClick: () -> Unit) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .clickable(onClick = onClick) .padding(8.dp) ) { Box( modifier = Modifier .size(56.dp) .background( MaterialTheme.colorScheme.primaryContainer, CircleShape ), contentAlignment = Alignment.Center ) { Icon( option.icon, contentDescription = option.label, tint = MaterialTheme.colorScheme.onPrimaryContainer ) }
Spacer(Modifier.height(8.dp))
Text( option.label, style = MaterialTheme.typography.bodySmall ) }}Filter Bottom Sheet
Phần tiêu đề “Filter Bottom Sheet”@OptIn(ExperimentalMaterial3Api::class)@Composablefun FilterBottomSheet( show: Boolean, currentFilter: FilterState, onDismiss: () -> Unit, onApply: (FilterState) -> Unit) { if (show) { var filter by remember { mutableStateOf(currentFilter) }
ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { // Header Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( "Filters", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold )
TextButton(onClick = { filter = FilterState() }) { Text("Reset") } }
Spacer(Modifier.height(24.dp))
// Sort by Text( "Sort by", style = MaterialTheme.typography.titleSmall, color = Color.Gray )
Spacer(Modifier.height(8.dp))
FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { SortOption.entries.forEach { option -> FilterChip( selected = filter.sortBy == option, onClick = { filter = filter.copy(sortBy = option) }, label = { Text(option.label) } ) } }
Spacer(Modifier.height(24.dp))
// Price range Text( "Price range", style = MaterialTheme.typography.titleSmall, color = Color.Gray )
Spacer(Modifier.height(8.dp))
RangeSlider( value = filter.priceRange, onValueChange = { filter = filter.copy(priceRange = it) }, valueRange = 0f..1000f )
Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text("$${filter.priceRange.start.toInt()}") Text("$${filter.priceRange.endInclusive.toInt()}") }
Spacer(Modifier.height(24.dp))
// Categories Text( "Categories", style = MaterialTheme.typography.titleSmall, color = Color.Gray )
Spacer(Modifier.height(8.dp))
Category.entries.forEach { category -> Row( modifier = Modifier .fillMaxWidth() .clickable { filter = filter.copy( categories = if (filter.categories.contains(category)) { filter.categories - category } else { filter.categories + category } ) } .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox( checked = filter.categories.contains(category), onCheckedChange = null ) Spacer(Modifier.width(8.dp)) Text(category.label) } }
Spacer(Modifier.height(24.dp))
// Apply button Button( onClick = { onApply(filter) onDismiss() }, modifier = Modifier .fillMaxWidth() .height(56.dp), shape = RoundedCornerShape(12.dp) ) { Text("Apply Filters") }
Spacer(Modifier.height(32.dp)) } } }}
data class FilterState( val sortBy: SortOption = SortOption.NEWEST, val priceRange: ClosedFloatingPointRange<Float> = 0f..500f, val categories: Set<Category> = emptySet())
enum class SortOption(val label: String) { NEWEST("Newest"), PRICE_LOW("Price: Low"), PRICE_HIGH("Price: High"), POPULAR("Popular")}
enum class Category(val label: String) { ELECTRONICS("Electronics"), CLOTHING("Clothing"), BOOKS("Books"), HOME("Home & Garden")}Confirmation Bottom Sheet
Phần tiêu đề “Confirmation Bottom Sheet”@OptIn(ExperimentalMaterial3Api::class)@Composablefun ConfirmationBottomSheet( show: Boolean, title: String, message: String, confirmText: String = "Confirm", cancelText: String = "Cancel", isDestructive: Boolean = false, onConfirm: () -> Unit, onDismiss: () -> Unit) { if (show) { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState() ) { Column( modifier = Modifier .fillMaxWidth() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Icon( if (isDestructive) Icons.Default.Warning else Icons.Default.Info, contentDescription = null, modifier = Modifier.size(48.dp), tint = if (isDestructive) { MaterialTheme.colorScheme.error } else { MaterialTheme.colorScheme.primary } )
Spacer(Modifier.height(16.dp))
Text( title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold )
Spacer(Modifier.height(8.dp))
Text( message, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = Color.Gray )
Spacer(Modifier.height(24.dp))
Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { OutlinedButton( onClick = onDismiss, modifier = Modifier.weight(1f), shape = RoundedCornerShape(12.dp) ) { Text(cancelText) }
Button( onClick = { onConfirm() onDismiss() }, modifier = Modifier.weight(1f), shape = RoundedCornerShape(12.dp), colors = if (isDestructive) { ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error ) } else { ButtonDefaults.buttonColors() } ) { Text(confirmText) } }
Spacer(Modifier.height(32.dp)) } } }}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Component | Mục đích |
|---|---|
ModalBottomSheet |
Standard bottom sheet |
rememberModalBottomSheetState |
Sheet state |
skipPartiallyExpanded |
Full expansion |
Best Practices
Phần tiêu đề “Best Practices”- Padding bottom cho navigation bar
- Drag handle tự động có sẵn
- skipPartiallyExpanded cho form dài
- onDismissRequest để close khi tap outside
Tiếp theo
Phần tiêu đề “Tiếp theo”Học về Dialog.