Flutter 组件集录 | MenuAnchor 与多级菜单


theme: cyanosis

前言

多级菜单在桌面端应用中非常常见,是很多应用程序中不可缺少的一环。它的价值在于:

将大量的交互操作事件进行归类,
通过弹框的形式,以极小的空间占用,实现大量功能。

那 Flutter 既然支持桌面端,那自然少不了对多级菜单的支持,菜单按钮的事件也往往伴随着快捷键的使用。本文就来介绍一下基于 MenuAnchor 组件,如何实现弹出多级菜单,以及快捷键的使用:

104.gif


1. MenuAnchor 组件的简单使用

MenuAnchor 是一个 Flutter 内置的 StatefulWidget,它可以将子组件视为 "锚点",以锚点为基础展开浮层菜单。显示显示

image.png

先通过一个最简单的案例了解一下 MenuAnchor 组件的使用。下面点击 文件 区域时,通过 MenuAnchor 在下方展示 新建打开 两个按钮:

image.png

MenuAnchor 组件最重要的是两个参数:

  • builder 回调中构建展示的按钮视图,也就是上面的 文件 按钮。
  • menuChildren 是组件列表,是弹出菜单的展示内容。

dart @override Widget build(BuildContext context) { return Center( child: MenuAnchor( builder: _buildView, menuChildren: _buildMenus, ), ); }

其中 builder 回调中可以访问 MenuController对象,可以用于打开和关闭菜单。其中返回的组件可以自定义构建,此处是一个蓝框加上文字:

dart 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 回调中打印信息。此时点击菜单条目时,菜单会隐藏,并且触发点击事件:

image.png

dart List<Widget> get _buildMenus => [ MenuItemButton( child: Text('新建'), onPressed: () { print("======新建=========="); }, ), MenuItemButton( child: Text('打开'), onPressed: () { print("======打开=========="); }, ), ];


2. SubmenuButton 实现多级菜单

在菜单条目列表中,可以通过 SubmenuButton 容纳多个子菜单项,效果如下:

image.png

dart SubmenuButton( menuChildren: [ MenuItemButton( child: Text('导出 PNG'), onPressed: () { print("======导出 PNG=========="); }, ), MenuItemButton( child: Text('导出 SVG'), onPressed: () { print("======导出 SVG=========="); }, ), ], child: Text("导出"), )


3. MenuItemButton 与快捷键

MenuItemButton 在构造函数中可以传入 shortcut 参数设置菜单项的快捷键。

image.png

如下所示,为打开菜单条目设置 Ctrl+O 快捷键,指定 SingleActivator 对象进行配置。MenuItemButton 在设置快捷键后会在右侧展示:

image.png

dart MenuItemButton( child: Text('打开'), shortcut: SingleActivator(LogicalKeyboardKey.keyO, control: true), onPressed: () { print("======打开=========="); }, ),

只是在 MenuItemButton 声明使用了该快捷键,并不能使快捷键生效。需要在通过 ShortcutRegistry 来注册快捷键和事件的映射关系。如下所示,在状态类的 didChangeDependencies 回调中调用 _shortcutRegistry 进行注册:

其中 key 值是 SingleActivator 对象,也就是快捷键的信息描述,值是 Intent 表示触发的事件,这里设置为 VoidCallbackIntent 表示无参数的回调事件。此时只要按下 Ctrl+O 就可以触发其中的回调:

image.png

```dart ShortcutRegistryEntry? _shortcutsEntry; @override void didChangeDependencies() { super.didChangeDependencies(); _shortcutRegistry(); }

void _shortcutRegistry() { _shortcutsEntry?.dispose(); final Map shortcuts = {}; shortcuts[SingleActivator(LogicalKeyboardKey.keyO, control: true)] = VoidCallbackIntent((){ print("打开事件---快捷键"); }); _shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts); } ```


4. 封装按钮入口节点

如果按照普通的方式来写堆砌菜单按钮,那么随着菜单增加,代码将会非常复杂。并且每个按钮处理自己的事件,非常零散。而且注册快捷键的代码和按钮的回调相对割裂。

pix_editor 项目中,将每个菜单项封装为 MenuEntry 对象,其中

  • 可以包含若干个节点,也就是说将其定义为树形结构。
  • 每个菜单节点可以指定快捷键以及 MenuAction 事件类型

image.png

```dart 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 ? menuChildren; ```


MenuAction 枚举表示菜单的动作事件,便于统一由外界根据菜单的事件类型,处理回调事件:

dart enum MenuAction{ newFile, openFile, importFile, saveFile, outputFilePng, outputFileJpg, outputFileSvg, back, undo, copy, past, clear, }

菜单栏封装为 AppToolMenuBar,将菜单的点击事件回调给外界:

image.png


如下所示在代码中,菜单树的数据将通过 MenuEntry 列表来维护,只要在其中配置菜单按钮的信息即可。 接下来,定义 buildByMenuEntryList 方法,解析 MenuEntry 列表,构建对应的菜单项;其中传入 ValueChanged<MenuAction?> 方法除了按钮点击事件:

image.png

dart 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?> 方法处理快捷键事件:

dart 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; }

这样就能完成快捷键事件和按钮点击事件的统一处理:

image.png

dart void _onTapMenu(BuildContext context, MenuAction? value) async { /// TODO 处理菜单事件、快捷键事件 if (value == MenuAction.importFile) { _handleImportImage(context); } }


5. 小结

总的来看,MenuAnchor 组件是一个很强大的组件,它可以让以任意组件为锚点,弹出菜单栏。并且子组件和菜单组件都有非常大的定制空间,灵活性非常高。另外 MenuAnchor 还有其他属性:

  • 默认情况下,菜单栏将锚点组件的左下角对齐,可以通过 alignmentOffset 设置偏移量。
  • onOpenonClose 方法可以监听打开和关闭浮层的事件:

image.png

如果不喜欢 Flutter 提供的 MenuItemButton 样式,可以通过主题的 menuButtonTheme 进行修改。甚至是自己定义组件来实现 MenuItemButton 功能。 那本文就到这里,谢谢观看 ~

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值