Flutter TolyUI 框架#04 | 侧栏菜单设计


theme: cyanosis

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


《Flutter TolyUI 框架》系列前言:

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

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

image.png

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


一、侧栏菜单设计思考

侧栏菜单可以说是 App 的第一门面,我们可以在很多桌面端应用产品中看到。它一般用于处理一些全局性的交互事件,比如导航、切换暗亮模式、弹出用户介绍面板等。比如下面自左到右依次是 企业微信有道翻译哔哩哔哩飞书

image.png


1. 封装、抽象与取舍

就像没有包治百病的药,也没有包实现所有功能的组件。封装在 取得 使用简洁性的同时,也必然要 舍去 一定的灵活性。框架主要目的就是将共性或复杂的功能封装在内部,以简化开发者使用,其关键在于如何平衡取舍。 侧栏菜单条目具有非常强的灵活性,靠一个组件来封装所有的可能性是不现实的。

功能需求的多变性和视图表现的多样性,让我思考:

对于 侧栏菜单 的交互过程中,什么是共性的、什么是个性的、什么是复杂的、什么是可封装的。


在视图结构中,侧栏菜单有着类似的结构,可以分为上中下三个部分,上分一般放置用户头像,或者应用 logo。中间放置菜单项,下方放置一些图标按钮触发事件:

image.png

对于不同的开发者来说,菜单项展示的具体视图是个性化的,每个 App 的 UI 设计或者功能需求都不同。但在交互过程在,菜单项的某些视觉表现也存在共性,比如 悬浮事件动画效果宽度拖拽 等功能。所以对于条目来说,如何在封装共性时,提供给开发者个性化的构建方式,是一个挑战。庆幸的是 TolyUI 实现了这一点。


2. 侧栏菜单设计动机

Flutter 虽然提供了 NavigationRail 组件展示侧栏菜单,但是可定制性很差。很多样式无法自主控制,所以 TolyUI 希望提供 TolyRailMenuBar 组件,使得侧栏的表现样式可以更自由地构建。

这就是侧栏菜单设计动机,它在交互语义上承担的职能是:

[1]. 承载若干个 事件元件 ,参与交互。
[2]. 展示菜单列表,一般用于切换导航中的路由界面。
[3]. 展示头像、logo、图标按钮等附加视图元件。

如下所示,是 TolyUI 提供的侧栏菜单效果。将 悬浮事件动画效果宽度拖拽 封装在内部,对于条目来说,使用者可以通过回调来自定义构建内容,其中是否悬浮、动画数据、宽度信息等内部数据,将会通过回调参数让使用者感知到,而不必在意内部具体的复杂逻辑实现。这就是框架需要承担的复杂性与灵活性的平衡:

01.gif


3. 导航视图模块: tolyui_navigation

为了让 TolyUI 的功能模块可以细粒度地服务于开发者,采取模块化的分包模式。导航视图相关的组件,将通过 【tolyui_navigation】 包独立维护。

image.png

tolyui 的模块化将呈现一个树形结构,父节点的模块可以享用子模块的所有功能。同时子模块又可以单独存在,服务于开发者。

image.png


二、TolyRailMenuBar 的使用方式

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

> 组件/反馈组件/popover: toly1994.com/ui/#//widgets/navigation/railmenubar

1. TolyRailMenuBar 的基本样式

左侧是支持拖拽拉伸,点击选中时条目背景色、字号、指示器动画变化。
中间是禁止拖拽拉伸的设置案例。
右间是自定义动画参数的配置案例。

02.gif

TolyRailMenuBar 只需要简单地配置属性,就可以达到展示的效果。

  • 菜单项属于来源于 MenuMate 元数据列表。元数据中可以指定图标、标签文字和对应的路径。
  • 它需要指定一个激活 id 表示当前的激活项,一般取用路径。
  • 菜单项的点击事件通过 onSelected 感知,可以在其中除了跳转路由或更新激活 id 的工作。
  • enableWidthChange 可以启用拖拽改变宽度,maxWidth 设置最大宽度值。

```dart String activeId = '/guide/start';

List navMenus = const [ MenuMate(icon: Icons.real estateagent rounded, label: "开始使用", router: '/guide/start'), MenuMate(icon: Icons.accounttree, label: "模块树", router: '/guide/modules'), MenuMate(icon: Icons.privacy tip, label: "设计原则", router: '/guide/principle'), MenuMate(icon: Icons.notealt, label: "更新日志", router: '/guide/update_log'), ];

TolyRailMenuBar( width: 72, maxWidth: 200, menus: navMenus, activeId: activeId, enableWidthChange: true, backgroundColor: backgroundColor, onSelected: onSelected, leading: (type) => DebugLeadingAvatar(type: type), tail: (type) => DebugTail(type: type), ); ```

和 Flutter 内置组件激活和事件设计类似,激活 id 变化后需要重新构建 TolyRailMenuBar 组件。实际使用中,一般会点击时跳转路由。然后监听路由的变化,改变激活 id。可参考 TolyUI 官网实现的相关源码。

dart void onSelected(String path) { setState(() { activeId = path; }); }


2. 菜单宽度类型

另外注意一点,leading 和 tail 是首尾的区域构建回调,使用者可以自由展示组件。其中会回调宽度类型辅助构建,这样便于实现宽窄模式下不同的视图表现,如下所示:

image.png

目前有两种类型,smalllarge:

dart enum MenuWidthType { small, large }

框架中 默认情况 下宽度大于 140 视为 large;但也提供了 widthTypeParser 解析策略,你可以设置它来自定义何时是 large。这就是在封装功能的基础上,给使用者自定义操作的空间。保证简易性的同时,增加灵活性。而这就是回调函数所带来的效力。

dart TolyRailMenuBar( width: 72, maxWidth: 240, widthTypeParser: (width) => width > 150 ? MenuWidthType.large : MenuWidthType.small, /// 略同...


3. 动画参数的配置

TolyRailMenuBar 可以通过 AnimationConfig 东西配置 动画时长动画曲线动画触发方式 三个数据:

03.gif

比如右侧的案例会在鼠标悬浮时触发动画,是因为 type 设置为 AnimTickType.hove,你也可以设置为 null 来禁用动画。默认是激活状态变化时触发动画。

image.png

dart TolyRailMenuBar( animationConfig: const AnimationConfig( duration: Duration(milliseconds: 500), curve: Curves.fastEaseInToSlowEaseOut, type: AnimTickType.hove, ), );

默认样式对暗色主题也有适配处理,当然配色方面你也可以自定义设置:

image.png


三、TolyRailMenuBar 自定义菜单项

业务需求千差万别,你可能需要在菜单项上加千奇百怪的装饰。对一个框架来说,是不可能,也没有必要面面俱到的。TolyRailMenuBar 提供了 MenuCellBuilder 构造器,让开发者拥有极大的发挥空间,来自定义菜单项内容。用变化来处理变化,才是永恒不变的真理。


1. 如何自定义菜单项

如果 TolyUI 默认的条目展示样式不符合需求,可以通过 cellBuilder 参数自定义菜单项,其中 menudisplay 分别承载菜单和展示信息的元数据,展示信息包括动画值、宽度类型、是否选中、是否激活等。这些封装在框架内部的功能,通过回调的方式暴露核心数据,让开发者可以感知到,并依赖于它们自由构建视图。

image.png

比如下面的 QiWeiMenuCell 是自定义的组件,模仿企业微信的侧栏菜单。该案例会禁用过渡动画,整个体看起来简洁清爽:

05.gif

自定义的逻辑也不过 40 行代码,将 MenuMateDisplayMate 作为自定义函数。构建过程中,依赖 DisplayMate 提供选中、悬浮数据,依赖 MenuMate 提供图标、标签信息。构建一个容器包裹文字和图标即可:

```dart class QiWeiMenuCell extends StatelessWidget{ final MenuMate menu; final DisplayMate display;

const QiWeiMenuCell({ super.key, required this.menu, required this.display, });

Color? get foregroundColor { return display.selected ? Colors.white : const Color(0xffafc8e8); }

Color? get backgroundColor { if(display.hovered&&display.selected) return const Color(0xff578acf); if(display.hovered) return const Color(0xff427cc9); if(display.selected) return const Color(0xff4c83cc); return Colors.transparent; }

@override Widget build(BuildContext context) { bool largeWidth = display.widthType==MenuWidthType.large; TextStyle style = TextStyle(color: foregroundColor, fontSize: largeWidth?14:11); BorderRadius br = const BorderRadius.all( Radius.circular(6)); return Container( alignment: largeWidth?Alignment.centerLeft:Alignment.center, padding: largeWidth?const EdgeInsets.symmetric(horizontal: 12):null, decoration: BoxDecoration(color: backgroundColor, borderRadius: br), height: largeWidth?42:56, child: Wrap( spacing: 6, direction: largeWidth ? Axis.horizontal : Axis.vertical, crossAxisAlignment: WrapCrossAlignment.center, children: [ Icon(menu.icon, color: foregroundColor, size: 18), Text(menu.label, style: style), ], ), ); } } ```


2. 如何自定义菜单项:仿哔哩哔哩

如下所示,哔哩哔哩桌面端应用侧栏导航没有圆角着色,动画触发的事件在悬浮时,文字颜色由黑渐变到粉色,取消激活时从紫色渐变到黑色。同样自定义一个 BilibliMenuCell 作为 cellBuilder 返回值即可:

image.png

07.gif

这个效果中使用了动画,通过 DisplayMate 对象可以感知到当前动画值这样借由 Tween 皆可以轻松实现数值上的过渡。代码如下所示:

```dart class BilibliMenuCell extends StatelessWidget { final MenuMate menu; final DisplayMate display;

const BilibliMenuCell({ super.key, required this.menu, required this.display, });

ColorTween get foregroundTween => ColorTween( begin: const Color(0xff61666d), end: const Color(0xffff6699), );

ColorTween get textTween => ColorTween( begin: const Color(0xff9499a0), end: const Color(0xffff6699) );

Color? get foregroundColor => foregroundTween.transform(display.rate); Color? get textColor => textTween.transform(display.rate);

@override Widget build(BuildContext context) { bool largeWidth = display.widthType==MenuWidthType.large; TextStyle style = TextStyle(color: textColor, fontSize: 12); return Container( alignment: largeWidth?Alignment.centerLeft:Alignment.center, height: 64, child: Wrap( spacing: 6, direction: largeWidth ? Axis.horizontal : Axis.vertical, crossAxisAlignment: WrapCrossAlignment.center, children: [ Icon(menu.icon, color: foregroundColor, size: 24), Text(menu.label, style: style), ], ), ); } } ```


3. 自定义 TolyUI 默认样式

除了 cellBuilder 自定义菜单项展示之外,为了简化使用 TolyUI 默认样式也提供了样式数据,通过 MenuCellStyle 对象来配置,如下是一个黑色风格的侧栏导航。

06.gif

配置方式如下所示,如果这些样式无法满足你的需求,可以将源码中的 TolyUiMenuCell 组件改吧改吧拿来用。

dart TolyRailMenuBar( width: 64, gap: 10, maxWidth: 200, cellStyle: const MenuCellStyle( showIndicator: false, // 隐藏指示器 hideActiveText: false, // 激活时不隐藏文字 height: 56, // 高度 heightLarge:46, // 宽模式高度 hoverColor: Color(0xff4b5569), activeColor: Colors.white, foregroundColor: Color(0xffc2c5cc), iconSize: 20, ),

我思考过是否需要为 TolyRailMenuBar 提供主题配置。就目前而言 TolyRailMenuBar 有足够灵活的自定义方式进行展示,它只是封装了宽度拖拽、布局结构、动画处理等共性功能。

另外,应该 App 中可能有 500 个链接组件,1000 个按钮组件。但侧栏导航并不会出现非常多次,通过主题来统一样式配置的意义也不大。所以希望把时间和精力花在刀刃上,暂时不提供 TolyRailMenuBar 的主题配置。后面有时间再酌情考量。


四、 TolyRailMenuBar 实践: FlutterUnit 侧栏导航

下面以一个具体的案例,来介绍一下 TolyRailMenuBar 的使用。关注我的应该的知道 【FlutterUnit】 是我的一个知名开源项目,介绍 Flutter 内置组件的使用,以及一些有趣的知识集锦。其中的侧栏菜单是之前花了挺大心力手搓的,现在看一下如何通过 TolyRailMenuBar 来轻松实现它,旧版代码可在 这里 查看。


1. 迁移样式

FlutterUni 的侧栏菜单之前效果如下,包括菜单项激活状态变化的动画效果:

09.gif

tolyui 全家桶目前还没有正式发布,而是分模块逐步推进。所以这里只使用导航模块 【tolyui_navigation】,将其加入到 pubspec.yaml 中 :

yaml dependencies: ... ## tolyui 导航模块 tolyui_navigation: ^0.0.2


侧栏菜单分为上中下三个区域,顶部 MenuBarLeading、底部 MenuBarTail 组件和之前界面类似,这里不做细数,已经抽离为两个组件,详见源码。

image.png


2. 自定义菜单项

菜单项是一个右圆角矩形,在激活变化时,宽度、颜色、字号会动画渐变。这里通过三个 Tween 对动画数值进行计算。通过 MenuMateDisplayMate 中的数据,构建具体的菜单条目组件内容:

image.png

```dart final Tween _widthTween = Tween(begin: 0.82, end: 0.95); final Tween _sizeTween = Tween(begin: 18.0, end: 22.0); final Tween _fontSizeTween = Tween(begin: 14.0, end: 15);

class FlutterUnitMenuCell extends StatelessWidget { final MenuMate menu; final DisplayMate display;

const FlutterUnitMenuCell.create(this.menu, this.display, {super.key});

Color? get foregroundColor => display.selected ? Colors.white : Colors.white70;

@override Widget build(BuildContext context) { double height = 42; double anim = display.rate; Color? color = ColorTween( begin: Colors.white.withAlpha(33), end: Theme.of(context).primaryColor).transform(anim); double iconSize = _sizeTween.transform(anim); double fontSize = _fontSizeTween.transform(anim); TextStyle style = TextStyle(color: foregroundColor, fontSize: fontSize); Radius radius = Radius.circular(height / 2); BorderRadius br = BorderRadius.only(topRight: radius, bottomRight: radius);

return Align(
  alignment: Alignment.centerLeft,
  child: Container(
    alignment: Alignment.center,
    decoration: BoxDecoration(color: color, borderRadius: br),
    width: _widthTween.transform(anim) * 130,
    height: height,
    child: Wrap(
      spacing: 6,
      crossAxisAlignment: WrapCrossAlignment.center,
      children: [
        Icon(menu.icon, color: foregroundColor, size: iconSize),
        Text(menu.label, style: style),
      ],
    ),
  ),
);

} } ```


3. 使用 TolyRailMenuBar 组件

首先菜单的数据需要国际化,在 didChangeDependencies 中初始化 List<MenuMeta> 菜单元数据:

```dart late List deskNavBarMenus;

@override void didChangeDependencies() { super.didChangeDependencies(); String widget = context.l10n.widgetCollection; String canvas = context.l10n.paintCollection; String knowledge = context.l10n.knowledgeCollection; String treasure = context.l10n.treasureTools; String account = context.l10n.homeAccount; deskNavBarMenus = [ MenuMeta(label: widget, icon: TolyIcon.iconlayout, router: '/widget'), MenuMeta(label: canvas, icon: Icons.palette, router: '/painter'), MenuMeta(label: knowledge, icon: TolyIcon.iconartifact, router: '/knowledge'), MenuMeta(label: treasure, icon: TolyIcon.icon_fast, router: '/tools'), MenuMeta(label: account, icon: Icons.person, router: '/account'), ]; } ```


然后 build 方法中构建 TolyRailMenuBar 组件,cellBuilder 指定为 FlutterUnitMenuCellcreate 构造。create 是一个命名构造方法,其本质上也是一个函数,而 cellBuilder 需要的也是一个返回 Widget 的函数,所以可以直接赋值。简化书写形式。
另外在 onSelected 回调事件触发 context.go 跳转路由,这也是赋值函数对象:

image.png

dart @override Widget build(BuildContext context) { return TolyRailMenuBar( cellBuilder: FlutterUnitMenuCell.create, width: 130, gap: 8, padding: EdgeInsets.zero, backgroundColor: const Color(0xff2C3036), menus: deskNavBarMenus, activeId: activePath, enableWidthChange: false, onSelected: context.go, tail: (_) => const MenuBarTail(), leading: (_) => const MenuBarLeading(), ); }

最后激活 id 通过当前路由来确定,由下方的 activePath 承担该工作,它会依赖 GoRouterState 得到当前路由,取第一段作为激活路径:

```dart final RegExp _segReg = RegExp(r'/\w+');

String? get activePath { final String path = GoRouterState.of(context).uri.toString(); RegExpMatch? match = _segReg.firstMatch(path); if (match == null) return null; String? target = match.group(0); return target; } ```


4. 尾声

到这里 TolyRailMenuBar 就介绍完了。对于树形的导航菜单将单独通过另一个组件 TolyRailMenuTree 实现。目前为止,TolyUI 已经完成了响应式布局和反馈模块的核心功能。下一步是对导航模块的设计开发:

image.png

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

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

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Flutter是一个跨平台的移动应用框架,它能够在iOS和Android上运行。Flutter for web是Flutter团队推出的一个新项目,它可以让Flutter应用在浏览器上运行。选择使用Flutter for web可以让你使用同一套代码同时开发移动应用和Web应用,减少重复工作量。但是, Flutter for web 目前还在 beta 版本,开发者需要解决一些兼容性问题. ### 回答2: 选择Flutter Web框架可以让我们在构建跨平台应用程序时享受到许多好处。以下是选择Flutter Web框架的一些原因: 首先,Flutter Web是Google官方支持的框架,由Google团队开发和维护。这意味着它得到了一流的技术支持和持续的更新,并且在未来会持续改进和发展。这使得我们能够始终跟上最新的Web开发趋势和最佳实践。 其次,Flutter Web采用了Dart语言,这是一种功能强大且易于学习的语言。Dart具有先进的特性,如热重载、即时编译和静态类型检查,可以显著提高开发效率和编码质量。因此,即使没有Web开发经验的开发人员也可以快速上手。 第三,Flutter Web具有出色的性能。由于Flutter使用自己的渲染引擎,并能够通过直接绘制UI来避免JavaScript的性能损失,因此Flutter Web应用程序能够提供流畅且快速的用户体验。这对于要求高性能的Web应用程序来说是非常重要的。 另外,Flutter Web提供了丰富而强大的UI组件库,可以帮助我们快速构建精美的用户界面。这些组件可以轻松地适配各种屏幕大小,并且具有高度可定制的特性。此外,Flutter还支持热重载,这意味着我们可以在实时预览中立即看到应用程序的更改,从而提高了开发效率。 最后,由于Flutter Web可以直接编译为JavaScript代码并在现代浏览器上运行,因此我们可以将应用程序直接部署到Web服务器上,而无需额外的插件或工具。这使得我们能够以一种简单且高效的方式将应用程序发布到Web上,并获得更广泛的用户群体。 综上所述,考虑到Flutter Web的技术支持、性能优势、易用性和丰富的UI组件库,选择Flutter Web框架是一个明智的决策,可以帮助我们快速构建出色的Web应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值