Flutter 组件集录 | 下拉菜单 DropdownMenu 组件


theme: cyanosis

output.png

1. 前言

Flutter 框架中新增了 DropdownMenu 下拉按钮,可以让我们更方便地实现下拉选择的交互。本文案例源码可以详见 【FlutterUnit 的 DropdownMenu】

38.gif


2. DropdownMenu 基础使用

首先通过一个最简单的案例体验一下 DropdownMenu 的使用,如下所示: - 点击使会下拉展示菜单选项,选择科目 ; - 点击时选中科目,下方的文本相应变化; - 支持输入定位到指定的菜单条目;

37.gif

实现的代码如下,DropdownMenu 组件支持一个泛型,案例中使用了下面几个配置参数:

| 参数名 | 类型 | 介绍 | --- | --- |--- | | dropdownMenuEntries | List<DropdownMenuEntry<T>> | 必须传入,菜单条目列表 | initialSelection | T? | 初始选项值 | onSelected | ValueChanged<T?>? | 选中条目回调事件 | menuHeight | double | 菜单高度 | width | double | 输入框宽度

```dart class DropdownMenuNode1 extends StatefulWidget { const DropdownMenuNode1({super.key});

@override State

createState() => _DropdownMenuNode1State(); }

class _DropdownMenuNode1State extends State

{ final List

data = ['语文', '数学', '英语', '物理', '化学', '生物', '地理']; late String _dropdownValue = data.first;

@override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ DropdownMenu

( menuHeight: 200, initialSelection: data.first, onSelected:
onSelect, dropdownMenuEntries: _buildMenuList(data), ), const SizedBox(height: 8,), Text('你选择的学科是: $dropdownValue') ], ); }

void _onSelect(String? value) { setState(() { _dropdownValue = value!; }); }

List

> _buildMenuList(List

data) { return data.map((String value) { return DropdownMenuEntry

(value: value, label: value); }).toList(); } } ```


3. DropdownMenu 样式配置

DropdownMenu 本质上是由 TextField + MenuAnchor 实现的,所以样式配置上面主要和这两个组件有关。

| 参数名 | 类型 | 介绍 | --- | --- |--- | | controller | TextEditingController? | 文字输入控制器 | label | Widget? | 输入框标签 | textStyle | TextStyle? | 输入框文字样式 | inputDecorationTheme | InputDecorationTheme? | 输入框装饰主题 | leadingIcon | Widget? | 左侧图标 | trailingIcon | Widget? | 右侧为展开菜单时图标 | selectedTrailingIcon | Widget? | 右侧展开菜单时图标 | hintText | String? | 输入框提示文字 | helperText | String? | 输入框辅助文字 | errorText | String? | 输入框错误文字 | menuStyle | MenuStyle? | 弹出菜单样式

image.png

下面是右侧选择图标的 DropdownMenu 组件构建逻辑,其中

  • requestFocusOnTap: 点击时是否获取焦点,置为 true 在移动端上会弹出软键盘,桌面端无法输入。
  • enableFilter: 弹出菜单项是否以当前内容搜索,如果为 true, 会因为过滤使得菜单响应减少。

dart Widget _buildLabelMenu() { return DropdownMenu<IconLabel>( controller: iconController, enableFilter: false, requestFocusOnTap: true, leadingIcon: const Icon(Icons.search), label: const Text('Icon'), inputDecorationTheme: const InputDecorationTheme( filled: true, contentPadding: EdgeInsets.symmetric(vertical: 5.0), ), onSelected: (IconLabel? icon) { setState(() { selectedIcon = icon; }); }, dropdownMenuEntries: IconLabel.values.map((IconLabel icon) { return DropdownMenuEntry<IconLabel>( value: icon, label: icon.label, leadingIcon: Icon(icon.icon), ); }, ).toList(), ); }


下面是左侧选择颜色的 DropdownMenu 组件构建逻辑,其中

  • menuStyle 可以调节菜单面板的样式,比如背景色、边距、最大最小尺寸、形状等。
  • dropdownMenuEntries 中可以通过 DropdownMenuEntry 的 enable 参数设置是否禁用菜单项。

image.png

```dart Widget _buildColorMenu(){ return DropdownMenu

( initialSelection: ColorLabel.green, controller: colorController, requestFocusOnTap: true, label: const Text('Color'), menuHeight: 150, menuStyle: const MenuStyle( backgroundColor: MaterialStatePropertyAll

(Colors.white), surfaceTintColor: MaterialStatePropertyAll

(Colors.white), padding: MaterialStatePropertyAll

(EdgeInsets.symmetric(vertical: 20)), ), onSelected: (ColorLabel? color) { setState(() { selectedColor = color; }); }, dropdownMenuEntries: ColorLabel.values.map((ColorLabel color) { return DropdownMenuEntry

( value: color, label: color.label, enabled: color.label != 'Grey', style: MenuItemButton.styleFrom( foregroundColor: color.color, ), ); } ).toList(), ); }

```

另外,如果 DropdownMenu 的菜单条目比较复杂,想要定制展示内容,可以通过 DropdownMenuEntry 的 labelWidget 构建,如下所示,根据 User 对象构建菜单条目。

image.png

38.gif

```dart class _UserItem extends StatelessWidget { final User user;

const _UserItem({ Key? key, required this.user, }) : super(key: key);

@override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ CircleAvatar( foregroundColor: Colors.transparent, backgroundImage: AssetImage('assets/images/head_icon/${user.image}'), ), const SizedBox(width: 20), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(user.name), Text( '性别: ${user.man ? '男' : '女'}', style: const TextStyle(color: Colors.grey), ), ], ), ], ), ); } } ```

到这里,DropdownMenu 组件使用相关的属性就介绍的差不多了。下面来简单瞄一眼 DropdownMenu 的代码实现。


4. DropdownMenu 源码实现简看

DropdownMenu 是一个 StatefulWidget ,通过状态类 _DropdownMenuState 维护状态数据以及处理视图的构建逻辑。

image.png

从构建逻辑上来看最主要依赖 ShortcutsMenuAnchorTextFiled 等组件:

image.png

其中 Shortcuts 在最顶层,和 Actions 联合使用处理键盘快捷键事件。比如菜单栏展开时 按键可以上下激活选中菜单。借此我们也可以学到如何让一个组件响应快捷键处理逻辑。

image.png


其中最核心的视图表现是对 MenuAnchor 组件的封装,在 builder 回调中构建输入框、首尾按钮等展示内容。内容的排列通过 _DropdownMenuBody完成;菜单列表是 menuChildern 属性,传入 menu :

image.png

其中 menu 对象是通过 _buildButtons 构造的组件列表,也就是 DropdownMenuEntry 列表形成的菜单项:

image.png

DropdownMenu 的核心逻辑也就这些,它是对 MenuAnchor 使用的一个简单封装,如果希望定制化更过细节,也可以自己通过 MenuAnchor 来实现。之后有机会,会详细介绍一下 MenuAnchor 组件的使用。那么本就到这里,谢谢观看 ~