底部弹窗是移动端最自然的"详情+操作"模式。本文详解 E-Brufen 中 ModalBottomSheet 的完整实现。
一、为什么是 BottomSheet 而非新页面?
| 方案 | 优点 | 缺点 |
|---|---|---|
| Navigator.push | 完整页面,可承载复杂内容 | 打断用户浏览流 |
| AlertDialog | 简单直接 | 缺乏操作空间 |
| ModalBottomSheet | 不打断浏览,手势关闭自然 | 不适合超长内容 |
对于"查看情绪详情"这个场景:
- 内容量小(emoji + 标签 + 日期 + 备注)
- 操作简单(编辑 / 删除)
- 用户希望保持时间线的浏览上下文
→ BottomSheet 是最佳选择。
二、触发方式
dart
// diary_page.dart --- 时间线列表项
ListTile(
leading: Text(mood.moodType.emoji, style: TextStyle(fontSize: 32)),
title: Text(mood.moodType.label),
subtitle: mood.note?.isNotEmpty == true
? Text(mood.note!, maxLines: 1, overflow: TextOverflow.ellipsis)
: null,
trailing: Text(_formatDate(mood.createdAt)),
onTap: () => _showMoodDetail(mood), // ← 点击触发
)
三、BottomSheet 构建

dart
void _showMoodDetail(MoodEntry mood) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${mood.moodType.emoji} ${mood.moodType.label}',
style: const TextStyle(fontSize: 24)),
const SizedBox(height: 8),
Text(_formatDate(mood.createdAt),
style: const TextStyle(color: Colors.grey)),
// 备注(如果有)
if (mood.note != null && mood.note!.isNotEmpty) ...[
const SizedBox(height: 12),
Text(mood.note!),
],
const SizedBox(height: 16),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () {
Navigator.pop(context); // 先关闭 BottomSheet
_editMood(mood); // 再进入编辑
},
icon: const Icon(Icons.edit),
label: const Text('编辑'),
),
TextButton.icon(
onPressed: () {
Navigator.pop(context);
if (mood.id != null) _deleteMood(mood.id!);
},
icon: const Icon(Icons.delete, color: Colors.red),
label: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
],
),
),
);
}
四、设计要点
4.1 圆角顶部
dart
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
)
只给顶部圆角------这是 Material Design BottomSheet 的视觉惯例。
4.2 Column 的最小高度
dart
mainAxisSize: MainAxisSize.min
确保 BottomSheet 的高度自适应内容,不留多余空白。
4.3 操作顺序
[取消] ... [编辑] [删除]
编辑在左、删除在右------但通过 MainAxisAlignment.end 将所有操作按钮推到右侧。删除用红色作为危险操作的视觉警告。
4.4 编辑前先关闭 BottomSheet
dart
onPressed: () {
Navigator.pop(context); // 关闭 BottomSheet
_editMood(mood); // 然后切换到编辑模式
},
如果先调用 _editMood(会切换 Tab),BottomSheet 会在错误的 Tab 上显示,视觉上会很怪异。
五、删除确认
dart
void _deleteMood(int id) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('删除确认'),
content: const Text('确定删除这条记录吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
widget.moodStorage.delete(id);
},
child: const Text('删除'),
),
],
),
);
}
删除需要二次确认------这是不可逆操作的标准 UX 模式。
★ Insight ─────────────────────────────────────交互流程是:时间线列表项 → BottomSheet(详情+操作) → AlertDialog(删除确认)。这个"渐进式揭示"(Progressive Disclosure)模式将复杂度按层次暴露------用户先看到概览,需要时才深入操作,最终只有删除才看到确认弹窗。
─────────────────────────────────────────────────
六、与 iOS 风格对比
| 特性 | Material (当前) | iOS (Cupertino) |
|---|---|---|
| 底部弹窗 | showModalBottomSheet | showCupertinoModalPopup |
| 圆角 | RoundedRectangleBorder | 自带圆角 |
| 操作按钮 | TextButton | CupertinoActionSheet |
当前使用 Material 风格是为了与应用的其他 Material 3 组件保持一致。
小结
ModalBottomSheet 是移动端"不打断浏览"的经典交互模式。E-Brufen 的实现覆盖了完整的使用流程:展示详情、提供操作、确认危险操作、跨 Tab 联动编辑。约 40 行代码完成了一个自然流畅的交互闭环。
作者简介 :E-Brufen Dev,Flutter & 鸿蒙开发者,专注于跨平台移动应用开发与心理健康数字化,项目地址:AtomGit - E-Brufen。