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 行代码,就可以实现如此丰富的功能。下一篇,将会带来对这个响应式布局的源码分析。包括在我实现过程中的思考、走的弯路、代码的优化等等中间历程。敬请期待~

相关推荐
江上清风山间明月12 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能1 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人1 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen1 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang1 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang1 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1231 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-1 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力2 天前
Flutter应用开发:对象存储管理图片
flutter