前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 组件集录 | MenuAnchor 与多级菜单

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

作者头像
张风捷特烈
发布2024-04-16 08:19:42
1850
发布2024-04-16 08:19:42
举报
前言

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

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

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

104.gif
104.gif

1. MenuAnchor 组件的简单使用

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

image.png
image.png

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

image.png
image.png

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

  • builder 回调中构建展示的按钮视图,也就是上面的 文件 按钮。
  • menuChildren 是组件列表,是弹出菜单的展示内容。
代码语言:javascript
复制
@override
Widget build(BuildContext context) {
  return Center(
    child: MenuAnchor(
      builder: _buildView,
      menuChildren: _buildMenus,
    ),
  );
}

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

代码语言:javascript
复制
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
image.png
代码语言:javascript
复制
List<Widget> get _buildMenus => [
      MenuItemButton(
        child: Text('新建'),
        onPressed: () {
          print("======新建==========");
        },
      ),
      MenuItemButton(
        child: Text('打开'),
        onPressed: () {
          print("======打开==========");
        },
      ),
    ];

2. SubmenuButton 实现多级菜单

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

image.png
image.png
代码语言:javascript
复制
SubmenuButton(
  menuChildren: [
    MenuItemButton(
      child: Text('导出 PNG'),
      onPressed: () {
        print("======导出 PNG==========");
      },
    ),
    MenuItemButton(
      child: Text('导出 SVG'),
      onPressed: () {
        print("======导出 SVG==========");
      },
    ),
  ],
  child: Text("导出"),
)

3. MenuItemButton 与快捷键

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

image.png
image.png

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

image.png
image.png
代码语言:javascript
复制
MenuItemButton(
  child: Text('打开'),
  shortcut: SingleActivator(LogicalKeyboardKey.keyO, control: true),
  onPressed: () {
    print("======打开==========");
  },
),

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

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

image.png
image.png
代码语言:javascript
复制
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);
}

4. 封装按钮入口节点

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

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

  • 可以包含若干个节点,也就是说将其定义为树形结构。
  • 每个菜单节点可以指定快捷键以及 MenuAction 事件类型
image.png
image.png
代码语言:javascript
复制
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 枚举表示菜单的动作事件,便于统一由外界根据菜单的事件类型,处理回调事件:

代码语言:javascript
复制
enum MenuAction{
  newFile,
  openFile,
  importFile,
  saveFile,
  outputFilePng,
  outputFileJpg,
  outputFileSvg,
  back,
  undo,
  copy,
  past,
  clear,
}

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

image.png
image.png

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

image.png
image.png
代码语言:javascript
复制
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?> 方法处理快捷键事件:

代码语言:javascript
复制
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
image.png
代码语言:javascript
复制
void _onTapMenu(BuildContext context, MenuAction? value) async {
  /// TODO 处理菜单事件、快捷键事件
  if (value == MenuAction.importFile) {
    _handleImportImage(context);
  }
}

5. 小结

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

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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-04-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1. MenuAnchor 组件的简单使用
  • 2. SubmenuButton 实现多级菜单
  • 3. MenuItemButton 与快捷键
  • 4. 封装按钮入口节点
  • 5. 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档