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

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

作者头像
张风捷特烈
发布2024-05-17 09:09:07
960
发布2024-05-17 09:09:07
举报
《Flutter TolyUI 框架》系列前言:

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

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

image.png
image.png

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


一、侧栏菜单设计思考

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

image.png
image.png

1. 封装、抽象与取舍

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

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

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


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

image.png
image.png

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


2. 侧栏菜单设计动机

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

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

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

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

01.gif
01.gif

3. 导航视图模块: tolyui_navigation

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

image.png
image.png

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

image.png
image.png

二、TolyRailMenuBar 的使用方式

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

组件/反馈组件/popover: toly1994.com/ui/#//widge…


1. TolyRailMenuBar 的基本样式

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

02.gif
02.gif

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

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

List<MenuMate> navMenus = const [
  MenuMate(icon: Icons.real_estate_agent_rounded, label: "开始使用", router: '/guide/start'),
  MenuMate(icon: Icons.account_tree, label: "模块树", router: '/guide/modules'),
  MenuMate(icon: Icons.privacy_tip, label: "设计原则", router: '/guide/principle'),
  MenuMate(icon: Icons.note_alt, 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 官网实现的相关源码。

代码语言:javascript
复制
void onSelected(String path) {
  setState(() {
    activeId = path;
  });
}

2. 菜单宽度类型

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

image.png
image.png

目前有两种类型,smalllarge:

代码语言:javascript
复制
enum MenuWidthType { small, large }

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

代码语言:javascript
复制
TolyRailMenuBar(
  width: 72,
  maxWidth: 240,
  widthTypeParser: (width) =>
    width > 150 ? MenuWidthType.large : MenuWidthType.small,
  /// 略同...

3. 动画参数的配置

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

03.gif
03.gif

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

image.png
image.png
代码语言:javascript
复制
TolyRailMenuBar(
  animationConfig: const AnimationConfig(
      duration: Duration(milliseconds: 500),
      curve: Curves.fastEaseInToSlowEaseOut,
      type: AnimTickType.hove,
    ),
);

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

image.png
image.png

三、TolyRailMenuBar 自定义菜单项

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


1. 如何自定义菜单项

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

image.png
image.png

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

05.gif
05.gif

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

代码语言:javascript
复制
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
image.png
07.gif
07.gif

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

代码语言:javascript
复制
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
06.gif

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

代码语言:javascript
复制
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
09.gif

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

代码语言:javascript
复制
dependencies:
  ...
  ## tolyui 导航模块
  tolyui_navigation: ^0.0.2

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

image.png
image.png

2. 自定义菜单项

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

image.png
image.png
代码语言:javascript
复制
final Tween<double> _widthTween = Tween(begin: 0.82, end: 0.95);
final Tween<double> _sizeTween = Tween(begin: 18.0, end: 22.0);
final Tween<double> _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> 菜单元数据:

代码语言:javascript
复制
late List<MenuMeta> 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.icon_layout, router: '/widget'),
    MenuMeta(label: canvas, icon: Icons.palette, router: '/painter'),
    MenuMeta(label: knowledge, icon: TolyIcon.icon_artifact, 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
image.png
代码语言:javascript
复制
@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 得到当前路由,取第一段作为激活路径:

代码语言:javascript
复制
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
image.png

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

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

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 《Flutter TolyUI 框架》系列前言:
  • 一、侧栏菜单设计思考
    • 1. 封装、抽象与取舍
      • 2. 侧栏菜单设计动机
        • 3. 导航视图模块: tolyui_navigation
        • 二、TolyRailMenuBar 的使用方式
          • 1. TolyRailMenuBar 的基本样式
            • 2. 菜单宽度类型
              • 3. 动画参数的配置
              • 三、TolyRailMenuBar 自定义菜单项
                • 1. 如何自定义菜单项
                  • 2. 如何自定义菜单项:仿哔哩哔哩
                    • 3. 自定义 TolyUI 默认样式
                    • 四、 TolyRailMenuBar 实践: FlutterUnit 侧栏导航
                      • 1. 迁移样式
                        • 2. 自定义菜单项
                          • 3. 使用 TolyRailMenuBar 组件
                            • 4. 尾声
                            相关产品与服务
                            容器服务
                            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档