16、对话框与弹窗:AlertDialog、Dialog、BottomSheet、Popup、DropdownMenu
对话框和弹窗,说白了就是App里那些「突然冒出来」的UI元素。它们用来打断用户当前操作,让用户做选择、填信息,或者单纯展示一条消息。
我在项目中见过不少新手把对话框用成「灾难现场」——弹窗叠弹窗,用户点都点不完。嗯,今天我们就来把这些组件捋清楚,看看它们各自该在什么场景下登场。
核心原则:对话框是「打断」,不是「骚扰」。能用轻量级弹窗(如Toast、Snackbar)解决的问题,别上AlertDialog。
16.1 AlertDialog:最经典的确认框
AlertDialog是Android里最老牌的对话框。在Jetpack Compose里,它的用法比传统View时代清爽太多了。
@Composable
fun MyAlertDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("确认删除?") },
text = { Text("删除后数据无法恢复,确定要继续吗?") },
confirmButton = {
TextButton(onClick = onConfirm) {
Text("确认")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("取消")
}
}
)
}
这里有个细节:onDismissRequest 是用户点击对话框外部或按返回键时触发的。我习惯把「取消」逻辑也放这里,但注意别和 dismissButton 重复触发。
我的习惯:如果对话框只有「确认」按钮(比如展示一条重要通知),我会把 dismissButton 设为 null,但保留 onDismissRequest 让用户能关闭。
16.2 Dialog:更自由的容器
AlertDialog的布局是固定的——标题、内容、按钮。如果你想要自定义布局,比如放一个表单、一张图片,那就用 Dialog。
@Composable
fun CustomDialog(show: Boolean, onDismiss: () -> Unit) {
if (show) {
Dialog(onDismissRequest = onDismiss) {
Surface(
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surface
) {
Column(modifier = Modifier.padding(24.dp)) {
Text("请输入昵称", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(value = "", onValueChange = {})
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onDismiss) {
Text("保存")
}
}
}
}
}
}
注意:Dialog本身没有背景,你需要用 Surface 或 Card 包裹内容。我曾经踩过坑——忘了加Surface,结果对话框背景是透明的,文字直接浮在底层UI上,丑得没法看。
16.3 BottomSheet:从底部滑出的面板
BottomSheet适合展示「辅助性」内容,比如分享菜单、筛选选项。它不会完全遮挡主界面,用户能感知到上下文。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyBottomSheet() {
val sheetState = rememberModalBottomSheetState()
var showSheet by remember { mutableStateOf(false) }
Column {
Button(onClick = { showSheet = true }) {
Text("打开底部面板")
}
}
if (showSheet) {
ModalBottomSheet(
onDismissRequest = { showSheet = false },
sheetState = sheetState
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("分享到...", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Text("微信")
Text("朋友圈")
Text("QQ")
}
}
}
}
这里有个坑:ModalBottomSheet 默认会有一个拖拽手柄,但如果你内容太少,用户拖不动。我建议内容高度至少占屏幕的30%,否则用Popup更合适。
16.4 Popup:轻量级浮层
Popup是最轻量的弹窗——它没有遮罩层,不会阻断用户操作。适合做工具提示、小菜单。
@Composable
fun MyPopup() {
var showPopup by remember { mutableStateOf(false) }
Box {
Button(onClick = { showPopup = true }) {
Text("更多操作")
}
DropdownMenu(
expanded = showPopup,
onDismissRequest = { showPopup = false }
) {
DropdownMenuItem(
text = { Text("编辑") },
onClick = { showPopup = false }
)
DropdownMenuItem(
text = { Text("删除") },
onClick = { showPopup = false }
)
}
}
}
注意:Popup的位置是相对于它的父容器。如果你把Button放在一个窄的Column里,Popup可能会被裁剪。我一般用 Box 包裹,并给Box设置 clipToPadding = false。
16.5 DropdownMenu:下拉菜单
DropdownMenu本质上是Popup的一种特殊形式,专门用来做菜单列表。它和 ExposedDropdownMenuBox 配合,可以实现类似「选择器」的效果。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyDropdownMenu() {
val options = listOf("选项A", "选项B", "选项C")
var expanded by remember { mutableStateOf(false) }
var selectedText by remember { mutableStateOf(options[0]) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
value = selectedText,
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { option ->
DropdownMenuItem(
text = { Text(option) },
onClick = {
selectedText = option
expanded = false
}
)
}
}
}
}
这里有个关键点:Modifier.menuAnchor() 必须加在TextField上,否则菜单不知道锚定在哪里。我见过有人漏掉这个,结果菜单飘到屏幕左上角去了。
16.6 如何选择?一张图说清楚
16.7 避坑指南
我曾经踩过的坑:
- 弹窗叠弹窗:在AlertDialog的确认按钮里又弹一个AlertDialog,用户得关两次。解决方案:用状态机管理弹窗层级,同一时间只显示一个。
- BottomSheet内容被键盘遮挡:如果BottomSheet里有输入框,键盘弹起时内容可能被挡住。记得给BottomSheet加
imePadding()。 - DropdownMenu位置偏移:忘了加
menuAnchor(),菜单跑到屏幕外面。检查锚点绑定。 - Dialog背景透明:没包Surface,文字直接浮在底层UI上。记住Dialog本身没有背景。
16.8 总结
对话框和弹窗是UI交互中「打断」与「辅助」的平衡艺术。我个人习惯遵循一个原则:
- 需要用户做决定 → AlertDialog
- 需要自定义布局 → Dialog
- 需要展示较多选项 → BottomSheet
- 需要轻量浮层 → Popup
- 需要列表选择 → DropdownMenu
你想想看,如果每个弹窗都上AlertDialog,用户得多烦?选对组件,体验能提升一大截。