前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter TolyUI 框架#05 | 树形菜单设计

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

作者头像
张风捷特烈
发布2024-05-23 08:14:00
1500
发布2024-05-23 08:14:00
举报
文章被收录于专栏:Android知识点总结

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

《Flutter TolyUI 框架》系列前言:

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

开源地址: github.com/TolyFx/toly…

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


一、树形菜单设计思考

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

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


1. 树形菜单设计动机

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

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


2. 树形菜单的职能

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

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

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


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

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

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

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

代码语言:javascript
复制
class MenuMeta implements Identify<String>{
  final String router;
  final String label;
  final bool enable;
  final IconData? icon;
  
  // ...
  
  @override
  String get id => router;
}

abstract interface class Identify<T> {
  T get id;
}

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

代码语言:javascript
复制
class MenuNode implements Identify<String> {
  final List<MenuNode> children;
  final int depth;
  final MenuMeta data;

二、 TolyRailMenuTree 的基本使用

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

组件/导航/rail_menu_tree: toly1994.com/ui/#/widget…


1. 菜单节点树的解析

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

代码语言:javascript
复制
---->[伪代码]----
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 项目的映射数据:

: 树形结构的嵌套层级不可避免,数据全部信息可以参阅 plcki_menu_tree_data.dart

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

代码语言:javascript
复制
MenuNode root = MenuNode.fromMap(plckiMenuData);

2. TolyRailMenuTree 的使用

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

  • activeMenu: 当前激活的菜单。
  • expandMenus : 展开的菜单标识列表。
  • root : MenuNode 菜单节点树。
代码语言:javascript
复制
class MenuTreeMeta {
  final List<String> expandMenus; 
  final MenuNode? activeMenu;
  final MenuNode root;

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

代码语言:javascript
复制
class RailMenuTreeDemo1 extends StatefulWidget {
  const RailMenuTreeDemo1({super.key});
  
  @override
  State<RailMenuTreeDemo1> createState() => _RailMenuTreeDemo1State();
}

class _RailMenuTreeDemo1State extends State<RailMenuTreeDemo1> {

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

代码语言:javascript
复制
  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 方法,便于开发者基于当前状态处理选中时的数据变化:

代码语言:javascript
复制
  @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 模式不会产生任何影响。

代码语言:javascript
复制
@override
  void reassemble() {
    _initTreeMeta();
    super.reassemble();
  }

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


3. 仅展开一个子面板

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

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

代码语言:javascript
复制
void _onSelect(MenuNode menu) {
  _menuMeta = _menuMeta.select(menu,singleExpand: true);
  setState(() {});
}

4. 树形菜单配置参数

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

属性名

类型

介绍

enableWidthChange

bool

是否支持宽度拉伸

width

double

默认宽度

maxWidth

double

可拉伸最大宽度

leading

Widget

头部组件

tail

Widget

尾部组件

代码语言:javascript
复制
TolyRailMenuTree(
  leading: const DebugLeadingAvatar(),
  tail: const VersionTail(),
  ...

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

属性名

类型

介绍

backgroundColor

Color?

背景色

expandBackgroundColor

Color?

展开背景色

cellStyle

MenuTreeCellStyle

菜单项样式

如下所示,对于暗色模式的适配,可以通过上下文感知是否是暗色模式。为 TolyRailMenuTree 配置不同的颜色:

:context.isDark 是 TolyUI 的拓展方法,本质是 Theme.of(this).brightness == Brightness.dark

代码语言:javascript
复制
@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,
    ...
  ),
}

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

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


1. 拓展元数据

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

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

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

代码语言:javascript
复制
class PlckiMenuMetaExt extends MenuMateExt {
  final String? subtitle;
  final String? tag;

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

  factory PlckiMenuMetaExt.fromMap(Map<String, dynamic> map) {
    return PlckiMenuMetaExt(
      subtitle: map['subtitle'],
      tag: map['tag'],
    );
  }
}

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

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

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

代码语言:javascript
复制
void _initTreeMeta() {
  MenuNode root = MenuNode.fromMap(
    plckiMenuDataPlus,
    extParser: PlckiMenuMetaExt.fromMap,
  );
}

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


3. 自定义菜单项构建

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

image.png
image.png
代码语言:javascript
复制
typedef MenuTreeCellBuilder = Widget Function(
  MenuNode node,
  DisplayMeta display,
);

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

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

代码语言:javascript
复制
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,敬请期待 ~

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

github 开源地址: github.com/TolyFx/toly… TolyUI 官方案例演示网站:toly1994.com/ui

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 《Flutter TolyUI 框架》系列前言:
  • 一、树形菜单设计思考
    • 1. 树形菜单设计动机
      • 2. 树形菜单的职能
        • 3. 树形菜单在使用上的设计
        • 二、 TolyRailMenuTree 的基本使用
          • 1. 菜单节点树的解析
            • 2. TolyRailMenuTree 的使用
              • 3. 仅展开一个子面板
                • 4. 树形菜单配置参数
                • 三、拓展元数据和自定义菜单样式
                  • 1. 拓展元数据
                    • 2. 映射数据拓与展元数据解析
                      • 3. 自定义菜单项构建
                      • 四、小结
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档