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

前言

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

将大量的交互操作事件进行归类,

通过弹框的形式,以极小的空间占用,实现大量功能。

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


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

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

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

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

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

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

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

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

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

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

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

dart 复制代码
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 事件类型
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<MenuEntry>? menuChildren;

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

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

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


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

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

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

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

5. 小结

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

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

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

相关推荐
openinstall全渠道统计10 分钟前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫32 分钟前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫1 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫2 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫2 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标2 小时前
android 快速定位当前页面
android
Zsnoin能4 小时前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人5 小时前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen5 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
雾里看山6 小时前
【MySQL】内置函数
android·数据库·mysql