Bottom Sheet
Hướng dẫn sử dụng Bottom Sheet trong Compose Multiplatform.
Modal Bottom Sheet
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun 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 }
)
}
}
}
@Composable
fun 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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun 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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun 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)
}
@Composable
fun 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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun 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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun 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
| Component | Mục đích |
|---|---|
ModalBottomSheet | Standard bottom sheet |
rememberModalBottomSheetState | Sheet state |
skipPartiallyExpanded | Full expansion |
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
Học về Dialog.
Last updated on