多级菜单在桌面端应用中非常常见,是很多应用程序中不可缺少的一环。它的价值在于:
将大量的交互操作事件进行归类, 通过弹框的形式,以极小的空间占用,实现大量功能。
那 Flutter 既然支持桌面端,那自然少不了对多级菜单的支持,菜单按钮的事件也往往伴随着快捷键的使用。本文就来介绍一下基于 MenuAnchor
组件,如何实现弹出多级菜单,以及快捷键的使用:
MenuAnchor 是一个 Flutter 内置的 StatefulWidget,它可以将子组件视为 "锚点"
,以锚点为基础展开浮层菜单。显示显示
先通过一个最简单的案例了解一下 MenuAnchor
组件的使用。下面点击 文件
区域时,通过 MenuAnchor
在下方展示 新建 和 打开 两个按钮:
MenuAnchor 组件最重要的是两个参数:
builder
回调中构建展示的按钮视图,也就是上面的 文件
按钮。menuChildren
是组件列表,是弹出菜单的展示内容。@override
Widget build(BuildContext context) {
return Center(
child: MenuAnchor(
builder: _buildView,
menuChildren: _buildMenus,
),
);
}
其中 builder 回调中可以访问 MenuController
对象,可以用于打开和关闭菜单。其中返回的组件可以自定义构建,此处是一个蓝框加上文字:
Widget _buildView(_, MenuController controller, Widget? child) {
return GestureDetector(
onTap: controller.open,
child: ColoredBox(
color: Colors.blue,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Text(
"文件",
style: TextStyle(color: Colors.white),
),
),
));
}
展开的菜单面板可以是任何组件列表,Flutter 中提供了 MenuItemButton
组件,便于构建菜单按钮。这里展示了新建 和 打开 两个按钮,并在对应的 onPressed
回调中打印信息。此时点击菜单条目时,菜单会隐藏,并且触发点击事件:
List<Widget> get _buildMenus => [
MenuItemButton(
child: Text('新建'),
onPressed: () {
print("======新建==========");
},
),
MenuItemButton(
child: Text('打开'),
onPressed: () {
print("======打开==========");
},
),
];
在菜单条目列表中,可以通过 SubmenuButton
容纳多个子菜单项,效果如下:
SubmenuButton(
menuChildren: [
MenuItemButton(
child: Text('导出 PNG'),
onPressed: () {
print("======导出 PNG==========");
},
),
MenuItemButton(
child: Text('导出 SVG'),
onPressed: () {
print("======导出 SVG==========");
},
),
],
child: Text("导出"),
)
MenuItemButton 在构造函数中可以传入 shortcut
参数设置菜单项的快捷键。
如下所示,为打开菜单条目设置 Ctrl+O
快捷键,指定 SingleActivator
对象进行配置。MenuItemButton 在设置快捷键后会在右侧展示:
MenuItemButton(
child: Text('打开'),
shortcut: SingleActivator(LogicalKeyboardKey.keyO, control: true),
onPressed: () {
print("======打开==========");
},
),
只是在 MenuItemButton
声明使用了该快捷键,并不能使快捷键生效。需要在通过 ShortcutRegistry
来注册快捷键和事件的映射关系。如下所示,在状态类的 didChangeDependencies
回调中调用 _shortcutRegistry
进行注册:
其中 key 值是 SingleActivator
对象,也就是快捷键的信息描述,值是 Intent 表示触发的事件,这里设置为 VoidCallbackIntent 表示无参数的回调事件。此时只要按下 Ctrl+O
就可以触发其中的回调:
ShortcutRegistryEntry? _shortcutsEntry;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_shortcutRegistry();
}
void _shortcutRegistry() {
_shortcutsEntry?.dispose();
final Map<ShortcutActivator, Intent> shortcuts = {};
shortcuts[SingleActivator(LogicalKeyboardKey.keyO, control: true)] = VoidCallbackIntent((){
print("打开事件---快捷键");
});
_shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts);
}
如果按照普通的方式来写堆砌菜单按钮,那么随着菜单增加,代码将会非常复杂。并且每个按钮处理自己的事件,非常零散。而且注册快捷键的代码和按钮的回调相对割裂。
在 pix_editor
项目中,将每个菜单项封装为 MenuEntry
对象,其中
MenuAction
事件类型class MenuEntry {
const MenuEntry({
required this.label,
this.action,
this.tail,
this.shortcut,
this.menuChildren,
});
final String label;
final String? tail;
final MenuAction? action;
final MenuSerializableShortcut? shortcut;
final List<MenuEntry>? menuChildren;
MenuAction 枚举表示菜单的动作事件,便于统一由外界根据菜单的事件类型,处理回调事件:
enum MenuAction{
newFile,
openFile,
importFile,
saveFile,
outputFilePng,
outputFileJpg,
outputFileSvg,
back,
undo,
copy,
past,
clear,
}
菜单栏封装为 AppToolMenuBar
,将菜单的点击事件回调给外界:
如下所示在代码中,菜单树的数据将通过 MenuEntry
列表来维护,只要在其中配置菜单按钮的信息即可。 接下来,定义 buildByMenuEntryList
方法,解析 MenuEntry 列表,构建对应的菜单项;其中传入 ValueChanged<MenuAction?>
方法除了按钮点击事件:
List<Widget> buildByMenuEntryList(List<MenuEntry> selections, ValueChanged<MenuAction?> onTapMenu) {
Widget buildSelection(MenuEntry selection) {
Widget child = Text(selection.label);
if (selection.tail != null) {
child = Row(
children: [
child,
const SizedBox(width: 20),
Text(
selection.tail!,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
);
}
if (selection.menuChildren != null) {
return SubmenuButton(
menuChildren: MenuEntry.build(selection.menuChildren!, onTapMenu),
child: child,
);
}
return MenuItemButton(
shortcut: selection.shortcut,
onPressed: () => onTapMenu(selection.action),
child: child,
);
}
return selections.map<Widget>(buildSelection).toList();
}
对于快捷键来说,也可以根据 MenuEntry
列表数据,解析生成快捷键和事件的映射关系。其中传入 ValueChanged<MenuAction?>
方法处理快捷键事件:
Map<MenuSerializableShortcut, Intent> shortcutsByMenuEntryList(
List<MenuEntry> selections, ValueChanged<MenuAction?> onTap) {
final Map<MenuSerializableShortcut, Intent> result =
<MenuSerializableShortcut, Intent>{};
for (final MenuEntry selection in selections) {
if (selection.menuChildren != null) {
result.addAll(shortcutsByMenuEntryList(selection.menuChildren!, onTap));
} else {
if (selection.shortcut != null) {
result[selection.shortcut!] =
VoidCallbackIntent(() => onTap(selection.action));
}
}
}
return result;
}
这样就能完成快捷键事件和按钮点击事件的统一处理:
void _onTapMenu(BuildContext context, MenuAction? value) async {
/// TODO 处理菜单事件、快捷键事件
if (value == MenuAction.importFile) {
_handleImportImage(context);
}
}
总的来看,MenuAnchor
组件是一个很强大的组件,它可以让以任意组件为锚点,弹出菜单栏。并且子组件和菜单组件都有非常大的定制空间,灵活性非常高。另外 MenuAnchor 还有其他属性:
alignmentOffset
设置偏移量。onOpen
和 onClose
方法可以监听打开和关闭浮层的事件:如果不喜欢 Flutter 提供的 MenuItemButton
样式,可以通过主题的 menuButtonTheme
进行修改。甚至是自己定义组件来实现 MenuItemButton
功能。 那本文就到这里,谢谢观看 ~