主题
Material 3 弹出菜单样式规范
最后更新:2026-01-22 版本:v1.0 状态:✅ 强制执行
💡 提示:本文档是PopupMenu的详细实战指南。如需查看完整的 Material 3 设计系统规范(包括文本、图标、颜色等基础规范),请参考 Material 3 设计系统
📋 目录
设计规范
主题配置(已在 theme_config.dart 中配置)
dart
// 位置:lib/config/theme_config.dart
popupMenuTheme: PopupMenuThemeData(
color: colorScheme.surface, // 不透明,纯色背景
elevation: 1, // 柔和阴影
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // 12dp圆角
),
textStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 14,
),
),使用场景
何时使用 PopupMenu
| 场景 | 推荐 | 说明 |
|---|---|---|
| AppBar 右侧操作 | ✅ 推荐 | 放置次要操作(设置、更多选项) |
| 卡片项操作 | ✅ 推荐 | 编辑、删除、分享等操作 |
| 列表项操作 | ✅ 推荐 | 长按或点击触发的上下文菜单 |
| 底部导航操作 | ❌ 不推荐 | 使用 BottomSheet 更合适 |
| 主要操作 | ❌ 不推荐 | 应使用显式按钮 |
菜单项数量建议
- ✅ 推荐:3-5 个菜单项
- ⚠️ 可接受:6-7 个菜单项
- ❌ 避免:超过 8 个菜单项(考虑分组或使用对话框)
样式规范
| 属性 | 值 | 说明 |
|---|---|---|
| 圆角 | 12dp | 与Card、Button保持一致 |
| 阴影 | elevation: 1 | 柔和阴影,不突兀 |
| 背景色 | surface(不透明) | 暖白色(浅色模式)/暖灰色(深色模式) |
| 文字大小 | 14sp | bodyMedium |
| 文字颜色 | onSurface | 自动适配深浅色模式 |
| 菜单项高度 | 48dp (默认) | Material 3标准 |
| 水平内边距 | 12dp | 菜单项左右padding |
| 最小宽度 | 112dp | Material 3最小宽度 |
| 最大宽度 | 280dp | Material 3最大宽度 |
✅ 正确用法
1. 基础菜单(图标 + 文字)
dart
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
tooltip: 'More actions',
offset: const Offset(0, 40), // 向下偏移,避免遮挡内容
itemBuilder: (context) => [
PopupMenuItem<String>(
value: 'action1',
child: Row(
children: [
Icon(
Icons.edit_outlined,
color: Theme.of(context).colorScheme.onSurface,
size: 20,
),
const SizedBox(width: 12),
const Text('编辑'),
],
),
),
PopupMenuItem<String>(
value: 'action2',
child: Row(
children: [
Icon(
Icons.delete_outline,
color: Colors.red.shade600,
size: 20,
),
const SizedBox(width: 12),
const Text('删除'),
],
),
),
],
onSelected: (value) {
// 处理菜单选择
},
)2. 纯图标菜单(国际化设计)
dart
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
tooltip: 'Actions',
offset: const Offset(0, 40),
itemBuilder: (context) => [
// 只用图标,不用文字(适合国际化)
PopupMenuItem<String>(
value: 'undo',
enabled: currentIndex > 0, // 条件禁用
child: Icon(
Icons.undo,
color: currentIndex > 0
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
size: 24,
),
),
PopupMenuItem<String>(
value: 'star',
child: Icon(
Icons.star_outline,
color: Colors.amber.shade600,
size: 24,
),
),
],
onSelected: (value) {
// 处理菜单选择
},
)3. 禁用状态
dart
PopupMenuItem<String>(
value: 'disabled_action',
enabled: false, // 禁用
child: Row(
children: [
Icon(
Icons.lock_outline,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38), // 38%透明度
size: 20,
),
const SizedBox(width: 12),
Text(
'已锁定',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
),
),
],
),
)4. 分组菜单(带分隔线)
dart
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
// 第一组
PopupMenuItem(value: 'edit', child: Text('编辑')),
PopupMenuItem(value: 'copy', child: Text('复制')),
// 分隔线
const PopupMenuDivider(),
// 第二组(危险操作)
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete_outline, color: Colors.red.shade600, size: 20),
const SizedBox(width: 12),
Text('删除', style: TextStyle(color: Colors.red.shade600)),
],
),
),
],
)❌ 常见错误
1. 硬编码背景色
dart
// ❌ 错误:覆盖主题配置
PopupMenuButton(
color: Colors.white, // 不要硬编码!
itemBuilder: ...
)
// ✅ 正确:使用主题配置
PopupMenuButton(
// 自动使用 popupMenuTheme
itemBuilder: ...
)2. 圆角不一致
dart
// ❌ 错误:自定义圆角
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // 与主题不一致!
),
itemBuilder: ...
)
// ✅ 正确:使用主题配置
PopupMenuButton(
// 自动使用 popupMenuTheme 的圆角
itemBuilder: ...
)3. 阴影过深
dart
// ❌ 错误:阴影太深
PopupMenuButton(
elevation: 8, // 太突兀!
itemBuilder: ...
)
// ✅ 正确:使用主题配置
PopupMenuButton(
// 自动使用 popupMenuTheme 的 elevation: 1
itemBuilder: ...
)4. 菜单遮挡内容
dart
// ❌ 错误:没有偏移,遮挡锚点
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: ...
)
// ✅ 正确:向下偏移
PopupMenuButton(
icon: const Icon(Icons.more_vert),
offset: const Offset(0, 40), // 向下偏移40px
itemBuilder: ...
)5. 图标大小不一致
dart
// ❌ 错误:图标大小不一致
PopupMenuItem(
child: Row(
children: [
Icon(Icons.edit, size: 16), // 16px
Icon(Icons.delete, size: 24), // 24px - 不一致!
],
),
)
// ✅ 正确:统一大小
PopupMenuItem(
child: Row(
children: [
Icon(Icons.edit_outlined, size: 20), // 统一20px
const SizedBox(width: 12),
Icon(Icons.delete_outline, size: 20), // 统一20px
],
),
)🎨 图标规范
图标大小
| 用途 | 大小 | 使用场景 |
|---|---|---|
| 20dp | 推荐 | 菜单项图标(带文字) |
| 24dp | 推荐 | 菜单项图标(纯图标菜单) |
| 18dp | 可选 | 次要图标 |
图标样式
dart
// ✅ 推荐:使用 outlined 样式
Icons.edit_outlined
Icons.delete_outline
Icons.star_outline
// ❌ 避免:使用 filled 样式(过于沉重)
Icons.edit // 太重
Icons.delete
Icons.star图标颜色
dart
// 常规图标
Icon(
Icons.edit_outlined,
color: Theme.of(context).colorScheme.onSurface, // 使用主题色
size: 20,
)
// 强调图标(如删除)
Icon(
Icons.delete_outline,
color: Colors.red.shade600, // 可以使用语义化颜色
size: 20,
)
// 成功图标
Icon(
Icons.check_circle_outline,
color: Colors.green.shade600,
size: 20,
)
// 禁用图标
Icon(
Icons.lock_outline,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38), // 38%透明度
size: 20,
)📏 间距规范
菜单项布局
dart
PopupMenuItem(
child: Row(
children: [
Icon(..., size: 20),
const SizedBox(width: 12), // 图标与文字间距:12dp
const Text('操作'),
],
),
)菜单偏移
dart
// AppBar右侧菜单
PopupMenuButton(
offset: const Offset(0, 40), // 向下偏移40dp
...
)
// 页面内容菜单
PopupMenuButton(
offset: const Offset(0, 8), // 向下偏移8dp(轻微偏移)
...
)🔍 实际案例
案例1:Review Session 页面菜单
位置:lib/features/vocabulary_notebook/presentation/pages/review_session_page.dart
dart
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
tooltip: 'More actions',
offset: const Offset(0, 40), // 避免遮挡进度分数
itemBuilder: (context) => [
// 撤销(纯图标)
PopupMenuItem<String>(
value: 'undo',
enabled: currentIndex > 0,
child: Icon(
Icons.undo,
color: currentIndex > 0
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
size: 24,
),
),
// 标记已掌握(图标+文字)
PopupMenuItem<String>(
value: 'mark_mastered',
child: Row(
children: [
Icon(
Icons.verified_outlined,
color: Colors.green.shade600,
size: 20,
),
const SizedBox(width: 12),
const Text('标记已掌握'),
],
),
),
],
onSelected: (value) {
if (value == 'undo') {
notifier.undoLastReview();
} else if (value == 'mark_mastered') {
_cardKey.currentState?.playSlideOutAnimation(() {
notifier.markAsEasy();
});
}
},
)设计亮点:
- ✅ 使用
offset避免遮挡标题栏进度分数 - ✅ 撤销使用纯图标(国际化)
- ✅ 禁用状态用38%透明度
- ✅ 成功操作用绿色图标
📊 主题配置验证
验证清单
- [x] 圆角:12dp
- [x] 阴影:elevation 1
- [x] 背景:surface(不透明)
- [x] 文字:14sp, onSurface
- [x] 浅色模式:暖白色背景 (#FAF6F0)
- [x] 深色模式:暖灰色背景 (#2A2520)
测试方法
dart
// 创建测试菜单
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(child: Text('测试项')),
],
)检查项:
- 菜单背景是否透明(能看到下方内容)
- 圆角是否为12dp
- 阴影是否柔和(不突兀)
- 文字颜色是否清晰可读
🚀 最佳实践
1. 使用主题配置
原则:除非有特殊需求,否则不要覆盖 popupMenuTheme。
dart
// ✅ 好:使用主题
PopupMenuButton(
itemBuilder: ...
)
// ❌ 坏:覆盖主题
PopupMenuButton(
color: Colors.white,
elevation: 3,
shape: ...,
itemBuilder: ...
)2. 语义化颜色
原则:使用语义化颜色表达操作意图。
dart
// 删除/危险操作 → 红色
Icon(Icons.delete_outline, color: Colors.red.shade600)
// 成功/确认操作 → 绿色
Icon(Icons.check_circle_outline, color: Colors.green.shade600)
// 警告操作 → 橙色
Icon(Icons.warning_outline, color: Colors.orange.shade600)
// 常规操作 → 主题色
Icon(Icons.edit_outlined, color: colorScheme.onSurface)3. 国际化设计
原则:纯图标菜单适合国际化应用。
dart
// ✅ 国际化友好(纯图标)
PopupMenuItem(
value: 'undo',
child: Icon(Icons.undo, size: 24),
)
// ⚠️ 需要翻译(文字)
PopupMenuItem(
value: 'undo',
child: Row(
children: [
Icon(Icons.undo, size: 20),
const SizedBox(width: 12),
const Text('撤销'), // 需要翻译
],
),
)4. 菜单项数量
原则:控制菜单项数量,避免滚动。
- ✅ 推荐:3-5个菜单项
- ⚠️ 可接受:6-7个菜单项
- ❌ 避免:超过8个菜单项
🔗 相关规范
- Material 3 设计系统 - 颜色、文字、图标基础规范
- 按钮样式规范 - 按钮组件规范
- 对话框样式规范 - 对话框组件规范
📝 变更日志
| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-01-22 | v1.0 | 初始版本,定义PopupMenu基础规范 |
维护者:Reading Vocab Helper Team 问题反馈:请在项目中提 Issue