本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
TolyUI 是 张风捷特烈 打造的 Fluter 全平台应用开发 UI 框架。具备 全平台、组件化、源码开放、响应式 四大特点。可以帮助开发者迅速构建具有响应式全平台应用软件:
开源地址: github.com/TolyFx/toly…
该系列将详细介绍 TolyUI 框架使用方式、框架开发过程中的技术知识、设计理念、难题解决等。
下拉菜单 是我曾经开发桌面端 Flutter 应用的一根骨刺,虽然 Flutter 内置了 MenuAnchor 组件支持多级菜单。但是 Material 风格的样式很难自定义,在 TolyUI 实现 Popover 之后,让我看到了多级菜单自定义的曙光。
下拉菜单是一个非常非常重要的视图元件,它会将很多交互事件 收敛 到一块浮层区域。通过某些手势交互,比如点击、移入、右键等展开菜单浮层,参与交互。甚至可以通过树形结构来组织交互元件,从而大大拓展了可交互区域,另外其点击外部即可关闭的特点,也使得浮层交互非常轻量级。下面是几款应用中下拉菜的表现:
飞书 | 有道云笔记 | 企业微信 |
---|---|---|
| | |
导航之目的在于:对 布局空间 的拓展,以较小的区域来驱动更大的操作空间。比如侧栏导航的一个菜单项,可以驱动右侧大区域的内容变化。这就是一种以小区域,调度大区域的手段:
广义上来说,所有可以通过小区域调度其他区域的手段都可以称之为 导航。对于 UI 界面的交互来说,提示信息 Tooltip、 弹出浮层 Popover、对话框 Dialog 、侧栏导航 RailMenu 、Tabs 页签、新界面跳转,都是导航的一种体现。
弹出层是一个非常经典的以小区域博得额外大区域的交互手段,它可以在目标元件的基础上,展开额外的视觉元件,进行呈现或参与交互。可能会有人觉得,Tooltip、Popover、DropMenu 看起来都差不多,为什么不只用一个组件来完成呢?
Tooltip | Popover | DropMenu |
---|---|---|
| | |
视觉元件在界面中有其固有的语义,就像医生负责治疗、警察负责治安、教师进行教育。再细化分,医生有不同的门类,警察有不同的警种,教师有不同的科目。各司其职是一个社会稳定的保障。如果将整个应用程序交互,看作一个由视觉元件参与运转的社会,每种视觉元件应具有其固有的职能,这就是视觉元件的语义。
本质上来说,导航就是浮层面板的添加和移除。Flutter 中通过 Navigator push 推入的界面,最终也是以浮层节点的方式被加入路由栈,进行展示。根据浮层区域的大小和交互性,可以大致分为三个类别:
模态浮层 | 路由浮层 |
---|---|
| |
Popover 的职能是展示浮层面板,其功能比较宽泛,你可以在浮层面板中展示任何组件,所以其目的性比较弱。而 DropMenu 是基于 Popover 实现的一种 仅用于展示浮层菜单 的浮层面板。
打个比方,Popover 相当于一个警察的笼统概念,而 DropMenu 相当于刑警,专门负责侦查破案、抓捕罪犯工作。DropMenu 的职能是将若干个菜单交互动作,集中起来,通过浮层面板进行呈现和交互。
Tooltip 也相当于 Popover 的一种具体的功能实现者,它 仅用于展示提示文字,所以界面交互要比 DropMenu 更轻量。在 TolyUI 模块化中,Popover 隶属于交互反馈模块 tolyui_feedback 。而 DropMenu 希望视为导航模块的一员,所以 tolyui_navigation 将依赖于 tolyui_feedback 模块,调用相关功能。
TolyDropMenu 的使用案例介绍可以网站访问 TolyUI 的 web 版 Flutter 应用。或者下载各平台的桌面端程序查阅体验。
组件/导航/rail_menu_tree: toly1994.com/ui/#/widget…
如下效果是 TolyDropMenu 的基本使用方式:
通过 TolyDropMenu
组件,以 child
为目标展开下拉菜单,其中通过 hoverConfig
可以配置悬浮时打开菜单。具有点击行为的菜单项,称之为 ActionMenu
,它持有 MenuMeta 表示菜单的元数据。MenuMeta 的 router 是一个菜单项的唯一标识,在 onSelect
回调中可以响应点击每个菜单项的事件:
TolyDropMenu(
hoverConfig: const HoverConfig(enterPop: true, exitClose: true),
onSelect: onSelect,
menuItems: [
ActionMenu(const MenuMeta(router: '01', label: '1st menu item')),
ActionMenu(const MenuMeta(router: '02', label: '2nd menu item')),
ActionMenu(const MenuMeta(router: '03', label: '3rd menu item')),
ActionMenu(const MenuMeta(router: '04', label: '4ur menu item')),
],
child: DebugDisplayButton(
info: 'Hover Pop',
onPressed: () {},
),
);
void onSelect(MenuMeta menu) {
$message.success(message: '点击了 [${menu.label}] 个菜单');
}。
由于 TolyDropMenu 在底层基于 TolyPopover 实现,所以 TolyPopover 的很多基本属性对 TolyDropMenu 同样适用。比如:
decorationConfig
参数可以配置外框装饰效果;placement
参数配置浮层与目标组件的定位关系;childBuilder
回调构建目标组件,从而自主控制菜单的展示时间;offsetCalculator
偏移计数器,对浮层进行偏移。气泡框,bottom | 非气泡框,bottomStart |
---|---|
| |
TolyDropMenu(
placement: Placement.bottomStart,
offsetCalculator: boxOffsetCalculator,
decorationConfig: DecorationConfig(isBubble: false, backgroundColor: bgColor),
onSelect: onSelect,
menuItems: [
ActionMenu(const MenuMeta(router: '01', label: '1st menu item')),
ActionMenu(const MenuMeta(router: '02', label: '2nd menu item')),
ActionMenu(const MenuMeta(router: '03', label: '3rd menu item')),
ActionMenu(const MenuMeta(router: '04', label: '4ur menu item')),
],
childBuilder: (_, ctrl, __) {
return DebugDisplayButton(
info: 'Click Pop',
onPressed: ctrl.open,
);
});
上图所示,MenuMeta 可以设置 icon 展示图标,通过 DividerMenu
展示菜单项中的分割线:
将 ActionMenu 中的 enable
设置为 false,可以禁用菜单项,鼠标移入时会变为禁止样式:
ActionMenu(const MenuMeta(router: '03', label: '3rd menu item'), enable: false),
TolyDropMenu 支持子菜单的悬浮展开,并且子菜单超出边界时,也会自动适应对齐方式。比如下面的右侧案例,第三级菜单出现时超出右边界,会自动适应展示在左侧,这本质上是 TolyPopover 的特性。另外,通过 TolyDropMenu#subMenuGap
可以配置子菜单的水平偏移间距。
通过 SubMenu
来承载菜单项及子菜单数据,可以在 menus
参数中设置若干个菜单项。这里 menus 是 MenuDisplay 类型,目前有三种类型的菜单项,未来如有需要,还可以继续拓展:
menuItems: [
ActionMenu(const MenuMeta(router: '01', label: '1st menu item')),
ActionMenu(const MenuMeta(router: '02', label: '2nd menu item')),
SubMenu(const MenuMeta(router: 'export', label: 'export image'),
menus: [
ActionMenu(const MenuMeta(router: 'png', label: 'sub out .png')),
ActionMenu(const MenuMeta(router: 'jpeg', label: 'sub out .jpeg')),
ActionMenu(const MenuMeta(router: 'svg', label: 'sub out .svg')),
SubMenu(const MenuMeta(router: 'sub sub', label: 'sub sub menu'),
menus: [
ActionMenu(const MenuMeta(router: 's1', label: 'sub menu1')),
ActionMenu(const MenuMeta(router: 's2', label: 'sub menu2')),
ActionMenu(const MenuMeta(router: 's3', label: 'sub menu3')),
]),
]),
const DividerMenu(),
ActionMenu(const MenuMeta(router: '03', label: '3rd menu item'), enable: false),
ActionMenu(const MenuMeta(router: '04', label: '4ur menu item')),
],
TolyPopover 的 12 种弹出方式,对 TolyDropMenu 依旧适用。可以通过 placement
参数进行设置,效果如下:
TolyUI 的宗旨是为开发者提供灵活的视图元件构建方式,所以会尽可能地提供样式和回调,让开发者可以自主定义展示效果。TolyUI 框架内部会封装动画、结构、解析等逻辑,兼具易用性和灵活性。
如下所示,如果希望鼠标移入时背景色呈浅灰的圆角矩形。可以通过 DropMenuCellStyle
来配置样式,作为 TolyDropMenu#style 入参:
如下所示,DropMenuCellStyle 可以设置边距、圆角半径、前景色、背景色、禁用色、悬浮的前景和背景色。
DropMenuCellStyle lightStyle = const DropMenuCellStyle(
padding: EdgeInsets.symmetric(horizontal: 8),
borderRadius: BorderRadius.all(Radius.circular(6)),
foregroundColor: Color(0xff1f1f1f),
backgroundColor: Colors.transparent,
disableColor: Color(0xffbfbfbf),
hoverBackgroundColor: Color(0xfff5f5f5),
hoverForegroundColor: Color(0xff1f1f1f),
);
TolyDropMenu(
style: style,
...
默认情况下,菜单项首尾组件很难自定义。如何让左侧展示图片资源,或者任意组件呢。还记得上一篇 《树形菜单设计》 中 MenuMeta 可以通过拓展来丰富菜单项的展示内容吗?这里也是类似:
如下所示,自定义 MenuDisplayExt
,其中拓展了图片资源、尾部文字、以及左侧组件三个数据。
class MenuDisplayExt extends MenuMateExt {
final ImageProvider? image;
final String? action;
final Widget? leading;
const MenuDisplayExt({this.image, this.action,this.leading});
}
在对应的 MenuMeta
中设置对应的 ext
拓展对象,比如前面两个提供图片资源,后面两个提供 leading 组件:
ActionMenu(const MenuMeta(
router: '01',
label: '1st menu item',
ext: MenuDisplayExt(image: AssetImage('assets/images/icon_head.webp'), action: 'Ctrl+J'))),
ActionMenu(
const MenuMeta(router: '02',
label: '2nd menu item',
ext: MenuDisplayExt(image: AssetImage('assets/images/logo.png'), action: 'Ctrl+P')),
),
ActionMenu(const MenuMeta(router: '03', label: '3rd menu item',
ext: MenuDisplayExt(leading: SizedBox(width: 20))), enable: false),
ActionMenu(const MenuMeta(router: '04', label: '4ur menu item',
ext: MenuDisplayExt(leading: FlutterLogo(size: 20)))),
拓展属性之所以能在界面上展示出来,自然少不了基于这些属性构建 Widget 的逻辑。 TolyDropMenu
基于 tailBuilder
和 leadingBuilder
两个回调来自定义构建组件。同样回调中可以感知菜单的元数据 MenuMeta 以及内部的一些参数。
拿尾巴来说,可以通过 menu.ext?.me
方法查看拓展是否为指定类型,并得到该类型对象。如果 MenuDisplayExt 中的 action 文字非空,返回 Text 文字展示。也就是下面的尾部组件:
Widget? _tailBuilder(_, MenuMeta menu, DropMenuDisplayMeta display) {
MenuDisplayExt? ext = menu.ext?.me<MenuDisplayExt>();
if (ext?.action != null) {
const TextStyle style = TextStyle(color: Colors.grey, fontSize: 12);
return Padding(
padding: const EdgeInsets.only(left: 12.0),
child: Text(ext!.action!, style: style),
);
}
return null;
}
同理左侧的头部,也是通过 MenuDisplayExt
中的数据,构建出对应的组件进行呈现:
Widget? _leadingBuilder(_, MenuMeta menu, DropMenuDisplayMeta display) {
MenuDisplayExt? ext = menu.ext?.me<MenuDisplayExt>();
Widget? child;
if (ext?.image != null) {
child = Image(image: ext!.image!, width: 20);
}
if (ext?.leading != null) {
child = ext?.leading;
}
if (menu.icon != null) {
child = Icon(menu.icon!, size: 20);
}
if(child!=null){
return Padding(padding: const EdgeInsets.only(right: 8.0), child: child);
}
return null;
}
右键展开浮层菜单,对于桌面端来说是非常常见的功能,TolyDropMenu 也提供了支持。最后一个案例中就是这个功能,效果如下:
TolyDropMenu 的 childBuilder
回调可以感知 PopoverController 控制器。 GestureDetector
组件的 onSecondaryTapDown
用于监听鼠标右键按下事件。在其中通过 open
打开,并传入落点的相对坐标即可:
class DropMenuDemo7 extends StatelessWidget{
const DropMenuDemo7({super.key});
@override
Widget build(BuildContext context) {
return TolyDropMenu(
/// 略...
childBuilder: _childBuilder,
);
}
Widget _childBuilder(_, PopoverController ctrl, __) {
return GestureDetector(
onTapDown: (_) => ctrl.close(),
onSecondaryTapDown: (detail) => _onSecondaryTapDown(detail, ctrl),
child: Container(
color: const Color(0xfff7f7f7),
alignment: Alignment.center,
height: 180,
child: const Text('Right Click on here'),
),
);
}
void _onSecondaryTapDown(TapDownDetails details, PopoverController ctrl) async {
if (ctrl.isOpen) {
ctrl.close();
await Future.delayed(const Duration(milliseconds: 280));
}
ctrl.open(position: details.localPosition);
}
到这里 TolyUI 就完成了一个可以灵活定制的下拉菜单 TolyDropMenu。目前为止,TolyUI 已经完成了响应式布局和反馈模块的核心功能。导航模块也完成了三个非常重要的组件,下一步会继续对导航模块进行开发,目标是下拉菜单 Tabs 和 Breadcrumb,敬请期待 ~
感谢你关注 tolyui 的成长,如果喜欢,也希望你能在 github 中点赞支持~
github 开源地址: github.com/TolyFx/toly… TolyUI 官方案例演示网站:toly1994.com/ui