Flutter TolyUI 框架#01 | 响应式布局#使用篇


theme: cyanosis

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


《Flutter TolyUI 框架》系列前言:

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

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

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


一、响应式布局理念和使用

作为一个支持全平台的 UI 界面框架,只要在桌面端和移动端打造应用程序,就注定需要面对一套代码,响应不同设备尺寸的功能需求。Flutter 官方没有一种比较完善的方案。但好在前端 Web 技术早在十几年前就已经为我们摸出了过河的石头,那就是 BootStrap 的栅格系统。目前流行的前端 UI 框架,如 ElementUI 、Ant Design 等,都采用了类似的栅格系统来适应不同尺寸的屏幕。

如何让 Flutter 支持栅格布局,完成响应式布局的需求,将是本文探讨的核心,也是 TolyUI 需要解决的首要问题。目前 tolyui 的响应式布局模块已经完成,可在 官网组件界面 查看介绍信息以及使用方式:

http://toly1994.com/ui/#/widgets/basic/layout

下面通过一个视频展示一下,TolyUI 为 Flutter 打造的响应式布局和栅格系统的功能:

jvideo


1. TolyUI 的响应式布局模块

为了更好的拆分 TolyUI 的职能,也为了开发者拥有更 细粒度 的选择。将相对独立的模块 单独分包 ,在通过一个包整合。拿响应式布局模块来说,它将作为 【tolyuirxlayout】 单独存在;也会作为 【tolyui】 的一部分。也就是说,使用者如果只想使用响应式布局,可以引入 tolyuirxlayout 包即可;想要使用全家桶,可以使用 tolyui 包。这种组件化的选择灵活性,是 TolyUI 的一大特性。

```

仅使用响应式布局

dependencies: tolyuirxlayout: ^last_version

使用 tolyui 全家桶

dependencies: tolyui: ^last_version ```

tolyui 借鉴 ElementUI 、Ant Design 等成熟的前端 UI 框架,将一个区域在横向划分为 24 格。在布局过程中,通过指定单元格的跨度来调节区域宽度:

响应式布局根据屏幕尺寸宽度,由小到大分为 xssmmdlgxl 五个阶层,我称之为 响应式尺阶 ,简称 尺阶


TolyUI 官网 界面正是基于此实现的响应式布局。拿 功能特性 条目展示来说来说:宽屏时可以展示四栏,也就是每个条目占据 4 个栅格:

随着窗口尺寸宽度的变化,内容可以自适应宽度。如下所示,每行两个条目或一个条目。原理是指定单元格占据的栅格个数,比如下面左图每个条目占 12 栅格,所以可以排两个;右侧每个条目占 24 栅格,所以只能排一个,以此类推:

| 两个条目 | 一个条目 | | --- | --- | | | |


2.单元格 cell 与跨度 span

栅格系统最基础的是在布局区域宽度缩放时,其中的单元格尺寸占比保持不变(如下图所示)。下面:

  • 每个色块区间被称为 Cell,可以指定跨度。
  • 若干色块横向排列,形成一行称之为 Row$ : 为了更好的语义,以及区分内置组件名。响应式组件命名中会以 $ 结尾。

在使用方面,引入 tolyui_rx_layout 后,通过 Row$ 组件展示一行,其中每个子区域对应一个 Cell 单元格。单元格可指定 span 表示跨度:

```dart import 'package:tolyuirx layout/tolyuirxlayout.dart';

Widget cellAndSpanExample() { const Color color1 = Color(0xffd3dce6); const Color color2 = Color(0xffe5e9f2); return Row$(cells: [ Cell(span: 5.rx, child: const Box(color: color1, text: '5')), Cell(span: 4.rx, child: const Box(color: color2, text: '4')), Cell(span: 9.rx, child: const Box(color: color1, text: '9')), Cell(span: 6.rx, child: const Box(color: color2, text: '6')), ]); } ```


3. 响应式参数: Cell#span

上面 Cell 的 span 赋值时,其后添加的 rx,可能大家会有所诧异。其实 Cell 中的 span 是 响应式的数字。确切来说是

基于响应尺寸创建数字的函数对象

其中拓展方法 rx 会返回一个函数,便于创建任何响应尺寸中都一致的数字。响应式布局的精髓在于:可以基于当前窗口尺寸,给出适应性的 span 数字。比如下面在窗口宽度缩小的过程中:

  • UI 格对应的 span 会逐阶减小,在最小阶尺寸时消失。
  • Toly 格会逐阶增大到 6、7 ,然后保持不变。

下面是我设计的调用方式,基于 Dart 模式匹配的新特性。可以通过 switch 来匹配五个尺阶 Rx 枚举,返会对应 span 的大小。其优势在于可以不多不少 全面枚举

```dart ---->[UI 单元格响应式设置]---- spanSecond(Rx r) => switch (r) { Rx.xs => 0, Rx.sm => 1, Rx.md => 2, Rx.lg => 3, Rx.xl => 4, };

Cell(span: spanSecond, child: const Box(color: color2, text: 'UI')), ```

通过 switch 匹配还有一点点其他的优势,可以基于匹配值进行逻辑运算。比如上面的逐阶递减,可以通过 4 - r.index 返回即可:

dart spanSecond(Rx r) => switch (r) { _ => 4 - r.index };


如果只想设置某几阶的响应值,在 switch 中可以通过 _ 提供其余的默认值。switch 关键字的模式匹配,简化了基于一个值,构建另一个值的过程。

```dart ---->[Toly 单元格响应式设置]---- spanFirst(Rx r) => switch (r) { Rx.lg => 6, Rx.xl => 5, _ => 7 };

Cell(span: spanFirst, child: const Box(color: color1, text: 'Toly')), ```


4. 响应式解析策略与自定义

其中五阶尺寸和前端响应式布局一致,通过 Rx 枚举表示。具体如下:

dart ---->[源码,使用者无需在意]---- enum Rx { xs, // (超小屏): sm, // (小屏幕): md, // (中屏幕): lg, // (大屏幕): xl, // (超大屏幕): }

在设计的过程中,我发现前端不同的 UI 框架对响应阶层的划分并不一致。为了使用者可以 更灵活 地使用响应式布局,这里将五阶的解析逻辑进行抽象,并提供默认的解析方式 defaultParserStrategy

dart ---->[源码,使用者无需在意]---- /// xs: [0,576) /// sm: [576,768) /// xs: [768,992) /// xs: [992,1200) /// xs: [1200,) Rx defaultParserStrategy(double width) { if (width < 576) return Rx.xs; if (width >= 576 && width < 768) return Rx.sm; if (width >= 768 && width < 992) return Rx.md; if (width >= 992 && width < 1200) return Rx.lg; return Rx.xl; }

如果你想要自定义五阶的解析范围,可以通过 ReParserStrategyTheme 主题进行设置。比如下面是 ElementUI 框架中响应式的解析逻辑,它限定的尺寸要更大一些: : 自定义解析主题是 非必须 的,不配置会有默认的解析逻辑。

dart Rx _elementUiRxParserStrategy(double width) { if (width < 768) return Rx.xs; if (width >= 768 && width < 992) return Rx.sm; if (width >= 992 && width < 1200) return Rx.md; if (width >= 1200 && width < 1920) return Rx.lg; return Rx.xl; }


二、 响应式间隔与对齐方式

响应式布局组件 Row$ ,在构造时可以传入其他参数控制单元格的排列信息。右如下五个属性:

| 名称 | 响应式类型 | 作用 | | --- | --- |--- | | gutter | double | 响应式水平间隔 | verticalGutter | double | 响应式竖直间隔 | padding | EdgeInsetsGeometry | 响应式内边距 | justify | RxAlign | 竖直方向对其方式 | align | RxJustify | 水平方向对其方式


1. 间隔与边距

Row$ 支持 24 栅格,如果单元格总长度大于 24 栅格,将会自动换行。如下图所示:

  • gutter 表示每个单元格的间距。
  • verticalGutter 表示换行后,竖直间距。
  • padding 表示四周的内边距。

这三个都是响应式值,可以通过函数指定不同尺阶对应的数值:

dart Widget gutterExample(){ const Color color1 = Color(0xffd3dce6); const Color color2 = Color(0xffe5e9f2); return Row$( gutter: 20.0.rx, verticalGutter: 12.0.rx, padding: const EdgeInsets.symmetric(horizontal: 40).rx, cells: [ Cell(span: 6.rx, child: const Box(color: color1, text: 'Toly')), Cell(span: 6.rx, child: const Box(color: color2, text: 'UI')), Cell(span: 6.rx, child: const Box(color: color1, text: 'Responsive')), Cell(span: 6.rx, child: const Box(color: color2, text: 'Layout')), Cell(span: 12.rx, child: const Box(color: color2, text: '12')), Cell(span: 6.rx, child: const Box(color: color2, text: '6')), Cell(span: 2.rx, child: const Box(color: color2, text: '2')), Cell(span: 4.rx, child: const Box(color: color2, text: '4')), ]); }


2. 水平方向对齐方式

在水平方向上,单元格有六种对齐方式,通过 justify 参数配置。它具有六种中元素,下图自上而下依次是 startendcenterspaceBetweenspaceAroundspaceEvenly

dart enum RxJustify { start, end, center, spaceBetween, spaceAround, spaceEvenly, }

dart Widget justifyExample(){ const Color color1 = Color(0xffd3dce6); const Color color2 = Color(0xffe5e9f2); return Column( children: RxJustify.values.map((e) => Row$( justify: e, padding: const EdgeInsets.symmetric(horizontal:12,vertical: 8).rx, cells: [ Cell(span: 4.rx, child: const Box(color: color1, text: 'Toly')), Cell(span: 2.rx, child: const Box(color: color2, text: 'UI')), Cell(span: 6.rx, child: const Box(color: color1, text: 'Responsive')), Cell(span: 6.rx, child: const Box(color: color2, text: 'Layout')), ])).toList(), ); }


3. 竖直方向对齐方式

在竖直方向上,单元格有三种对齐方式,通过 align 参数配置。它具有三种元素,下图自上而下依次是 topbottommiddle

dart enum RxAlign { top, bottom, middle, }

dart Widget alignExample(){ const Color color1 = Color(0xffd3dce6); const Color color2 = Color(0xffe5e9f2); return Column( children: RxAlign.values.map((e) => Row$( align: e, padding: const EdgeInsets.symmetric(horizontal:12,vertical: 8).rx, cells: [ Cell(span: 6.rx, child: const Box(color: color1, text: 'Toly')), Cell(span: 4.rx, child: const Box(color: color2, text: 'UI',height: 54,)), Cell(span: 8.rx, child: const Box(color: color1, text: 'Responsive',height: 72,)), Cell(span: 6.rx, child: const Box(color: color2, text: 'Layout')), ])).toList(), ); }


三、Cell 单元格其他响应式参数

上面是响应式布局 Row$ 的核心用法,在实际使用过程中。单元格 Cell 有其他的辅助参数便于操作和布局。

| 名称 | 响应式类型 | 作用 | | --- | --- |--- | | span | int | 单元格跨度 | offset | int | 偏移单元格数量 | push | int | 右移数量 | pull | int | 左移数量


1. offset 参数

offset 可以指定某个单元格左侧的偏移边距,单位是栅格宽度。如下所示,第三个单元格偏移 2 格,跨度为 7 :

下面通过

dart Widget cellOffsetExample() { const Color color1 = Color(0xffd3dce6); const Color color2 = Color(0xffe5e9f2); return Column( children: [ Row$(gutter: 20.0.rx, cells: [ Cell(span: 6.rx, child: const Box(color: color1)), Cell(span: 6.rx, offset: 6.rx, child: const Box(color: color2)), ]), const SizedBox(height: 12), Row$(gutter: 20.0.rx, cells: [ Cell(span: 6.rx, offset: 6.rx, child: const Box(color: color2)), Cell(span: 6.rx, offset: 6.rx, child: const Box(color: color2)), ]), const SizedBox(height: 12), Row$( gutter: 20.0.rx, cells: [ Cell(span: 12.rx, offset: 6.rx, child: const Box(color: color1)), ]), ], ); }


2.偏移 push 和 pull

和 offset 不同的是,pushpush 仅对对单元格进行平移,并不占据栅格空间。如下图所示,24 个栅格是相当于坐标系,push 作用是向右移动指定单位;pull 作用是向左移动指定单位。移动后单元格会发生局部覆盖行为:

dart Widget cellPushPullExample() { const Color color1 = Color(0xffd3dce6); const Color color2 = Color(0xffe5e9f2); return Column( children: [ Row$( gutter: 10.0.rx, cells: List.generate(24, (index) => Cell(span: 1.rx, child: Box2(color: color1, text: '${index + 1}'))).toList()), const SizedBox(height: 12), const SizedBox(height: 8), Row$(gutter: 10.0.rx, cells: [ Cell(span: 6.rx, child: const Box(color: color1, text: 'Toly')), Cell(span: 4.rx, push: 1.rx, child: const Box2(color: color2, text: 'push#1')), Cell(span: 8.rx, child: const Box(color: Color(0x660000ff), text: 'Responsive')), Cell(span: 6.rx, pull: 2.rx, child: const Box2(color: Color(0x99e5e9f2), text: 'pull#2')), ]), ], ); }


四、响应式布局构造器

Row$ 组件实现了栅格系统+响应式参数,但它并不是响应式布局的根本。为了满足更一般的响应式布局需求。我封装了 WindowRespondBuilder 组件,便于在任何界面逻辑中使用响应式布局。 在 Row$ 组件 的源码实现中,也是依赖于 WindowRespondBuilder 感知窗口当前尺阶的。


1. 整体布局结构中使用响应式布局

如下是组件的展示界面,在 sm 以上的三个尺阶中,宽度有足够的空间容纳侧面菜单栏:


当尺寸宽度不断变小时,感知到 sm、xs 尺阶后,可以将侧面菜单栏隐藏,并展示菜单按钮,点击展开菜单栏。以此实现响应式的整体布局结构。而在窗口尺寸变化时,感知尺阶数据的核心就是 WindowRespondBuilder

| sm | xs | | --- | --- | | | |

代码实现如下:通过 WindowRespondBuilder 感知 Rx 尺阶。并根据尺阶控制布局逻辑。比如只在尺阶索引小于 1 时展示 AppBar 及设置 drawer; 在尺阶大于 1 时,才通过 _buildMenuBar 在主体内容中展示菜单栏:

```dart Widget? _buildDrawer(Rx r){ if(r.index > 1) return null; return Material( child: _buildMenuBar(), ); }

PreferredSizeWidget? _buildAppBar(Rx r){ if(r.index > 1) return null; return AppBar( toolbarHeight: 56, leading: Builder( builder: (BuildContext context) { return IconButton( icon: const Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ); }, ), ); } ```

另外,联系与赞助界面,也是基于 WindowRespondBuilder 在宽屏时展示左右栏;窄平时收起,并通过按钮打开左右抽屉进行展示:


2. 响应式尺寸盒 SizedBox$

有时,我们希望一个区域能够感知 Rx 尺阶来设置长宽。如下所示,不同的尺阶中,灰色的区域尺寸会根据指定的长宽进行变化。以此适应各个尺阶中的展示需求。我基于 WindowRespondBuilder 提供了一个便于使用的 SizedBox$ 组件完成这一功能:

它有两个响应式参数 widthheight, 使用代码如下所示:

```dart class LayoutDemo5 extends StatelessWidget { const LayoutDemo5({super.key});

@override Widget build(BuildContext context) { return ColoredBox( color: const Color(0xffd3dce6), child: SizedBox$( child: const Center(child: Text("宽高根据屏幕尺寸变化的盒子")), width: (re) => switch (re) { Rx.xs => 200, Rx.sm => 200, Rx.md => 300, Rx.lg => 400, Rx.xl => 500, }, height: (re) => switch (re) { _ => 40.0 * (re.index + 1) })); } } ```


3. 响应式边距 Padding$

有时,在宽屏下希望边距打一些,窄屏中布局小一些。这就是响应式边距的需求。为了简单使用我也通过了一个 Padding$ 组件实现响应式边距的功能。

它有响应式参数 padding 设置内边距, 使用代码如下所示:

```dart class LayoutDemo6 extends StatelessWidget { const LayoutDemo6({super.key});

@override Widget build(BuildContext context) { return ColoredBox( color: const Color(0xffd3dce6), child: SizedBox( width: 300, height: 150, child: Padding$( child: Container( color: Colors.orange.withOpacity(0.6), alignment: Alignment.center, child: const Text("边距根据屏幕尺寸变化")), padding: (re) => switch (re) { Rx.xs => const EdgeInsets.symmetric(horizontal: 8, vertical: 6), Rx.sm => const EdgeInsets.symmetric(horizontal: 16, vertical: 12), Rx.md => const EdgeInsets.symmetric(horizontal: 24, vertical: 18), Rx.lg => const EdgeInsets.symmetric(horizontal: 32, vertical: 24), Rx.xl => const EdgeInsets.symmetric(horizontal: 40, vertical: 30), }), )); } } ```


这就是 TolyUI 为 Flutter 打造的响应式布局和栅格系统。感兴趣的朋友可以研究一下我写的源码,一共也不过 200 行代码,就可以实现如此丰富的功能。下一篇,将会带来对这个响应式布局的源码分析。包括在我实现过程中的思考、走的弯路、代码的优化等等中间历程。敬请期待~

相关推荐
helloxmg2 小时前
鸿蒙harmonyos next flutter通信之MethodChannel获取设备信息
flutter
helloxmg2 小时前
鸿蒙harmonyos next flutter混合开发之开发package
flutter·华为·harmonyos
lqj_本人1 天前
flutter_鸿蒙next_Dart基础②List
flutter
lqj_本人1 天前
flutter_鸿蒙next_Dart基础①字符串
flutter
The_tuber_sadness1 天前
【Flutter】- 基础语法
flutter
helloxmg1 天前
鸿蒙harmonyos next flutter通信之BasicMessageChannel获取app版本号
flutter
linpengteng2 天前
使用 Flutter 开发数字钱包应用(Dompet App)
前端·flutter·firebase
云兮Coder2 天前
鸿蒙 HarmonyNext 与 Flutter 的异同之处
flutter·华为·harmonyos