Flutter3.7版本新增元件-Menu菜單系列介紹

語言: CN / TW / HK

theme: juejin

之前Flutter的選單選擇、下拉選單的支援非常簡單且不友好,對於非常常見的下拉選單選擇功能是需要自己自定義實現,今天看到Flutter3.7版本新增了一系列選單的元件,馬上來試試。

選單元件介紹

本次Flutter穩定版本菜單系列元件新增了 MenuAnchorMenuBarSubmenuButtonMenuItemButton 元件, 這四個元件可以單獨使用也可以相互配合使用。他們都位於menu_anchor.dart檔案內,下面對這幾個元件詳細介紹下。

MenuAnchor元件

這是一個具有子選單的獨立區域元件,點進去我們可以看到是一個StatefulWidget元件, 這四個元件除了MenuBar是靜態元件,其他都是動態元件。

image.png

說明我們可以當作普通的Widget元件去使用它們,MenuAnchor可以獨立使用,通過這一個元件可以簡單的實現下拉選單的功能。
建構函式:
dart const MenuAnchor({ super.key, this.controller,// 控制器 this.childFocusNode,//如果選單是輸入框,焦點控制 this.style, //選單樣式 this.alignmentOffset = Offset.zero,//相對於元件左下角位置 this.clipBehavior = Clip.none,// 超出螢幕剪下 不常用 this.anchorTapClosesMenu = false,// 設定為true時,選單開啟時,點選會重複開啟。 this.onOpen,//打開回調 this.onClose,//關閉回撥 this.crossAxisUnconstrained = true, required this.menuChildren,//下拉選單列表 this.builder,//元件本身,通常是控制選單的按鈕 this.child,//傳遞給上方builder裡的child元件 }); 官方示例:

Jan-30-2023 16-18-55.gif 官方示例選單後面的字母是自定義快捷鍵的操作,我們重點看下選單的聯動功能,選單聯動是和SubmenuButton實現的,例如官方示例中的設定背景色的選單就是使用它實現的。接下來介紹下這個元件。

SubmenuButton 聯級選單按鈕

通過這個按鈕可以實現選單的聯級呼叫,一般用來該選項下還有下級選單時使用。該元件一般和MenuAnchorMenuBar配合使用。

dart const SubmenuButton({ super.key, this.onHover,//按鈕是否選中回撥 在pc端屬於滑鼠指標在此選單上 this.onFocusChange,//是否獲取焦點回調 this.onOpen,//開啟下級選單回撥 this.onClose,//關閉下級選單回撥 this.style,//按鈕本身樣式 this.menuStyle,//下級選單樣式 this.alignmentOffset,//相對位置偏移量 預設和元件上邊對齊 this.clipBehavior = Clip.none, this.focusNode, this.statesController,//元件狀態擴充套件 this.leadingIcon,//左邊可選圖示 this.trailingIcon,//右邊可選圖示 required this.menuChildren,//聯級選單 required this.child,//元件本身 });

MenuItemButton 選單按鈕元件

具體選單的選項,一般選單選項沒有下一級選單時具有具體的功能使用,通過構造方法可以自定義快捷鍵,快捷鍵功能一般在PC端上使用。

構造方法:

dart const MenuItemButton({ super.key, this.onPressed,//點選事件 this.onHover,//選中回撥 this.requestFocusOnHover = true,//指標懸停是否聚焦 this.onFocusChange,//是否獲取焦點回調 this.focusNode,//焦點控制 this.shortcut,//快捷鍵設定 this.style,//本身樣式 this.statesController,//元件狀態擴充套件 this.clipBehavior = Clip.none, this.leadingIcon,//... this.trailingIcon,//... required this.child,//... });

MenuBar 多選單聯級選單頭部Bar

此元件是管理多個聯級選單頭部的元件,例如掘金編輯器下圖,如果選單選項只有1個可以使用MenuAnchor,多個時使用MenuBar.

image.png

紅框內的元件集就是MenuBar元件的作用,它可以管理各個選單之間的聯動,預設他們共用一個控制器。一般和SubmenuButtonMenuItemButton配合使用。
dart const MenuBar({ super.key, this.style,// 選單樣式 this.clipBehavior = Clip.none, this.controller, required this.children, }); 示例效果:

左邊的選單1、2、3是一組MenuBar元件,右邊是可以獨立的MenuAnchor元件。

Jan-31-2023 14-13-17.gif

示例原始碼:
相較於官方示例,該示例下方展示了上方四個選單元件的單獨使用以及聯合使用的簡單示例,去掉了快捷鍵設定的屬性,更直觀的瞭解選單元件的使用。快捷鍵的使用一般在PC端使用。

```dart import 'package:flutter/material.dart'; void main() => runApp(const MenuApp()); enum MenuEntry { about('About'), showMessage('Show Message'), hideMessage('Hide Message'), colorMenu('Color Menu'), colorRed('Red Background'), colorGreen('Green Background'), colorBlue('Blue Background');

final String label; const MenuEntry(this.label); }

class MyCascadingMenu extends StatefulWidget { const MyCascadingMenu({super.key, required this.message});

final String message;

@override State createState() => _MyCascadingMenuState(); }

class _MyCascadingMenuState extends State { MenuEntry? _lastSelection; final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');

Color get backgroundColor => _backgroundColor; Color _backgroundColor = Colors.red; set backgroundColor(Color value) { if (_backgroundColor != value) { setState(() { _backgroundColor = value; }); } }

bool get showingMessage => _showingMessage; bool _showingMessage = false; set showingMessage(bool value) { if (_showingMessage != value) { setState(() { _showingMessage = value; }); } }

@override void dispose() { _buttonFocusNode.dispose(); super.dispose(); }

@override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MenuBar( style: MenuStyle( backgroundColor: MaterialStateColor.resolveWith((states) => Colors.white),), children: [ SubmenuButton(menuChildren: _meunList(), child: const Text("選單1")), SubmenuButton(menuChildren: _meunList(), child: const Text("選單2")), SubmenuButton(menuChildren: _meunList(), child: const Text("選單3")), MenuAnchor( childFocusNode: _buttonFocusNode, menuChildren: _meunList(), builder: (BuildContext context, MenuController controller, Widget? child) { return TextButton( focusNode: _buttonFocusNode, onPressed: () { if (controller.isOpen) { controller.close(); } else { controller.open(); } }, child: const Text('OPEN MENU'), ); }, ), ]),

    Expanded(
      child: Container(
        alignment: Alignment.center,
        color: backgroundColor,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: Text(
                showingMessage ? widget.message : '',
                style: Theme.of(context).textTheme.headlineSmall,
              ),
            ),
            Text(_lastSelection != null
                ? 'Last Selected: ${_lastSelection!.label}'
                : ''),
          ],
        ),
      ),
    ),
  ],
);

}

void _activate(MenuEntry selection) { setState(() { _lastSelection = selection; });

switch (selection) {
  case MenuEntry.about:
    showAboutDialog(
      context: context,
      applicationName: 'MenuBar Sample',
      applicationVersion: '1.0.0',
    );
    break;
  case MenuEntry.hideMessage:
  case MenuEntry.showMessage:
    showingMessage = !showingMessage;
    break;
  case MenuEntry.colorMenu:
    break;
  case MenuEntry.colorRed:
    backgroundColor = Colors.red;
    break;
  case MenuEntry.colorGreen:
    backgroundColor = Colors.green;
    break;
  case MenuEntry.colorBlue:
    backgroundColor = Colors.blue;
    break;
}

}

List _meunList() { return [ MenuItemButton( child: Text(MenuEntry.about.label), onPressed: () => _activate(MenuEntry.about), ), if (_showingMessage) MenuItemButton( onPressed: () => _activate(MenuEntry.hideMessage), child: Text(MenuEntry.hideMessage.label), ), if (!_showingMessage) MenuItemButton( onPressed: () => _activate(MenuEntry.showMessage), child: Text(MenuEntry.showMessage.label), ), SubmenuButton( leadingIcon: const Icon(Icons.ac_unit_sharp), menuChildren: [ MenuItemButton( onPressed: () => _activate(MenuEntry.colorRed), child: Text(MenuEntry.colorRed.label), ), MenuItemButton( onPressed: () => _activate(MenuEntry.colorGreen), child: Text(MenuEntry.colorGreen.label), ), MenuItemButton( onPressed: () => _activate(MenuEntry.colorBlue), child: Text(MenuEntry.colorBlue.label), ), ], child: const Text('Background Color'), ), ]; } }

class MenuApp extends StatelessWidget { const MenuApp({super.key});

static const String kMessage = '"Talk less. Smile more." - A. Burr';

@override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold(body: MyCascadingMenu(message: kMessage)), ); } } ```

選單樣式 MenuStyle

構造方法:
構造方法內大多數引數使用的是 MaterialStateProperty<T>具有狀態選擇設定,這樣做的好處是在PC端例如懸停、點選、不可點選等狀態設定不同樣式時,會非常的方便。例如系統自帶的顏色、邊框MaterialStateColorMaterialStateBorderSide等都是通過 MaterialStateProperty擴充套件的。 dart const MenuStyle({ this.backgroundColor, this.shadowColor, this.surfaceTintColor, this.elevation, this.padding, this.minimumSize, this.fixedSize, this.maximumSize, this.side, this.shape, this.mouseCursor, this.visualDensity, this.alignment, });

原生系統菜單系列元件

使用平臺原生選單元件實現,非Flutter渲染,例如在MacOS系統上特別有用,因為在MacOS上需要一個系統級選單。 - PlatformMenuBar - PlatformMenu - PlatformMenuItem - PlatformMenuItemGroup
...

使用方法大同小異,區別就是這是基於不同平臺實現的系統選單選項。

小結

上面就是本次更新新增的選單相關使用的元件,可以看出這一系列元件更傾向於桌面端使用,裡面加入了實現快捷鍵的操作,反而對於移動端操作需要的選單以外部分的陰影,選單彈出動畫都沒有找到支援的方法。