Bỏ qua để đến nội dung

Bottom Sheet

Hướng dẫn sử dụng Bottom Sheet trong Compose Multiplatform.

@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
}
}

@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()
}
)
}
}
}
}
}

@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
)
}
}

@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")
}

@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))
}
}
}
}

Component Mục đích
ModalBottomSheet Standard bottom sheet
rememberModalBottomSheetState Sheet state
skipPartiallyExpanded Full expansion
  1. Padding bottom cho navigation bar
  2. Drag handle tự động có sẵn
  3. skipPartiallyExpanded cho form dài
  4. onDismissRequest để close khi tap outside

Học về Dialog.