Flutter TolyUI 框架#05 | 树形菜单设计


theme: cyanosis

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


《Flutter TolyUI 框架》系列前言:

TolyUI张风捷特烈 打造的 Fluter 全平台应用开发 UI 框架。具备 全平台组件化源码开放响应式 四大特点。可以帮助开发者迅速构建具有响应式全平台应用软件:

开源地址: https://github.com/TolyFx/toly_ui

image.png

该系列将详细介绍 TolyUI 框架使用方式、框架开发过程中的技术知识、设计理念、难题解决等。


一、树形菜单设计思考

树形是一种非常自然而常见结构,它可以展示大量具有自相似的信息。比如文件夹中包含文件夹、文件;XMind 中一个节点可以分出若干个枝节点,这些都树形结构数据在界面上展示信息的需求。

image.png

在布局空间中,树形结构具有 折叠特性 ,可以延和收起子区域。子区域的偏移也能更好的展示树形的层次结构。 本文将探讨 TolyUI 在树形导航菜单中的设计。


1. 树形菜单设计动机

树形菜单是 Flutter 本身不支持的,但在桌面端或 Web 端中是非常常见。所以设计一个树形菜单组件是非常必要,它属于一种基础设施。有了之前的 TolyRailMenuBar 的实现经验,抽象与封装就相对简单。其中条目提供了 TolyUI 的默认样式,并且也提供了菜单项的自定义构建途径。

TolyUI 模块化设计中,树形菜单对应的组件是 TolyRailMenuTree。隶属于 【tolyui_navigation】 导航模块:

image.png


2. 树形菜单的职能

树形菜单在交互语义上承担的职能是:

[1]. 承载若干个 视图元件 ,并参与交互。
[2].  视图元件 间呈树形组织结构。
[3]. 允许交互时,动画折叠/收起子节点。

下面是 PLCKI 项目导航的树形结构效果,采用了 TolyUI 的默认风格:

01.gif


3. 树形菜单在使用上的设计

树形结构在使用时,最复杂的地方莫过于节点对象的创建。如何更好的提供树形数据组织形式和解析方式,也是 TolyRailMenuTree 需要考量的地方。

Toly对于树形菜单,定义了两个类型 MenuNodeMenuMeta:

image.png

其中 MenuMeta 是菜单的元数据,包含菜单项需要的所有基本信息。包括路由、标签、图标、是否可用四个核心字段。另外这里定义了一个 Identify 的接口,标识唯一的身份标识,对于 MenuMeta 来说,路由信息 router 是一个 MenuMeta 的唯一标识:

```dart class MenuMeta implements Identify { final String router; final String label; final bool enable; final IconData? icon;

// ...

@override String get id => router; }

abstract interface class Identify { T get id; } ```

MenuNode 会持有 MenuMeta 数据以及 MenuNode 列表,以此实现树形的组织结构结构:

dart class MenuNode implements Identify<String> { final List<MenuNode> children; final int depth; final MenuMeta data;


二、 TolyRailMenuTree 的基本使用

TolyRailMenuTree 的使用案例介绍可以网站访问 TolyUI 的 web 版 Flutter 应用。或者下载各平台的桌面端程序查阅体验。

组件/导航/railmenutree: http://toly1994.com/ui/#/widgets/navigation/railmenutree


1. 菜单节点树的解析

如果仅靠手动书写菜单节点树,会写出非常复杂的代码。比如下面的伪代码,这不仅不便于阅读和维护,也不便于数据传输。比如菜单树的节点信息树如果是网络请求返回的 Json 数据,这种方式需要额外的解析:

dart ---->[伪代码]---- MenuNode( data:MenuMeta..., children: [ MenuNode( data:MenuMeta..., children: [ MenuNode(data:MenuMeta...), MenuNode(data:MenuMeta...), MenuNode(data:MenuMeta...), MenuNode(data:MenuMeta...), ] ), MenuNode( data:MenuMeta..., children: [ MenuNode(data:MenuMeta...), MenuNode(data:MenuMeta...), MenuNode(data:MenuMeta...), MenuNode(data:MenuMeta...), ] ) ] )

为了更便于开发者的使用,TolyUI 内部提供了映射关系 Map 到 MenuNode 的转换逻辑。你只需要定义类似于 Json 样式的 Map 对象,传入解析器即可得到 MenuNode 节点。映射数据是菜单数据的源泉,一份映射数据对应着唯一的菜单树,比如下面是 PLCKI 项目的映射数据: : 树形结构的嵌套层级不可避免,数据全部信息可以参阅 plckimenutree_data.dart

image.png

你只需要通过 MenuNode.fromMap 构造,既可以将 plckiMenuData 的映射数据解析转换为 MenuNode 树形节点:

dart MenuNode root = MenuNode.fromMap(plckiMenuData);


2. TolyRailMenuTree 的使用

对于树形菜单来说,交互过程中决定它展示样式的有三个核心数据。这里通过 MenuTreeMeta 进行维护:

  • activeMenu: 当前激活的菜单。
  • expandMenus : 展开的菜单标识列表。
  • root : MenuNode 菜单节点树。

dart class MenuTreeMeta { final List<String> expandMenus; final MenuNode? activeMenu; final MenuNode root;


MenuTreeMeta 将作为树形菜单展示的核心数据,作为 TolyRailMenuTree 的入参。如下案例中,由于交互过程中 MenuTreeMeta 数据需要改变,使用 StatefulWidget 组件通过状态类维护状态变化,当然你也可以使用任何形式的状态管理 方式。

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

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

class _RailMenuTreeDemo1State extends State { ```

在状态类的 initState 回调中通过 _initTreeMeta 方法,初始化 _treeMeta 数据。默认展开 dashboard 、激活 /dashboard/home 菜单项。 MenuNode 中提供了 find 方法可以查找指定路径的节点:

```dart late MenuTreeMeta _treeMeta;

@override void initState() { super.initState(); _initTreeMeta(); }

void _initTreeMeta() { MenuNode root = MenuNode.fromMap(plckiMenuData); _treeMeta = MenuTreeMeta( expandMenus: ['/dashboard'], activeMenu: root.find('/dashboard/home'), root: root, ); } ```

组件构建过程中,使用 TolyRailMenuTree 组件,meta 参数为上面初始化的 MenuTreeMeta 数据。另外可以通过 onSelect 回调,感知用户点击条目的事件。MenuTreeMeta 中提供了 select 方法,便于开发者基于当前状态处理选中时的数据变化:

```dart @override Widget build(BuildContext context) { return SizedBox( height: 460, child: TolyRailMenuTree( enableWidthChange: true, meta: _treeMeta, onSelect: _onSelect, ), ); }

void _onSelect(MenuNode menu) { _menuMeta = _menuMeta.select(menu); setState(() {}); } } ```

最后说明一点,如果希望 Debug 模式下开发过程 中,每次修改 plckiMenuData 映射数据时,界面可以 热重载 。可以复写 reassemble 方法重新加载数据,它仅对 debug 模式生效,对 release 模式不会产生任何影响。

dart @override void reassemble() { _initTreeMeta(); super.reassemble(); }

这就是 TolyRailMenuTree 最基本的使用,树形结构的视图构建逻辑被封装在框架内部,使用者只需简单地配置数据即可。另外,通过自定义映射关系和解析函数,可以极大方便开发者对树形结构数据的维护。树形结构的映射关系,也可以通过网络请求 json 数据解码获得,这样就可以动态化配置菜单树。


3. 仅展开一个子面板

有时我们希望可以在展开子菜单面板时,关闭其他已展开面板。如下所示:

02.gif

菜单选择时状态变化,是通过 MenuTreeMeta#select 方法完成的。其中封装了选中和折叠的逻辑,并且提供了 singleExpand 参数,默认为 false。将其置为 true 时,可以实现上面的仅展开一个面板的功能:

dart void _onSelect(MenuNode menu) { _menuMeta = _menuMeta.select(menu,singleExpand: true); setState(() {}); }


4. 树形菜单配置参数

树形菜单和侧栏菜单类似,可以配置上方和下方区域的组件,以及右侧边线区域,可拉伸面板。

| 属性名 | 类型 | 介绍 | | --- | --- | --- | | enableWidthChange | bool | 是否支持宽度拉伸 | width | double | 默认宽度 | maxWidth | double | 可拉伸最大宽度 | leading | Widget | 头部组件 | tail | Widget | 尾部组件

image.png

dart TolyRailMenuTree( leading: const DebugLeadingAvatar(), tail: const VersionTail(), ...


配色方面,可以设置背景色、展开背景色、菜单项样式。如下所示,是暗色模式下对树形菜单的样式表现。

| 属性名 | 类型 | 介绍 | | --- | --- | --- | | backgroundColor | Color? | 背景色 | expandBackgroundColor | Color? | 展开背景色 | cellStyle | MenuTreeCellStyle | 菜单项样式

image.png

如下所示,对于暗色模式的适配,可以通过上下文感知是否是暗色模式。为 TolyRailMenuTree 配置不同的颜色: :context.isDark 是 TolyUI 的拓展方法,本质是 Theme.of(this).brightness == Brightness.dark

```dart @override Widget build(BuildContext context) { Color expandBackgroundColor = context.isDark?Colors.black:Colors.transparent; Color backgroundColor = context.isDark?Color(0xff001529):Colors.white;

TolyRailMenuTree( backgroundColor: backgroundColor, expandBackgroundColor: expandBackgroundColor, ... ), } ```


三、拓展元数据和自定义菜单样式

不同的应用程序,由于功能需求的差异,菜单的元数据可能会有不同。比如下面的菜单项可以展示 副标题标签 两个额外的信息。那该怎么办呢?

image.png


1. 拓展元数据

其实框架内部可以在 MenuMeta 提供两个字段,但这并不是最优解。因为还有可能有其他额外数据,总不能每遇到一个就添加一个。这样违背了开放封闭原则,也不利于开发者灵活地自定义,毕竟这个行为属于框架层的源码修改。于是我设计了一种策略,将变化交由外界处理,框架只在意变化的结果:

如下所示,MenuMeta 元数据中增加了一个 MenuMateExt 的抽象对象,表示拓展元数据。开发者可以继承 MenuMateExt 来拓展项目中的个性化菜单元数据。

image.png

比如 PLCKI 项目中,树形菜单需要副标题和标签两个拓展元数据。定义如下的 PlckiMenuMetaExt 持有数据,并提供 fromMap 构造基于映射对象创建 PlckiMenuMetaExt 对象:

```dart class PlckiMenuMetaExt extends MenuMateExt { final String? subtitle; final String? tag;

const PlckiMenuMetaExt({ required this.subtitle, required this.tag, });

factory PlckiMenuMetaExt.fromMap(Map map) { return PlckiMenuMetaExt( subtitle: map['subtitle'], tag: map['tag'], ); } } ```


2. 映射数据拓与展元数据解析

前面说过,树形结构是由 映射数据 决定的,所以拓展数据也需要加入到映射数据中。如下所示,在菜单项的映射数据中,可以放入对应的拓展项:完整数据可见 plckimenutreedataplus.dart

image.png

有了数据之后,接下来的问题就是:如何将映射数据中的拓展字段,解析到 MenuMeta 对象的拓展数据中。如下所示,在 MenuNode.fromMap 中,有一个 extParser 的解析函数,将其置为 PlckiMenuMetaExt.fromMap 构造函数即可。

dart void _initTreeMeta() { MenuNode root = MenuNode.fromMap( plckiMenuDataPlus, extParser: PlckiMenuMetaExt.fromMap, ); }

通过调试可以看出拓展的元数据已经被解析放入了 MenuNode 节点中了。可以看出,开发者可以很简单地拓展这些数据,其中复杂的解析逻辑,树形结构处理都由 TolyUI 框架内部处理。

image.png


3. 自定义菜单项构建

TolyRailMenuBar 一样,TolyRailMenuTree 也支持自定义菜单项条目。其中会回调 MenuNodeDisplayMeta 数据,作为菜单项构建过程中的数据支持:

image.png

dart typedef MenuTreeCellBuilder = Widget Function( MenuNode node, DisplayMeta display, );

我们上面已经将拓展数据解析放入了 MenuMetaext 字段中,而 MenuNode 持有MenuMeta。也就是说,我们可以在构建逻辑中访问拓展数据,将其呈现在界面上。

03.gif

PlckiTreeMenuCell 在构建过程中 ext 拓展数据通过 menuNode.data.ext 得到。下面是基于 PlckiMenuMetaExt 数据构建标题(_buildTitle)和标签( _buildTag) 的逻辑。其他的构建逻辑和 TolyUI 中的一致,具体可以参考案例的实现代码 railmenutree_demo4.dart

```dart PlckiMenuMetaExt? get ext { if (menuNode.data.ext is PlckiMenuMetaExt) { return (menuNode.data.ext) as PlckiMenuMetaExt; } return null; }

Widget _buildTitle(Color? fgColor) { TextStyle subStyle = const TextStyle(fontSize: 12, color: Colors.grey); TextStyle titleStyle = TextStyle(color: fgColor); Widget child = Text(menuNode.data.label, overflow: TextOverflow.ellipsis, maxLines: 1, style: titleStyle); if (ext?.subtitle != null) { child = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ child, Text(ext!.subtitle!, style: subStyle) ], ); } return child; }

Widget _buildTag(PlckiMenuMetaExt? ext) { TextStyle tagStyle = const TextStyle(color: Colors.white, height: 1, fontSize: 12); Widget child = Text('${ext?.tag}', overflow: TextOverflow.ellipsis, maxLines: 1, style: tagStyle); return Padding( padding: const EdgeInsets.only(right: 8.0), child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), decoration: BoxDecoration( color: Colors.red.withOpacity(0.8), borderRadius: BorderRadius.circular(4)), child: child), ); } ```


四、小结

到这里 TolyUI 就完成了一个可以灵活定制的侧栏树形菜单 TolyRailMenuTree。目前为止,TolyUI 已经完成了响应式布局和反馈模块的核心功能。导航模块也完成了两个非常重要的组件,下一步会继续对导航模块进行开发,目标是下拉菜单 DropMenu,敬请期待 ~

image.png

感谢你关注 tolyui 的成长,如果喜欢,也希望你能在 github 中点赞支持\~

github 开源地址: https://github.com/TolyFx/toly_ui\ TolyUI 官方案例演示网站:http://toly1994.com/ui

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值