From 4674894d490b51ddc199344dc67fedbccfbbf3ed Mon Sep 17 00:00:00 2001 From: guoshuyu <359369982@qq.com> Date: Wed, 17 May 2023 14:34:31 +0800 Subject: [PATCH] update --- Dart-300.md | 266 ++++++++++++++ FWREADME.md | 4 + Flutter-310.md | 441 ++++++++++++++++++++++++ Flutter-310Win.md | 120 +++++++ Flutter-IOW.md | 122 +++++++ Flutter-N21.md | 347 +++++++++++++++++++ Flutter-N24.md | 860 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 6 + SUMMARY.md | 10 + UPDATE.md | 2 + 10 files changed, 2178 insertions(+) create mode 100644 Dart-300.md create mode 100644 Flutter-310.md create mode 100644 Flutter-310Win.md create mode 100644 Flutter-IOW.md create mode 100644 Flutter-N21.md create mode 100644 Flutter-N24.md diff --git a/Dart-300.md b/Dart-300.md new file mode 100644 index 0000000..ed27f63 --- /dev/null +++ b/Dart-300.md @@ -0,0 +1,266 @@ +# Google I/O 2023 - Dart 3 发布,快来看看有什么更新吧 + +> 核心原文链接: https://medium.com/dartlang/announcing-dart-3-53f065a10635 + +自从 Flutter Forword 发布了 [Dart 3α 预览](https://juejin.cn/post/7194741144482218045) 之后,大家对 Dart 3 的正式发布就一直翘首以待,这不仅仅是 Dart 版本号追上了 Flutter 版本号,更是 Dart 在 2.0 之后迎来的最大一次更新,主要包括了: + +- 100% 空安全 +- records +- patterns +- class modifiers +- Wasm 对 Web 的增加支持,可以预览 dart wasm native 了 + + + +# 100% 空安全支持 + +如下图所示,Dart 的 null safety 历经三年的时间,如今 Dart 终于有用了完善的类型系统,现在的 Dart 3 下,如果一个类型说一个值不是 `null`,那么它永远不可能是 `null` 。 + +![](http://img.cdn.guoshuyu.cn/20230511_D3/image1.png) + +> 说起来,还真有不少用户的项目没升级到 null safety ,这次就不能再等了。 + +另外,目前 pub.dev 上排名前 1000 的包中有 99% 支持空安全,所以官方预计升级到 Dart 3 的兼容问题并不大,少数情况下,Dart 3 中的对一些历史代码的相关清理可能会影响某些代码的运行,例如 + +- 一些旧的核心库 API 已被删除([#34233](https://github.com/dart-lang/sdk/issues/34233)、[#49529](https://github.com/dart-lang/sdk/issues/49529)) +- 一些工具已被调整([#50707](https://github.com/dart-lang/sdk/issues/50707))。 + +> 如果你在迁移到到 Dart 3 时遇到问题,可以查阅 https://dart.dev/resources/dart-3-migration + +# Record, patterns 和 class modifiers + +关于万众期待的 record 和 patterns 其实在之前的 [Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](https://juejin.cn/post/7194741144482218045)上已经有个详细解释,这里主要重新根据官方内容简诉一些这些变化。 + +## 使用 record 构建结构化数据 + +在此之前 Dart 函数只能返回一个值,如果需要返回多个值,必须将这些值打包成其他数据类型,例如 Map 或 List,或者定义可以保存这些值的新类。 + +使用非类型化数据结构削弱了类型安全性,而定义新类来传输数据会增加编码过程中的工作量,但是现在,通过 record 就可以简洁明地构建结构化数据: + +```dart +(String, int) userInfo(Map json) { + return (json['name'] as String, json['height'] as int); +} +``` + +在 Dart 中,record 是一个通用功能,它们不仅可以用于函数返回值,还可以将它们存储在变量中,例如将它们放入 List 中或者它们用作 Map 中的键,或创建包含其他 record 的 record。 + +另外还可以添加未命名字段,就像我们在前面的示例中所做的那样,也可以添加命名字段,例如 `(42, description: ‘Meaning of life’)` 。 + +record 是值类型,没有标识,这让编译器能够在某些情况下完全擦除记录对象,记录还带有自动定义的 `==` 运算符和 `hashCode` 函数。 + +> 详细可以参考官方文档:https://dart.dev/language/records 或者之前相关的中文资料: https://juejin.cn/post/7194741144482218045 + +## 使用具有 pattern 和 pattern 匹配的结构化数据 + +record 简化了构建结构化数据的方式,这不会取代使用类来构建正式的类型层次结构的方式,它只是提供了另一种选择。 + +在任何一种情况下,你可能希望将结构化数据分解为单独的元素,这就是 pattern 匹配发挥作用的地方。 + +考虑 pattern 的基本形式,以下记录 pattern 将 record 解构为两个新变量 `name` 和 `height` ,然后可以像任何其他变量一样使用这些变量: + +```dart +var (String name, int height) = userInfo({'name': 'Michael', 'height': 180}); +print('User $name is $height cm tall.'); +``` + +List 和 Map 存在类似的 pattern ,都可以使用下划线模式跳过单个元素: + +```dart +var (String name, _) = userInfo(…); +``` + +在 switch 语法中, Dart 3 扩展了语句 switch 的支持,现在支持在这些情况下进行 pattern 匹配: + +```dart +switch (charCode) { + case slash when nextCharCode == slash: + skipComment(); + + case slash || star || plus || minus: + operator(charCode); + + case >= digit0 && <= digit9: + number(); + + default: + invalid(); +} +``` + +还可以通过新的表达式进行微调,以下示例函数返回 switch 表达式的值以计算今天工作日的描述: + +```dart +String describeDate(DateTime dt) => + switch (dt.weekday) { + 1 => 'Feeling the Monday blues?', + 6 || 7 => 'Enjoy the weekend!', + _ => 'Hang in there.' + }; +``` + +**模式的一个强大功能是检查 “exhaustiveness” 的能力,此功能可确保 switch 处理所有可能的情况**。 + +在前面的示例中,我们正在处理工作日的所有可能值,这是一个`int` ,所以我们通过针对特定值 `1` 或 `6 `/`7` 的匹配语句的组合来穷尽所有可能的值,然后通过 `_` 对其余情况使用默认情况。 + +要对用户定义的数据层次结构(例如类层次结构)启用该能力,请在类层次结构的顶部使用 `sealed` 修饰符,如下例所示: + +```dart +sealed class Animal { … } +class Cow extends Animal { … } +class Sheep extends Animal { … } +class Pig extends Animal { … } + +String whatDoesItSay(Animal a) => + switch (a) { Cow c => ' $c says moo' , Sheep s => ' $s says baa' }; +``` + +这将返回以下错误,提醒我们错过了最后一个可能的子类型 Pig 的处理: + +``` +line 6 • The type 'Animal' is not exhaustively matched by the switch cases +since it doesn't match 'Pig()'. +``` + +最后,`if ` 语句也可以使用 pattern ,在下面的例子里,我们使用 *if-case* 匹配映射模式来解构 JSON 映射,这里匹配常量值(字符串如 `'name'` and `'Michael' `)和类型测试模式 `int h` 以读出 JSON 值,如果模式匹配失败,Dart 将执行该 `else` 语句。 + +```dart +final json = {'name': 'Michael', 'height': 180}; + +// Find Michael's height. +if (json case {'name': 'Michael', 'height': int h}) { + print('Michael is $h cm tall.'); +} else { + print('Error: json contains no height info for Michael!'); +} +``` + +> 详细可以参考官方文档:http://dart.dev/language/patterns 或者之前相关的中文资料: https://juejin.cn/post/7194741144482218045 + + + +# classes with class modifiers + +Dart 3 的第三个语言特性是类修饰符,与前两个支持不同的是,这更像是一个高级用户功能,它主要是为了满足了 Dart 开发人员制作大型 API 或构建企业级应用时的需求。 + +> 目前是基于 *constructed*、 *extended* 和 *implemented* 来实现处理,关键词有 + +类修饰符使 API 作者能够仅支持一些特定的功能,而默认值保持不变,例如:`abstract`、`base` 、`final`、`interface`、`sealed`、`mixin` 。 + +> 只有`base `修饰符可以出现在 mixin 声明之前,修饰符不适用于其他声明如 `enum`、`typedef`或 `extension`。 + +```dart +class Vehicle { + String make; String model; + void moveForward(int meters) { … } +} + +// Construct. +var myCar = Vehicle(make: 'Ford', model: 'T',); + +// Extend. +class Car extends Vehicle { + int passengers; +} + +// Implement. +class MockVehicle implements Vehicle { + @override void moveForward … +} +``` + +例如要强制继承类或 mixin 的实现,就可以使用 `base` 修饰符。 `base` 不允许在其自己的库之外实现,这保证: + +- 每当创建类的子类型的实例时,都会调用基类构造函数 +- 所有实现的私有成员都存在于子类型中 +- 类中新实现的成 员`base `不会破坏子类型,因为所有子类型都继承了新成员 + +```dart +// Library a.dart +base class Vehicle { + void moveForward(int meters) { ... } +} + + +// Library b.dart +import 'a.dart'; + +var myCar = Vehicle(); // Can be constructed + +base class Car extends Vehicle { // Can be extended + int passengers; + // ... +} + +base class MockVehicle implements Vehicle { // ERROR: Cannot be implemented + @override + void moveForward { ... } +} +``` + +如果要创建一组已知的、可枚举的子类型,就可以使用修饰符 `sealed` ,[sealed 允许在那些静态](https://dart.dev/language/branches#exhaustiveness-checking)子类型上创建一个 switch 。 + +```dart +sealed class Vehicle { ... } + +class Car extends Vehicle { } +class Truck implements Vehicle { } +class Bicycle extends Vehicle { } + +// ... + +var vehicle = Vehicle(); // ERROR: Cannot be instantiated +var vehicle = Car(); // Subclasses can be instantiated + +// ... + +// ERROR: The switch is missing the Bicycle subtype or a default case. +return switch (vehicle) { + Car() => 'vroom', + Truck() => 'VROOOOMM' +}; +``` + +类修饰符存在一些添加限制,例如: + +- 使用 `interface class` ,可以定义 contract 给其他人去实现,但不能扩展接口类。 +- 使用 `base class`,可以确保类的所有子类型都继承自它,而不是实现它的接口,这确保私有方法在所有实例上都可用。 +- 使用 `final class`,可以关闭类型层次结构,以防止自己的库之外的任何子类。这样的好处是允许 API 所有者添加新成员,而不会出现破坏 API 使用者更改的风险。 + +> 是不是没看明白?有关详细信息,可以参考 https://dart.dev/language/class-modifiers + +# 展望未来 + +Dart 3 不仅仅是是在这些新功能上向前迈出了重要的一步,还为大家提供了下一步的预览。 + +## Dart language + +Records, patterns 和 class modifiers 是非常庞大的新功能,因此它们的某些设计可能还需要改进,所以接下来还会有一些更小、更增量的功能更新,这些功能完全不会中断,并且专注于在没有迁移成本的情况下提高开发人员的工作效率。 + +目前正在探索的还有 [primary constructors](https://github.com/dart-lang/language/issues/2364) 和 [inline classes](https://github.com/dart-lang/language/issues/2727) 包装,另外之前讨论过的宏(也称为[元编程](https://github.com/dart-lang/language/blob/main/working/macros/feature-specification.md))也在进行探索,因为元编程的规模和固有风险,目前正在采取一种更有效和彻底的方法进行探索,因此没有具体的时间表可以分享,即使是最终确定的设计决策。 + +## native interop + +移动和桌面上的应用通常依赖于 native 平台提供的大量 API,无论是通知、支付还是获取手机位置等。 + +在之前 Flutter 中,这些是通过构建插件来访问的,这需要为 API 编写 Dart 代码和一堆特定于平台的代码来提供实现。 + +目前已经支持与使用 `dart:ffi` 直接和原生语言进行交互,我们目前正在努力扩展它在Android 上的支持,再次之前可以看 [Java 和 Kotlin interop ](https://dart.dev/guides/libraries/java-interop) 以及 [Objective-C 和 Swift interop](https://juejin.cn/post/7137874832988831751) 。 + +> 请查看新的 Google I/O 23 的 [Android interop 视频](https://io.google/2023/program/2f02692d-9a41-49c0-8786-1a22b7155628/)。 + +## 编译为 WebAssembly——使用 native 代码定位 web + +[WebAssembly (缩写为 Wasm)作为跨](https://webassembly.org/)[所有浏览器的](https://caniuse.com/wasm)平台的二进制指令格式,其可用性度一直在增长,Flutter 框架使用 Wasm 有一段时间了,这就是我们如何通过 Wasm 编译模块将用 C++ 编写的 SKIA 图形渲染引擎交付给浏览器的实现。 + +Flutter 也一直对使用 Wasm 来部署 Dart 代码很感兴趣,但是在此之前该实现被阻止了,与许多其他面向对象的语言一样,因为 Dart 需要使用垃圾回收。 + +在过去的一年里,Flutter 和 Wasm 生态系统中的多个团队合作,将新的 WasmGC 功能添加到 WebAssembly 标准中,目前在 Chromium 和 Firefox 浏览器中已经接近稳定。 + +将 Dart 编译为 Wasm 模块的工作有两个针对 Web 的高级目标: + +- **加载时间:**我们希望我们可以使用 Wasm 交付部署有效负载,使浏览器可以更快地加载,从而缩短到达用户可以与 Web 交互的时间。 +- **性能:**由 JavaScript 提供支持的 Web 应用需要即时编译才能获得良好的性能,Wasm 模块更底层,更接近机器代码,因此我们认为它们可以提供更高的性能、更少的卡顿和更一致的帧率。 +- **语义一致性**:Dart 在我们支持的平台之间保持高度一致而自豪。但是,在 web 上有一些例外情况,例如 Dart web 目前在[数字表示](https://dart.dev/guides/language/numbers)方式上有所不同,而使用 Wasm 模块,我们可以将 web 视为具有与其他原生目标相似语义的“原生”平台。 + +**跟随 Dart3 的发布, Dart 到 Wasm 编译的第一个预览也一起发布**,这是最初的 Flutter Web 重点支持。虽然现在还早,后续还有很多工作要完成,但已经可以通过 https://flutter.dev/wasm 开始测试。 \ No newline at end of file diff --git a/FWREADME.md b/FWREADME.md index dd2f115..7c8fef6 100644 --- a/FWREADME.md +++ b/FWREADME.md @@ -76,6 +76,10 @@ * [ Flutter 小技巧之 3.7 更灵活的编译变量支持](Flutter-N19.md) * [面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯”](Flutter-GPT.md) * [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md) +* [Flutter 小技巧之霓虹灯文本的「故障」效果的实现](Flutter-N21.md) +* [Flutter 小技巧之横竖列表的自适应大小布局支持](Flutter-N24.md) +* [Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC](Flutter-IOW.md) +* [Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher](Flutter-310Win.md) diff --git a/Flutter-310.md b/Flutter-310.md new file mode 100644 index 0000000..68dd67b --- /dev/null +++ b/Flutter-310.md @@ -0,0 +1,441 @@ +# Google I/O 2023 - Flutter 3.10 发布,快来看看有什么更新吧 + + + +> 核心部分原文链接:https://medium.com/flutter/whats-new-in-flutter-3-10-b21db2c38c73 + + + +虽然本次 I/O 的核心 keynote 主要是 AI ,但是按照惯例依然发布了新的 Flutter 稳定版,不过并非大家猜测的 4.0,而是 3.10 ,Flutter 的版本号依然那么的出人意料。 + +**Flutter 3.10 主要包括有对 Web、mobile、graphics、安全性等方面的相关改进**,核心其实就是: + +- iOS 默认使用了 Impeller +- 一堆新的 Material 3 控件袭来 +- iOS 新能优化,Android 顺带可有可无的更新 +- Web 可以无 iframe 嵌套到其他应用 + +# Framework + + + +## Material 3 + +看起来谷歌对于 Material 3 的设计规范很上心,根据最新的 [Material Design spec](https://m3.material.io/components) 规范 Flutter 也跟进了相关的修改,其中包括有**新组件和组件主题和新的视觉效果等**。 + +目前依然是由开发者可以在 `MaterialApp` 主题配置下,通过 `useMaterial3` 标志位选择是否使用 Material 3,**不过从下一个稳定版本开始,`useMaterial3` 默认会被调整为 `true`** 。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image1.png) + +> 对于 Material 3 ,可以通过 https://flutter.github.io/samples/material_3.html 上的相关 Demo 预览。 + +## ColorScheme.fromImageProvider + +所有 M3 组件配置主题的默认颜色 `ColorScheme`,**默认配色方案使用紫色 shades,这有区别于之前默认的蓝色**。 + +除了可以从单一 “seed” 颜色来定制配置方案之后,通过 `fromImageProvider` 图像也可以创建自定义配色方案。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image2.gif) + +## NavigationBar + +本次还增加了一个 M3 版本的 `BottomNavigationBar` 控件效果,虽然 [M3](https://m3.material.io/components/navigation-bar/overview) 使用不同的颜色、highlighting 和 elevation,但它的工作方式其实还是和以前一样。 + +如果需要调整 `NavigationBars` 的默认外观,可以使用使用 `NavigationBarTheme` 来覆盖修改,虽然目前你不需要将现有 App 迁移到 `NavigationBars` ,但是官方建议还是尽可能在新项目里使用 `NavigationBars` 作为导航控件。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image3.gif) + + + +## NavigationDrawer + +M3 针对 Drawer 同样提供了新的 `NavigationDrawer `,它通过 `NavigationDestinations` 显示单选列表,也可以在该列表中包含其他控件。 + +> 同步M3下 `Drawer` 也更新了颜色和高度,同时对布局进行了一些小的更改。 + +`NavigationDrawer` 需要时可以滚动,如果要覆盖 `NavigationDrawer` 的默认外观,同样可以使用 `NavigationDrawerTheme` 来覆盖。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image4.gif) + + + +## SearchBar 和 SearchAnchor + +这是 Flutter 为搜索查询和提供预测效果新增的控件。 + +当用户在输入搜索查询时,会在 “search view” 中计算匹得到一个配响应列表,用户选择一个结果或调整匹配结果。 + +如果要覆盖 `SearchBarTheme` 的默认外观,同样可以使用 `SearchAnchorTheme` 来覆盖。 + +| ![](http://img.cdn.guoshuyu.cn/20230511_F3/image5.gif) | ![](http://img.cdn.guoshuyu.cn/20230511_F3/image6.gif) | +| ------------------------------------------------------ | ------------------------------------------------------ | + + + + +## Secondary Tab Bar + +M3 下 Flutter 现在默认提供创建第二层选项卡式内容的支持,针对二级 Tab 可以使用 `TabBar.secondary`。 + + + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image7.gif) + +## DatePicker 和 TimePicker 更新 + +M3下 `DatePicker `更新了控件的日历、文本字段的颜色、布局和形状等,对应 API 没有变动,但会个新增了 `DatePickerTheme` 用于调整控件样式。 + +`TimePicker` 和`DatePicker` 一样,更新了控件的常规版本和紧凑版本的颜色、布局和形状。 + +| ![](http://img.cdn.guoshuyu.cn/20230511_F3/image8.gif) | ![](http://img.cdn.guoshuyu.cn/20230511_F3/image9.gif) | +| ------------------------------------------------------ | ------------------------------------------------------ | + + + +## BottomSheet 更新 + + M3 下 `BottomSheet` 除了颜色和形状更新之外,还添加了一个可选的拖动手柄,当设置 `showDragHandle`为 `true` 时生效。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image10.gif) + + + +## ListTile 更新 + +M3下 `ListTile` 更新了定位和间距,包括 content padding、leading 和 trailing 控件的对齐、minimum leading width, 和 vertical spacing 等,但是 API 保持不变。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image11.gif) + + + +# TextField 更新 + +M3 更新了所有 `TextField` 对原生手势支持。 + +用鼠标双击或三次点击 `TextField` 和在触摸设备上双击或三次点击效果相同,默认情况下 `TextField` 和`CupertinoTextField ` 都可以使用该功能。 + +### `TextField` double click/tap 手势 + +- Double click + drag:扩展字块中的选择。 +- Double tap + drag:扩展字块中的选择。 + + + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image12.gif) + +### `TextField` triple click/tap 手势 + +Triple click + +- 在多行 `TextField`(Android/Fuchsia/iOS/macOS/Windows) 中选择点击位置的段落块。 +- 在多行 `TextField` (Linux) 内部时,在 click 位置选择一个行块。 +- 选择单行中的所有文本 `TextField`。 + +Triple tap + +- 在 multi-line `TextField` 中选择点击位置的段落块 。 +- 选择单行 `TextField` 中的所有文本 + +Triple click+拖动 + +- 扩展段落块中的选择 (Android/Fuchsia/iOS/macOS/Windows)。 +- 扩展行块中的选择 (Linux)。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image13.gif) + + + +> 简单来说,就是手势和鼠标在双击和三击下,会触发不同的选择效果,并且 Linux 在三击效果下会有点差异 + + + +## Flutter 支持 SLSA 级别 1 + +Flutter Framework 现在使用软件工件供应链级别 ( [SLSA](https://slsa.dev/) ) 级别 1 进行编译,这里面支持了许多安全功能的实现,包括: + +- **脚本化构建过程**:Flutter 的构建脚本现在允许在受信任的构建平台上自动构建,建立在受保护的架构上有助于防止工件篡改,从而提高供应链安全性。 +- **带有审计日志的多方批准**:Flutter 发布工作流程仅在多个工程师批准后执行,所有执行都会创建可审计的日志记录,这些更改确保没有人可以在源代码和工件生成之间引入更改。 +- **出处**:Beta 和稳定版本现在使用 [provenance](https://slsa.dev/provenance/v0.1) 构建,这意味着具有预期内容的可信来源构建了框架发布工件,每个版本都会发布链接以查看和验证 [SDK 存档](https://docs.flutter.dev/release/archive) 的出处。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image14.png) + +这项工作还在朝着 SLSA L2 和 L3 合规性迈进,这两个级别侧重于在构建过程中和构建之后提供 artifacts 保护。 + + + +# Web + + + +## 改进了加载时间 + +3.10 减小了图标字体的文件大小,它会从 Material 和 Cupertino 中删除了未使用的字形,从而提供了更快加载。 + +## CanvasKit 变小 + +基于 Chromium 的浏览器可以使用更小的自定义 CanvasKit 渠道,托管在 Google [gstatic.com ](http://gstatic.com/) 上的 CanvasKit 可以进一步提高性能。 + +## Element 嵌入 + +现在可以 [从页面中的特定 Element 来加载 Flutter Web](https://docs.flutter.dev/deployment/web#embedding-a-flutter-app-into-an-html-page) ,不需要 `iframe`,在这个版本之前 fluter web 是需要填充整个页面主体或显示在 `iframe` 标记内,简单说就是把 flutter web 嵌套到其他 Web 下更方便了。 + +> 具体 Demo 可见:https://github.com/flutter/samples/tree/main/web_embedding + +## **着色器支持** + +Web 应用可以使用 Flutter 的 [fragment shader](https://docs.flutter.dev/development/ui/advanced/shaders) : + +```yaml +flutter: + shaders: + - shaders/myshader.frag +``` + + + +# Engine + +## Impeller + +在 3.7 稳定版中 iOS 提供了 [Impeller ](https://docs.flutter.dev/perf/impeller) 预览支持,从那时起 Impeller 就收到并解决了用户的大量反馈。 + +在 3.10 版本中,我们对 Impeller 进行了 250 多次提交,**现在我们将 Impeller 设置为 iOS 上的默认渲染器**。 + +默认情况下,所有使用 Flutter 3.10 为 iOS 构建的应用都使用 Impeller,这样 iOS 应用预计将会有更少的卡顿和更一致的性能。 + +自 3.7 版本以来,iOS 上的 Impeller 改进了内存占用,可以使用较少的渲染通道和中间渲染目标。 + +在较新的 iPhone 上,**启用有损纹理压缩可在不影响保真度的情况下减少内存占用,这些进步也显着提高了 iPad 的性能**。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image15.png) + +比如 [Wonderous](https://flutter.gskinner.com/wonderous/) 应用中的 “pull quote” 页面,**这些改进是的当前页面下的内存占用量减少了近一半**。 + +内存使用量的减少也适度降低了 GPU 和 CPU 负载,Wondrous 应用可能不会记录这些负载下降,它的框架之前已经优化的不错,但这一变化应该会延长续航能力。 + +Impeller 还释放了团队可以更快地交付流行功能请求的能力,例如在 iOS 上支持更广泛的 P3 色域。 + +> 社区贡献加速了我们的进步,特别是 GitHub 用 户[ColdPaleLight](https://github.com/ColdPaleLight) 和 [luckysmg](https://github.com/luckysmg ) ,他们编写了多个与 Impeller 相关的补丁,提高了保真度和性能。 + +虽然 Impeller 满足大多数 Flutter 应用的渲染需求,但你可以选择关闭 Impeller。如果选择退出,请考虑在[ GitHub 上提交问题](https://github.com/flutter/flutter/issues/new/choose)以告诉我们原因。 + +> ``` +> FLTEnableImpeller +> +> ``` + +用户可能会注意到 Skia 和 Impeller 在渲染时存在细微差别,这些差异可能是错误,所以请勿在 Github 上提出问题,**在未来的版本中,我们将删除适用于 iOS 的旧版 Skia 渲染器以减小 Flutter 的大小**。 + +另外,Impeller 的 Vulkan 后端然在支持当中,Android 上的 Impeller 仍在积极开发中,但尚未准备好进行预览。 + +> 要了解 Impeller 进展,请查看 https://github.com/orgs/flutter/projects/21。 + +# Performance + +3.10 版本涵盖了除 Impeller 之外还有更多性能改进和修复。 + +## 消除卡顿 + +这里要感谢 [luckysmg](https://github.com/luckysmg), 他们发现可以缩短从 Metal 驱动获取下一个可绘制层的时间,而方式就是需要将 `FlutterViews` 背景颜色设置为非零值。 + +此更改消除了最近 iOS 120Hz 显示器上的低帧率问题,**在某些情况下它会使帧速率增加三倍**,这帮助我们解决了六个 GitHub issue。 + +**这一变化具有意义重大,以至于我们向后移植了一个修补程序到 3.7 版本中**。 + +在 3.7 稳定版中,我们将本地图像的加载从平台线程转移到 Dart 线程,以避免延迟来自平台线程的 vsync 事件。但是[用户](https://github.com/flutter/flutter/issues/121525)注意到 Dart 线程上的这项额外工作也导致了一些卡顿。 + +在 3.10 中,**我们将本地图像的打开和解码从 Dart 线程移至[后台线程](https://github.com/flutter/engine/pull/39918)**,这个更改消除了具有大量本地图像的屏幕上潜在的长时间停顿,同时避免了延迟 vsync 事件,在我们的本地测试和自动化基准测试中,这个更改将多个同步图像的加载时间缩短了一半。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image16.png) + + + +我们继续在 Flutter 新的内部 DisplayList 结构之上构建优化,在 3.10 中,我们添加了 [R-Tree based culling](https://github.com/flutter/engine/pull/38429) 机制。 + +这种机制在我们的渲染器中更早地移除了绘制操作的处理。[例如](https://github.com/flutter/flutter/issues/92366) 优化加速了输出在屏幕外失败的自定义painter。 + +我们的 [microbenchmarks](https://flutter-engine-perf.skia.org/e/?begin=1671661938&end=1671754421&keys=X789f7ff76f30f8ccc672464f335fe09b&num_commits=50&request_type=1&xbaroffset=31974) 显示 DisplayList 处理时间最多减少了 50%,具有裁剪自定义绘画的 App 可能会看到不同效果的改进,改进的程度取决于隐藏绘制操作的复杂性和数量。 + +## 减少 iOS 启动延迟 + +之前应用中标识符查找的[低效策略](https://github.com/flutter/flutter/issues/37826)增加了应用启动延迟,这个启动延迟的增长与应用的大小成正比。 + +而在 3.10 中,[我们修复了 bundle identifier lookup](https://github.com/flutter/engine/pull/39975),这将大型应用的启动延迟减少了 100 毫秒或大约 30–50%。 + +## 缩小尺寸 + +Flutter 使用 `SkParagraph` 作为文本、布局和渲染的默认库,之前我们包括了一个标志以支持回退到遗留 `libtxt`和 `minikin` 。 + +由于我们对 `SkParagraph` 有充分的信心,[我们](https://github.com/flutter/engine/pull/39499)在 3.10 中删除了 `libtxt` 和 `minikin` 以及它们的标志,这将 Flutter 的压缩大小减少了 30KB。 + +> 看起来信心十足了。 + +## 稳定性 + +在 3.0 版本中,我们在渲染管道后期启用了一项 Android 功能,该功能使用高级 GPU 驱动,当只有一个“dirty” 区域发生变化时,这些驱动功能会重新绘制较少的屏幕内容。 + +我们之前已经将它添加到早期的优化中以达到类似的效果,尽管我们的基准测试结果不错,但还是出现了两个问题: + +- 首先,改进最多的基准可能不代表实际用例。 +- 其次,[事实证明很难找到](https://github.com/flutter/engine/pull/37493)支持此 GPU 驱动功能的设备和 Android 版本集 + +鉴于有限的进步和支持,我们在 Android 上[禁用了](https://github.com/flutter/engine/pull/40898)部分重绘功能。 + +而使用 Skia 后端时,该功能在 iOS 上依然保持启用状态,我们希望在未来的版本中可以[通过 Impeller 启用它。](https://github.com/flutter/flutter/issues/124526) + + + +# API 改进 + +## APNG解码器 + +Flutter 3.10 [解决了一个我们最受关注的问题](https://github.com/flutter/flutter/issues/37247),它增加了 `APNG` 解码图像的[能力](https://github.com/flutter/engine/pull/31098),现在可以使用 Flutter 现有的图片加载 API 来加载 `APNG` 图片。 + +## 图片加载 API 改进 + +3.10 添加了一个[新方法](https://master-api.flutter.dev/flutter/dart-ui/instantiateImageCodecWithSize.html) `instantiateImageCodecWithSize`,该方法满足以下三个条件的[用例支持:](https://github.com/flutter/flutter/issues/118543) + +- 加载时宽高比未知 +- 边界框约束 +- 原始纵横比约束 + +# Mobile + +## iOS + +### 无线调试 + +**现在可以在无线的情况下运行和热重新加载的 Flutter iOS 应用**。 + +在 Xcode 中成功无线配对 iOS 设备后,就可以使用 flutter run 将应用部署到该设备,**如果遇到问题,请在 Window > Devices** 和 **Simulators > Devices**下验证网络图标是否出现在设备旁边。 + +> 要了解更多信息,可以查阅 https://docs.flutter.dev/get-started/install/macos#ios-setup。 + +### 宽色域图像支持 + +iOS 上的 Flutter 应用现在可以支持宽色域图像的精确渲染,要使用宽色域支持,应用必须使用 Impeller 并在 `Info.plist` 文件添加 `FLTEnableWideGamut ` 标志。 + +### 拼写检查支持 + + `SpellCheckConfiguration()` 控件现在默认支持 [Apple](https://developer.apple.com/documentation/uikit/uitextchecker) 在 iOS 上的拼写检查服务,可以使用 `spellCheckConfiguration` 中的参数对其进行设置 `CupertinoTextField` 。 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image17.gif) + + + +### 自适应复选框和单选 + +3.10 将 `CupertinoCheckBox` 和 `CupertinoRadio` 添加到库中 `Cupertino` ,他们创建符合 Apple 样式的复选框和单选按钮组件。 + +Material 复选框和单选控件添加了 `.adaptive` 构造函数,在 iOS 和 macOS 上,这些构造函数使用相应的 Cupertino 控件,在其他平台上使用 Material 控件。 + +### 优化 Cupertino 动画、过渡和颜色 + +Flutter 3.10 改进了一些动画、过渡和颜色以匹配 SwiftUI,这些改进包括: + +- [更新](https://github.com/flutter/flutter/pull/122275) `CupertinoPageRoute` +- [添加](https://github.com/flutter/flutter/pull/110127)标题放大动画 `CupertinoSliverNavigationBar` +- 添加几种[新的 iOS 系统颜色 ](https://github.com/flutter/flutter/pull/118971)`CupertinoColors` + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image18.gif) + + + +### PlatformView 性能 + +当 `PlatformViews `出现在屏幕上时,Flutter会限制 iOS 上的[刷新率以减少卡顿](https://github.com/flutter/engine/pull/39172),当应用显示动画或可滚动时,用户可能会在应用出现 `PlatformViews` 时注意到这一点。 + +### macOS 和 iOS 可以在插件中使用共享代码 + +Flutter 现在支持插件 `pubspec.yaml` 文件中的 `sharedDarwinSource` ,这个 key 表示 Flutter 应该共享 iOS 和 macOS 代码。 + +``` +ios: + pluginClass: PathProviderPlugin + dartPluginClass: PathProviderFoundation + sharedDarwinSource: true +macos: + pluginClass: PathProviderPlugin + dartPluginClass: PathProviderFoundation + sharedDarwinSource: true +``` + +### 应用扩展的新资源 + +我们为 Flutter 开发人员添加了使用 iOS 应用扩展文档,这些扩展包括实时活动、主屏幕控件和共享扩展。 + +为了简化创建主屏幕控件和共享数据,我们向 `path_provider` 和 `homescreen_widget` 插件添加了新方法。 + +> 具体可见:https://docs.flutter.dev/development/platform-integration/ios/app-extensions + +### 跨平台设计的新资源 + +该文档现在包括针对特定 [UI 组件](https://docs.flutter.dev/resources/platform-adaptations#ui-components)的跨平台设计注意事项,要了解有关这些 UI 组件的更多信息,请查看Flutter UX GitHub 存储库中的讨论: https://github.com/flutter/uxr/discussions + +> 具体可见:https://docs.flutter.dev/resources/platform-adaptations#ui-components + + + +## Android + +### Android CameraX 支持 + +[Camera X ](https://developer.android.com/training/camerax)是一个 Jetpack 库,可简化向 Android 应用添加丰富的相机功能。 + +该功能适用于多种 Android 相机硬件,在 3.10 中,我们为 Flutter Camera 插件添加了对 CameraX 的初步支持,此支持涵盖以下用例: + +- 图像捕捉 +- 视频录制 +- 显示实时相机预览 + +``` +Dependencies: + camera: ^0.10.4 # 最新相机版本 + camera_android_camerax: ^0.5.0 +``` + + + +# 开发者工具 + +我们继续改进了 DevTools,这是一套用于 Dart 和 Flutter 的性能和调试工具,一些亮点包括: + +- DevTools UI 使用 Material 3,这让外观现代化又增强了可访问性。 +- DevTools 控制台支持在调试模式下评估正在运行的应用,在 3.10 之前,只能在暂停应用时执行此操作。 +- 嵌入式 [Perfetto 跟踪查看器](https://perfetto.dev/)取代了以前的时间线跟踪查看器。 + +Perfetto 可以处理更大的数据集,并且比传统的跟踪查看器表现得更好,例如: + +- 允许固定感兴趣的线程 +- 单击并拖动以从多个帧中选择多个时间轴事件 +- 使用 SQL 查询从时间轴事件中提取特定数据 + +![](http://img.cdn.guoshuyu.cn/20230511_F3/image19.png) + +# 弃用和重大更改 + +## 弃用的 API + +3.10 中的重大更改包括在 v3.7 发布后过期的弃用 API。 + +要查看所有受影响的 API 以及其他上下文和迁移指南,请查看[之前版本的弃用指南](https://docs.flutter.dev/release/breaking-changes/3-7-deprecations)。 + +> [Dart Fix ](https://docs.flutter.dev/development/tools/flutter-fix)可以修复其中的许多问题,包括在 IDE 中快速修复和使用`dart fix`命令批量应用。 + +## Android Studio Flamingo 升级 + +将 Android Studio 升级到 Flamingo 后,你可能会在尝试 `flutter run` 或 `flutter build` Flutter Android 应用时看到错误。 + +[发生此错误是因为 Android Studio Flamingo 将其捆绑的 Java SDK 从 11 更新到 17,](https://docs.gradle.org/current/userguide/compatibility.html#java)使用 Java 17 时,之前的 7.3 Gradle 版本无法运行。 + +我们[更新](https://github.com/flutter/flutter/pull/123916)来了 `flutter analyze --suggestions` 以验证是否由于 Java SDK 和 Gradle 版本之间的不兼容而发生此错误。 + +> 要了解修复此错误的不同方法,请查看我们的迁移指南:https://docs.flutter.dev/go/android-java-gradle-error。 + +## Window singleton 弃用 + +改版本弃用了 Window singleton,依赖它的应用和库需要开始[迁移](https://docs.flutter.dev/release/breaking-changes/window-singleton)。 + +当你的应用在未来版本的 Flutter 中做支持时,这会可以为你的应用提前做好多窗口准备支持。 + +> PS:还可以关注下本次 I/O 基于 Flutter 发布的新小游戏:[I/O FLIP 小游戏](https://juejin.cn/post/7231378331139997757) \ No newline at end of file diff --git a/Flutter-310Win.md b/Flutter-310Win.md new file mode 100644 index 0000000..5ad8514 --- /dev/null +++ b/Flutter-310Win.md @@ -0,0 +1,120 @@ +# Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher + +Flutter 3.10 发布之后,大家可能注意到,在它的 [release note](https://juejin.cn/post/7231565908631633979#heading-46) 里提了一句: **Window singleton 相关将被弃用,并且这个改动是为了支持未来多窗口的相关实现**。 + +> 所以这是一个为了支持多窗口的相关改进,多窗口更多是在 PC 场景下更常见,但是又需要兼容 Mobile 场景,故而有此次改动作为提前铺垫。 + +如下图所示,如果具体到对应的 API 场景,主要就是涉及 `WidgetsBinding.instance.window` 和 `MediaQueryData.fromWindow` 等接口的适配,因为 `WidgetsBinding.instance.window` 即将被弃用。 + +![](http://img.cdn.guoshuyu.cn/20230517_310/image1.png) + +> 你可以不适配,还能跑,只是升级的技术债务往后累计而已。 + +那首先可能就有一个疑问,为什么会有需要直接使用 `WidgetsBinding.instance.window` 的使用场景?简单来说,具体可以总结为: + +- 没有 `BuildContext` ,不想引入 `BuildContext` +- 不希望获取到的 ` MediaQueryData` 受到所在 `BuildContext` 的影响,例如键盘弹起导致 padding 变化重构和受到 `Scaffold` 下的参数影响等 + +> 这部分详细可见:[《MediaQuery 和 build 优化你不知道的秘密》 ](https://juejin.cn/post/7114098725600903175)。 + +那么从 3.10 开始,针对 `WidgetsBinding.instance.window` 可以通过新的 API 方式进行兼容: + +- 如果存在 `BuildContex` , 可以通过 `View.of` 获取 `FlutterView`,这是官方最推荐的替代方式 +- 如果没有 `BuildContex` 可以通过 `PlatformDispatcher` 的 `views` 对象去获取 + +这里注意到没有,现在用的是 `View.of` ,获取的是 `FlutterView` ,对象都称呼为 View 而不是 「Window」,对应的 `MediaQueryData.fromWindow` API 也被弃用,修改为 `MediaQueryData.fromView` ,这个修改的依据在于: + +> 起初 Flutter 假定了它只支持一个 Window 的场景,所以会有 `SingletonFlutterWindow` 这样的 instance window 对象存在,同时 `window` 属性又提供了许多和窗口本身无关的功能,在多窗口逻辑下会显得很另类。 + +那么接下来就让我们用「长篇大论」来简单介绍下这两个场景的特别之处。 + +# 存在 BuildContext + +回归到本次的调整,首先是存在 BuildContext 的场景,如下代码所示,对于存在 `BuildContex` 的场景, `View.of` 相关的调整为: + +```dart +/// 3.10 之前 +double dpr = WidgetsBinding.instance.window.devicePixelRatio; +Locale locale = WidgetsBinding.instance.window.locale; +double width = + MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width; + + +/// 3.10 之后 +double dpr = View.of(context).devicePixelRatio; +Locale locale = View.of(context).platformDispatcher.locale; +double width = + MediaQueryData.fromView(View.of(context)).size.width; + +``` + +可以看到,这里的 `View` 内部实现肯定是有一个 `InheritedWidget` ,它将 `FlutterView` 通过 `BuildContext` 往下共享,从而提供类似 「window」 的参数能力,而通过 `View.of` 获取的参数: + +- **当 `FlutterView` 本身的属性值发生变化时,是不会通知绑定的 `context` 更新,这个行为类似于之前的 ` WidgetsBinding.instance.window`** +- 只有当 `FlutterView` 本身发生变化时,比如 `context` 绘制到不同的 `FlutterView` 时,才会触发对应绑定的 `context` 更新 + +可以看到 `View.of` 这个行为考虑的是「多 `FlutterView`」 下的更新场景,如果是需要绑定到具体对应参数的变动更新,如 `size` 等,还是要通过以前的 `MediaQuery.of` / `MediaQuery.maybeOf` 来实现。 + +而对于 `View` 来说,**每个 `FlutterView` 都必须是独立且唯一的**,在一个 Widget Tree 里,一个 `FlutterView` 只能和一个 `View` 相关联,这个主要体现在 `FlutterView` 标识 `GlobalObjectKey` 的实现上。 + +![](http://img.cdn.guoshuyu.cn/20230517_310/image2.png) + +简单总结一下:**在存在 `BuildContex` 的场景,可以简单将 `WidgetsBinding.instance.window` 替换为 `View.of(context)` ,不用担心绑定了 `context` 导致重构,因为 `View.of` 只对 `FlutterView` 切换的场景生效**。 + +# 不存在 BuildContext + +对于不存在或者不方便使用 `BuildContext` 的场景,官方提供了 `PlatformDispatcher.views` API 来进行支持,不过因为 `get views` 对应的是 `Map` 的 `values` ,它是一个 `Iterable` 对象,**那么对于 3.10 我们需要如何使用 `PlatformDispatcher.views` 来适配没有 `BuildContext` 的 `WidgetsBinding.instance.window` 场面**? + +![](http://img.cdn.guoshuyu.cn/20230517_310/image3.png) + +> `PlatformDispatcher` 内部的` views` 维护了中所有可用 `FlutterView` 的列表,用于提供在没有 `BuildContext` 的情况下访问视图的支持。 + +你说什么情况下会有没有 `BuildContext` ?比如 Flutter 里 的 `runApp` ,如下图所示,3.10 目前在 `runApp` 时会通过 `platformDispatcher.implicitView` 来塞进去一个默认的 `FlutterView` 。 + +![](http://img.cdn.guoshuyu.cn/20230517_310/image4.png) + +`implicitView` 又是什么?其实 `implicitView` 就是 `PlatformDispatcher._views` 里 id 为 0 的 `FlutterView` ,默认也是 `views` 这个 `Iterable` 里的 `first` 对象。 + +![](http://img.cdn.guoshuyu.cn/20230517_310/image5.png) + +也就是在没有 `BuildContext` 的场景, 可以通过 `platformDispatcher.views.first` 的实现迁移对应的 `instance.window` 实现。 + +```dart +/// 3.10 之前 +MediaQueryData.fromWindow(WidgetsBinding.instance.window) +/// 3.10 之后 +MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.first) +``` + +为什么不直接使用 `implicitView` 对象? 因为 `implicitView` 目前是一个过渡性方案,官方希望在多视图的场景下不应该始终存在 implicit view 的概念,而是应用自己应该主动请求创建一个窗口,去提供一个视图进行绘制。 + +![](http://img.cdn.guoshuyu.cn/20230517_310/image6.png) + +所以对于 `implicitView` 目前官方提供了 `_implicitViewEnabled` 函数,后续可以通过可配置位来控制引擎是否支持 `implicitView` ,也就是 **`implicitView` 在后续更新随时可能为 null ,这也是我们不应该在外部去使用它的理由**,同时它是在 `runApp` 时配置的,所以它在应用启动运行后永远不会改变,如果它在启动时为空,则它永远都会是 null。 + +> `PlatformDispatcher.instance.views[0]` 在之前的单视图场景中,无论是否有窗口存在,类似的 `implicitView` 会始终存在;而在多窗口场景下,`PlatformDispatcher.instance.views` 将会跟随窗口变化。 + +另外我们是通过 `WidgetsBinding.instance.platformDispatcher.views` 去访问 `views` ,而不是直接通过 `PlatformDispatcher.instance.views` ,因为通常官方更建议在 Binding 的依赖关系下去访问 `PlatformDispatcher` 。 + +> 除了需要在 `runApp()` 或者 `ensureInitialized()` 之前访问 PlatformDispatcher 的场景。 + +另外,如下图所示,通过 Engine 里对于 window 部分代码的实现,可以看到我们所需的默认` FlutterView` 是 id 为 0 的相关依据,所以这也是我们通过 `WidgetsBinding.instance.platformDispatcher.views` 去兼容支持的逻辑所在。 + +| ![](http://img.cdn.guoshuyu.cn/20230517_310/image7.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image8.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image9.png) | ![](http://img.cdn.guoshuyu.cn/20230517_310/image10.png) | +| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------- | + + + +# 最后 + +最后总结一下,说了那么多,其实不外乎就是将 `WidgetsBinding.instance.window` 替换为 `View.of(context)` ,如果还有一些骚操作场景,可以使用 `WidgetsBinding.instance.platformDispatcher.views` ,如果不怕后续又坑,甚至可以直接使用 `WidgetsBinding.instance.platformDispatcher.implicitView` 。 + +整体上解释那么多,**主要还是给大家对这次变动有一个背景认知,同时也对未来多窗口实现进展有进一步的了解**,相信下一个版本多窗口应该就可以和大家见面了。 + +更多讨论可见: + +- https://github.com/flutter/flutter/issues/120306 +- https://github.com/flutter/engine/pull/39553 +- https://github.com/flutter/flutter/issues/116929 +- https://github.com/flutter/flutter/issues/99500 +- https://github.com/flutter/engine/pull/39788 \ No newline at end of file diff --git a/Flutter-IOW.md b/Flutter-IOW.md new file mode 100644 index 0000000..ddf22c1 --- /dev/null +++ b/Flutter-IOW.md @@ -0,0 +1,122 @@ +# Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC + +随着 [Flutter 3.10 发布](https://juejin.cn/post/7231565908631633979),Flutter Web 也引来了它最具有「里程碑」意义的更新,**这里的「里程碑」不是说这次 Flutter Web 有多么重大的更新,而是 Flutter 官方对于 Web 终于有了明确的定位和方向**。 + +# 提升 + +首先我们简单聊提升,这不是本篇的重点,只是顺带。 + +本次提升主要在于两个大点:**Element 嵌入支持和 fragment shaders 支持 **。 + +首先是 Element 嵌入,**Flutter 3.10 开始,现在可以将 Flutter Web嵌入到网页的任何 HTML 元素中,并带有 `flutter.js` 引擎和 `hostElement` 初始化参数**。 + +简单来说就是不需要 `iframe` 了,如下代码所示,只需要通过 `initializeEngine ` 的 `hostElement` 参数就可以指定嵌入的元素,**灵活度支持得到了提高**。 + +```html + + + + + + + + +
Loading...
+ + + + +``` + +> PS :如果你的项目是在 Flutter 2.10 或更早版本中创建的,要先从目录中删除 `/web` 文件 ,然后通过 `flutter create . --platforms=web` 重新创建模版。 + +fragment shaders 部分一般情况下大家可能并不会用到,shaders 就是以 `.frag` 扩展名出现的 GLSL 文件,在 Flutter 里是在 `pubspec.yaml` 文件下的 `shaders` 中声明,现在它支持 Web 了: + +```yaml +flutter: + shaders: + - shaders/myshader.frag +``` + +> 一般运行时会把 frag 文件加载到 `FragmentProgram ` 对象中,通过 program 可以获取到对应的 `shader `,然后通过 `Paint.shader` 进行使用绘制, 当然 Flutter 里 shaders 文件是存在限制的,比如不支持 UBO 和 SSBO 等。 + +**当然,这里不是讲解 shaders ,而是宣告一下,Flutter Web 支持 shaders 了**。 + +# 未来 + +**其实未来才是本篇的重点**,我们知道 Flutter 在 Web 领域的支持上一直在「妥协」,Flutter Web 在整个 Flutter 体系下一直处于比较特殊的位置,因为它一直存在两种渲染方式:[html 和 canvaskit](https://juejin.cn/post/7095294020900880420)。 + +简单说 html 就是转化为 [JS + Html Element](https://juejin.cn/post/7095294020900880420) 渲染,而 canvaskit 是采用 [Skia + WebAssembly](https://skia.org/docs/user/modules/canvaskit/) 的方式,**而 html 的模式让 Web 在 Flutter 中显得「格格不入」,路径依赖和维护成本也一直是 Flutter Web 的头痛问题**。 + +![](http://img.cdn.guoshuyu.cn/20230512_IOW/image1.png) + +面对这个困境,官方在年初的 [Flutter Forword](https://juejin.cn/post/7192646390948823098) 大会上提出重新规划 Flutter Web 的未来,而随着 Flutter 3.10 的发布,官方终于对于 Web 的未来有了明确的定位: + +> **“Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架。”** + +Flutter 团队表示,**Flutter Web 的定位不是设计为通用 Web 的框架**,类似的 Web 框架现在有很多,比如 Angular 和 React 等在这个领域表现就很出色,而 Flutter 应该是围绕 CanvasKit 和 [WebAssembly](https://webassembly.org/) 等新技术进行架构设计的框架。 + +所以 Flutter Web 未来的路线更多会是 CanvasKit ,也就是 WebAssembly + Skia ,同时在这个领域 Dart 也在持续深耕:**从 Dart 3 开始,对于 Web 的支持将逐步演进为 WebAssembly 的 Dart native 的定位**。 + +![](http://img.cdn.guoshuyu.cn/20230512_IOW/image2.png) + +什么是 WebAssembly 的 dart native ?一直以来 Flutter 对于 WebAssembly 的支持都是:使用 Wasm 来处理CanvasKit 的 runtime,而 Dart 代码会被编译为 JS,而这对于 Dart 团队来时,其实是一个「妥协」的过渡期。 + +而随着官方与 WebAssembly 生态系统中的多个团队的深入合作,**Dart 已经开始支持直接编译为原生的 wasm 代码,一个叫 [WasmGC]((https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md)) 的垃圾收集实现被引入标准**,该扩展实现目前在基于 Chromium 的浏览器和 Firefox 浏览器中在趋向稳定。 + +> 目前在基准测试中,执行速度提高了 3 倍 + +要将 Dart 和 Flutter 编译成 Wasm,你需要一个支持 [WasmGC ](https://github.com/WebAssembly/gc/tree/main/proposals/gc) 的浏览器,目前 [Chromium V8](https://chromestatus.com/feature/6062715726462976) 和 Firefox 团队的浏览器都在进行支持,比如 Chromium 下: + +> 通过结构和数组类型为 WebAssembly 增加了对高级语言的有效支持,以 Wasm 为 target 的语言编译器能够与主机 VM 中的垃圾收集器集成。在 Chrome 中启用该功能意味着启用类型化函数引用,它会将函数引用存储在上述结构和数组中。 + +![](http://img.cdn.guoshuyu.cn/20230512_IOW/image3.png) + +现在在 Flutter master 分支下就可以提前尝试 wasm 的支持,运行 `flutter build web --help` 如果出现下图所示场, 说明支持 wasm 编译。 + +![](http://img.cdn.guoshuyu.cn/20230512_IOW/image4.png) + +之后执行 `flutter build web --wasm` 就可以编译一个带有 native dart wasm 的 web 包,命令执行后,会将产物输出到 `build/web_wasm` 目录下。 + +之后你可以使用 pub 上的 [`dhttpd`](https://pub.dev/packages/dhttpd) 包在 `build/web_wasm`目录下执行本地服务,然后在浏览器预览效果。 + +``` +> cd build/web_wasm +> dhttpd +Server started on port 8080 +``` + +目前需要版本 112 或更高版本的 Chromium 才能支持,同时需要启动对应的 Chrome 标识位: + +- `enable-experimental-webassembly-stack-switching` +- `enable-webassembly-garbage-collection` + +![](http://img.cdn.guoshuyu.cn/20230512_IOW/image5.png) + +当然,目前阶段还存在一些限制,例如: + +> Dart Wasm 编译器利用了 [ JavaScript-Promise Integration (JSPI) ](https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md)特性,Firefox 不支持 JSPI 提议,所以一旦 Dart 从 JSPI 迁移出来,Firefox 应启用适当的标志位才能运行。 + +另外还需要 JS-interop 支持,因为为了支持 Wasm,Dart 改变了它针对浏览器和 JavaScript 的 API 支持方式, 这种转变是为了防止把 `dart:html ` 或 `package:js` 编译为 Wasm 的 Dart 代码,大多数特定于平台的包如 url_launcher 会使用这些库。 + +![](http://img.cdn.guoshuyu.cn/20230512_IOW/image6.png) + +最后,**目前 DevTools 还不支持 `flutter run` 去运行和调试 Wasm**。 + +# 最后 + +很高兴能看到 Flutter 团队最终去定了 Web 的未来路线,这让 Web 的未来更加明朗,当然,正如前面所说的,**Flutter 是第一个围绕 CanvasKit 和 WebAssembly 等新兴 Web 技术进行架构设计的框架**。 + +**所以 Flutter Web不是为了设计为通用 Web 的框架去 Angular 和 React 等竞争,它是让你在使用 Flutter 的时候,可以将能力很好地释放到 Web 领域**,而 CanvasKit 带来的一致性更符合 Flutter Web 的定位,当然,解决加载时长问题会是任重道远的需求。 \ No newline at end of file diff --git a/Flutter-N21.md b/Flutter-N21.md new file mode 100644 index 0000000..e36b676 --- /dev/null +++ b/Flutter-N21.md @@ -0,0 +1,347 @@ +# Flutter 小技巧之霓虹灯文本的「故障」效果的实现 + +如下图所示,最近通过群友的问题在 [codepen.io ](https://codepen.io/mattgrosswork/pen/VwprebG) 上看到了一个文本「抽动」的动画实现,看起来就像是生活中常见的「霓虹灯招牌」故障时的「抽动」效果,而本篇的目标通过「抄袭」这个实现,帮助大家理解 Flutter 里的一些实现小技巧。 + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image1.gif) + +这个效果在 codepen 上是通过 CSS 实现的,实现思路 codepen 上的 [Glitch Walkthrough](https://codepen.io/mattgrosswork/pen/VwprebG) 大致有提示,但是 Flutter 没有强大的 CSS,那么如何将它「复刻」到 Flutter 上就是本篇的核心要点。 + +> 不得不说 CSS 很强大,要在 Flutter 上实现类似的效果还是比较「折腾」。 + +而要在 Flutter 上实现类似 Glitch Walkthrough 的效果,大致上我们需要处理: + +- 类似霓虹灯效果的文本 +- 文本内容撕裂的效果 +- 文本变形闪动的效果 + +那么接下来我们就按照这个流程来实现一个 Flutter 上的 Glitch Walkthrough 。 + +# 霓虹灯文本 + +这一步其实相对简单,Flutter 的 `TextStyle` 提供了 `shadows` 配置,通过它可以快速实现一个「会发光」的文本。 + +我们这里通过两个 `Shadow` 来实现「发光」的视觉效果,核心就是利用 `Shadow` 的 `blurRadius` 来让背景出现一定程度的模糊发散,然后两个 `Shadow` 形成不一样的颜色深度和发散效果,从而达到看起来「发亮」的效果。 + +> 如下图是没有填充文本颜色时 `Shadow` 的效果。 + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image2.png) + +最后,如下代码所示,我们只需要通过 `foreground` 给文本补充下颜色,就可以看到如下图所示的类似「霓虹灯」效果的文本。 + +> 当然这里你不想用 `foreground` ,只用简单的 `color` 也可以。 + +```dart +Text( + widget.text, + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + foreground: Paint() + ..style = PaintingStyle.fill + ..strokeWidth = 5 + ..color = Colors.white, + shadows: [ + Shadow( + blurRadius: 10, + color: Colors.white, + offset: Offset(0, 0), + ), + Shadow( + blurRadius: 20, + color: Colors.white30, + offset: Offset(0, 0), + ), + ], + ), +) +``` + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image3.png) + +这里提个题外话,其实类似的思路用在图片上也可以实现「发光」的效果,如下代码所示,通过 Stack 嵌套两个 `Image` ,然后中间通过 `BackdropFilter` 的 `ImageFilter` 做一层模糊,让底下的图片模糊后发散产生类似「发光」的效果。 + +```dart + var child = Image.asset( + 'static/test_logo.png', + width: 250, + ); + return Stack( + children: [ + child, + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: blurRadius, + sigmaY: blurRadius, + ), + child: Container(color: Colors.transparent), + ), + ), + child, + ], + ) + ); +``` + +如下图所示,图片最终可以通过自己的色彩产生类似「发光」的效果,当然这部分只是额外的拓展内容,和我们要实现的效果无关。 + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image4.png) + + + + + +# 文本撕裂 + +这部分可以说是需求效果的核心,这里我们需要用到 `ClipPath` 和 `Polygon` ,通过 `Polygon` 来实现随机的多边形路径,然后利用 `ClipPath` 对文本内容进行随机的路径裁剪。 + +虽然说用 `Polygon` , 但是 Flutter 官方并没有直接提供类似前端 CSS 的 `Polygon` 多边形 API 支持,但是社区总有「好心人」,我们可以直接使用 Flutter 上类似的第三方库: `polygon: ^0.1.0` 。 + +>简单说 `Polygon` 就是按照 step 对 `Path` 的 `moveTo` 和 `quadraticBezierTo` 等 API 进行了封装。 + +Flutter 上的 `Polygon` 取值范围是 -1 ~ 1 ,也就是按照比例决定位置,比如 - 1 就是起始点, 1 就是最大宽高, 更具体如下面的代码所示,这里利用 `Polygon` 添加了三个点,最终这三个点形成的 Path 会绘制出一个三角形。 + +```dart +List generatePoint() { + List points = []; + points.add(Offset(-1, -1)); + points.add(Offset(-1, 0)); + points.add(Offset(0, -1)); + return points; +} +``` + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image5.png) + +如下代码所示,那如果如果 point 的数量多了,就可以形成一系列不规则的形状,比如下面代码随机添加了 60 个点的位置,可以看到此时屏幕上的白色 `Container` 被裁剪成「凌乱」的形状。 + +```dart +List generatePoint() { + List points = []; + + points.add(Offset(-1.00, -0.76)); + points.add(Offset(0.06, -0.76)); + points.add(Offset(0.06, -0.48)); + points.add(Offset(-0.50, -0.48)); + points.add(Offset(-0.50, 0.72)); + points.add(Offset(-0.38, 0.72)); + points.add(Offset(-0.38, -1.00)); + points.add(Offset(0.06, -1.00)); + points.add(Offset(0.06, 0.67)); + points.add(Offset(0.84, 0.67)); + points.add(Offset(0.84, 0.63)); + points.add(Offset(0.39, 0.63)); + points.add(Offset(0.39, -0.42)); + points.add(Offset(0.56, -0.42)); + points.add(Offset(0.56, 0.30)); + points.add(Offset(0.37, 0.30)); + points.add(Offset(0.37, 0.32)); + points.add(Offset(0.54, 0.32)); + points.add(Offset(0.54, -0.09)); + points.add(Offset(0.70, -0.09)); + points.add(Offset(0.70, -0.48)); + points.add(Offset(0.94, -0.48)); + points.add(Offset(0.94, -0.43)); + points.add(Offset(0.67, -0.43)); + points.add(Offset(0.67, -0.31)); + points.add(Offset(0.08, -0.31)); + points.add(Offset(0.08, 0.78)); + points.add(Offset(-0.40, 0.78)); + points.add(Offset(-0.40, 0.15)); + points.add(Offset(0.65, 0.15)); + points.add(Offset(0.65, 0.00)); + points.add(Offset(0.36, 0.00)); + points.add(Offset(0.36, -0.28)); + points.add(Offset(0.24, -0.28)); + points.add(Offset(0.24, -0.80)); + points.add(Offset(-0.76, -0.80)); + points.add(Offset(-0.76, -0.31)); + points.add(Offset(0.19, -0.31)); + points.add(Offset(0.19, 0.13)); + points.add(Offset(0.96, 0.13)); + points.add(Offset(0.96, 0.65)); + points.add(Offset(-0.80, 0.65)); + points.add(Offset(-0.80, 0.06)); + points.add(Offset(0.82, 0.06)); + points.add(Offset(0.82, 0.67)); + points.add(Offset(0.60, 0.67)); + points.add(Offset(0.60, 0.65)); + points.add(Offset(-0.19, 0.65)); + return points; +} +``` + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image6.png) + +如果这时候把白色 `Container` 换成文本内容,那么我们就可以如下图所示的效果,看起来像不像一帧状态下文本的「错乱」效果?后面我们只需要每次生成一帧这样的 Path ,就可以实现文本动态「撕裂」的需求。 + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image7.png) + +> 我们只需要把这个实现做成随机输出,然后每次生成一个 `Path` 就可以了。 + +如下代码所示,我们通过 `generatePoint` 方法,每次随机生成 60 个点,然后将这些点通过 `computePath` 转化为 Path,然后继承 `CustomClipper` 配置到 `getClip` 方法里,在需要的时候(`tear` )对 child 按 Path 进行裁剪。 + +> 注意这里的 `i % 2` ,为的是让上次的 x 或者 y 可以是同一个位置,在连接上能连续。 + +```dart +class RandomTearingClipper extends CustomClipper { + bool tear; + + RandomTearingClipper(this.tear); + + List generatePoint() { + List points = []; + var x = -1.0; + var y = -1.0; + for (var i = 0; i < 60; i++) { + if (i % 2 != 0) { + x = Random().nextDouble() * (Random().nextBool() ? -1 : 1); + } else { + y = Random().nextDouble() * (Random().nextBool() ? -1 : 1); + } + points.add(Offset(x, y)); + } + return points; + } + + @override + Path getClip(Size size) { + var points = generatePoint(); + var polygon = Polygon(points); + if (tear) + return polygon.computePath(rect: Offset.zero & size); + else + return Path()..addRect(Offset.zero & size); + } + + @override + bool shouldReclip(RandomTearingClipper oldClipper) => true; +} +``` + +接着,我们只需要设置一个定期器,然后将前面的「霓虹灯文本」和「故障裁剪效果」配置到 `ClipPath` 上,如下图所示,我们就可以看到文本的随机撕裂效果。 + +```dart +timer = Timer.periodic(Duration(milliseconds: 400), (timer) { + tearFunction(); +}); + +return ClipPath( + child: Center( + child: Text( + widget.text, + style: TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + foreground: Paint() + ..style = PaintingStyle.fill + ..strokeWidth = 1 + ..color = Colors.white, + shadows: [ + Shadow( + blurRadius: 10, + color: Colors.white, + offset: Offset(0, 0), + ), + Shadow( + blurRadius: 20, + color: Colors.white30, + offset: Offset(0, 0), + ), + ], + ), + ), + ), + clipper: RandomTearingClipper(tear), + ); +``` + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image8.gif) + +> 此时看起来还不够形象。 + +# 变形闪动 + +为了达到我们预期的效果,最后我们还需要做一些特殊处理,比如再实现两个形状、颜色和位置不一样「霓虹灯文本」,为的就是实现「变形和闪动」的效果替换。 + +比如如下代码所示,通过 `ShaderMask` 可以实现一个渐变效果的的文本,这是用来在闪动的时候,提供一个短暂替换和色彩加深的作用。 + +```dart +ShaderMask( + blendMode: BlendMode.srcATop, + shaderCallback: (bounds) { + return LinearGradient( + colors: [Colors.blue, Colors.green, Colors.red], + stops: [0.0, 0.5, 1.0], + ).createShader(bounds); + }, + child: +``` + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image9.png) + +类似的我们还可以实现一个「变形」的文本,在之前的白色「霓虹灯」文本基础上增加「斜体」和「颜色变淡」等处理,用来闪动的时候提供「变形」的作用。 + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image10.png) + +最后我们再将之前的 ` ClipPath`添加到它们上面,并增加一个 `transform` 实现文本四周随意移动的效果支持,如下图所示,此时的效果已经肉眼可见的接近我们的需求。 + +```dart +transform: + Matrix4.translationValues(randomPosition(4), randomPosition(4), 0), + +double randomPosition(position) { + return Random().nextInt(position).toDouble() * + (Random().nextBool() ? -1 : 1); +} +``` + +| ![](http://img.cdn.guoshuyu.cn/20230322_N21/image11.gif) | ![](http://img.cdn.guoshuyu.cn/20230322_N21/image12.gif) | +| -------------------------------------------------------- | -------------------------------------------------------- | + +最后我们将这几个文本效果用 `Stack` 组合起来,然后再在定时器里不停去切换「故障」和「正常」的文本状态,并且随机选择展示不同的 「故障」状态。 + +```dart +timer = Timer.periodic(Duration(milliseconds: 400), (timer) { + tearFunction(); +}); +timer2 = Timer.periodic(Duration(milliseconds: 600), (timer) { + tearFunction(); +}); + +tearFunction() { + count++; + tear = count % 2 == 0; + if (tear == true) { + setState(() {}); + Future.delayed(Duration(milliseconds: 150), () { + setState(() { + tear = false; + }); + }); + } +} + +@override +Widget build(BuildContext context) { + var status = Random().nextInt(3); + return Stack( + children: [ + if (tear && (status == 1)) renderTearText1(RandomTearingClipper(tear)), + if (!tear || (tear && status != 2)) + renderMainText(RandomTearingClipper(tear)), + if (tear && status == 2) renderTearText2(RandomTearingClipper(tear)), + ], + ); +} +``` + +最终效果如下图所示,这里还额外对后面两个文本做了一个 `ClipRect` 处理,闪动切换的时候只展示部分内容,这样在「故障」时的切换不会显得太过生硬,可以看到简单的 CSS 效果在 Flutter 上的实现成本其实并不低。 + +![](http://img.cdn.guoshuyu.cn/20230322_N21/image13.gif) + +当然,这里的实现没考虑性能问题,所以代码也比较糙,不过这里主要是为了展示了 `ClipPath `和 `Shadow` 的使用技巧,相信通过这个例子,可以帮助大家更好地发掘 Flutter 里对于路径绘制和阴影的使用场景,这才是本篇的主要目的。 + +那么本篇小技巧到这里就结束了,如果你还有什么想说的,欢迎留言评论。 + +> 完整代码可见:https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/tear_text_demo_page.dart \ No newline at end of file diff --git a/Flutter-N24.md b/Flutter-N24.md new file mode 100644 index 0000000..96690e6 --- /dev/null +++ b/Flutter-N24.md @@ -0,0 +1,860 @@ +# Flutter 小技巧之横竖列表的自适应大小布局支持 + +今天这个主题看着是不是有点抽象?又是列表嵌套?之前不是分享过[《 ListView 和 PageView 的各种花式嵌套》](https://juejin.cn/post/7116267156655833102)了么?那这次的自适应大小布局支持有什么不同? + +> 算是某些奇特的场景下才会需要。 + +首先我们看下面这段代码,基本逻辑就是:我们希望 `vertical` 的 `ListView` 里每个 Item 都是根据内容自适应大小,并且 Item 会存在有 `horizontal` 的 `ListView` 这样的 child。 + +`horizontal` 的 `ListView` 我们也希望它能够根据自己的 `children` 去自适应大小。**那么你觉得这段代码有什么问题?它能正常运行吗?** + +```dart +@override +Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: new Text(""), + ), + extendBody: true, + body: Container( + color: Colors.white, + child: ListView( + children: [ + ListView( + scrollDirection: Axis.horizontal, + children: List.generate(50, (index) { + return Padding( + padding: EdgeInsets.all(2), + child: Container( + color: Colors.blue, + child: Text(List.generate( + math.Random().nextInt(10), (index) => "TEST\n") + .toString()), + ), + ); + }), + ), + Container( + height: 1000, + color: Colors.green, + ), + ], + ), + ), + ); +} +``` + +答案是不能,因为这段代码里 `vertical` 的 `ListView` 嵌套了 `horizontal` 的 `ListView` ,而横向的 `ListView` 并没有指定高度,并且垂直方向的 `ListView` 也没有指定 `itemExtent` ,所以我们会得到如下图所示的错误: + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image1.png) + +为什么会有这样的问题,简单说一下,我们都知道 Flutter 是从上往下传递约束,从上往上返回 `Size` 的一个布局过程,也就是需要 child 通过通过 parent 的约束来决定自己的大小,然后 parent 根据 child 返回的 `Size` 决定自己的尺寸。 + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image2.png) + +> 对这部分感兴趣的可以看 [《带你了解不一样的 Flutter》](https://juejin.cn/post/7053777774707736613) + +但是对于可滑动控件来说有点特殊,因为可滑动控件在其滑动方向的主轴上,理论是需要「无限大」的,所以对于可滑动控件来说,就需要有一个「窗口」的固定大小,也就是 `ViewPort` 这个「窗口」需要有一个主轴方向的大小。 + +比如 `ListView` ,一般情况下就是有一个 `ViewPort` ,然后内部的 `SliverList` 构建一个列表,然后通过手势在 `ViewPort` 「窗口」下相应产生移动,从而达到列表滑动的效果。 + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image3.png) + +> 如果感兴趣可以看 [《不一样角度带你了解 Flutter 中的滑动列表实现》](https://juejin.cn/post/6956215495440007175) + +那么我们再回到上面 `vertical` 的 `ListView` 嵌套 `horizontal` 的 `ListView ` 的问题: + +- 因为垂直的 `ListView` 没有设置 `itemExtent` ,所以它的每个 child 不会有一个固定高度,因为我们的需求是每个 Item 根据自己的需要自适应高度。 +- 横向的 `ListView` 没有设置明确高度,作为 parent 的垂直 `ListView` 高度理论又是「无限高」,所以横向的 `ListView` 无法计算得到一个有效的高度。 + +另外,由于 `ListView` 不像 `Row`/`Column `等控件,它拥有的 `children` 理论也是「无限」的,并且没有展示的部分一般是不会布局和绘制,所以不能像 `Row`/`Column ` 一样计算出所有控件的高度之后,来决定自身的高度。 + +那么破解的方式有哪些呢?目前情况下可以提供两种解决方式。 + +# SingleChildScrollView + +如下代码所示,首先最简单的就是把横向的 `ListView` 替换成 `SingleChildScrollView` ,因为不同于 `ListView` , `SingleChildScrollView` 只有一个 child ,所以它的 `ViewPort` 也比较特殊。 + +```dart +return Scaffold( + appBar: AppBar( + title: new Text("ControllerDemoPage"), + ), + extendBody: true, + body: Container( + color: Colors.white, + child: ListView( + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(50, (index) { + return Padding( + padding: EdgeInsets.all(2), + child: Container( + color: Colors.blue, + child: Text(List.generate( + math.Random().nextInt(10), (index) => "TEST\n") + .toString()), + ), + ); + }), + ), + ), + Container( + height: 1000, + color: Colors.green, + ), + ], + ), + ), +) +``` + +在 `SingleChildScrollView` 的 `_RenderSingleChildViewport` 里,布局时可以很简单的通过 `child!.layout` 之后得到 child 的大小,然后配合 `Row` 就计算出所有 child 的综合高度,这样可以实现横向的列表效果。 + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image4.png) + +运行之后结果入下图所示,可以看到此时在垂直的 `ListView `里,横向的 `SingleChildScrollView` 被正确渲染出来,但是此时出现「参差不齐」的高度布局。 + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image5.png) + +如下代码所示,这时候我们只需要在 `Row` 嵌套一个 `IntrinsicHeight` ,就可以让其内部高度对齐,因为 `IntrinsicHeight` 在布局时会提前调用 child 的 `getMaxIntrinsicHeight` 获取 child 的高度,修改 parent 传递给 child 的约束信息。 + +```dart +SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: IntrinsicHeight( + child: Row( + children: List.generate(50, (index) { + return Padding( + padding: EdgeInsets.all(2), + child: Container( + alignment: Alignment.bottomCenter, + color: Colors.blue, + child: Text(List.generate( + math.Random().nextInt(10), (index) => "TEST\n") + .toString()), + ), + ); + }), + ), + ), +), +``` + +运行效果如下所示,可以看到此时所有横向 Item 的高度都一致,但是这个解决方法也有两个比较致命的问题: + +- `SingleChildScrollView` 里是通过 `Row` 计算的高度,也就是布局时会需要一次性计算所有 child ,如果列表太长就会产生性能损耗 +- `IntrinsicHeight` 推算布局的过程会比较费时,可能会到 O(N²),虽然 Flutter 里针对这部分计算结果做了缓存,但是不妨碍它的耗时。 + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image6.png) + + + +# UnboundedListView + +第二个解决思路就是基于 `ListView` 去自定义,前面我们不是说 `ListView` 不会像 `Row` 那样去统计 children 的大小么?那我们完全可以自定义一个 `UnboundedListView` 来统计。 + +> 这部分思路最早来自 Github :https://gist.github.com/vejmartin/b8df4c94587bdad63f5b4ff111ff581c + +首先我们基于 `ListView` 定义一个 `UnboundedListView ` ,通过 `mixin` 的方式` override` 对应的 `Viewport` 和 `Sliver` ,也就是: + +- 把 `buildChildLayout` 里的 `SliverList` 替换成我们自定义的 `UnboundedSliverList` +- 把 `buildViewport` 里的 `Viewport` 替换成我们自定义的 `UnboundedViewport` +- 在 `buildSlivers` 里处理` Padding` 逻辑,把 `SliverPadding` 替换为自定义的 `UnboundedSliverPadding` + +```dart +class UnboundedListView = ListView with UnboundedListViewMixin; + + +/// BoxScrollView 的基础上 +mixin UnboundedListViewMixin on ListView { + @override + Widget buildChildLayout(BuildContext context) { + return UnboundedSliverList(delegate: childrenDelegate); + } + + @protected + Widget buildViewport( + BuildContext context, + ViewportOffset offset, + AxisDirection axisDirection, + List slivers, + ) { + return UnboundedViewport( + axisDirection: axisDirection, + offset: offset, + slivers: slivers, + cacheExtent: cacheExtent, + ); + } + + @override + List buildSlivers(BuildContext context) { + Widget sliver = buildChildLayout(context); + EdgeInsetsGeometry? effectivePadding = padding; + if (padding == null) { + final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context); + if (mediaQuery != null) { + // Automatically pad sliver with padding from MediaQuery. + final EdgeInsets mediaQueryHorizontalPadding = + mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0); + final EdgeInsets mediaQueryVerticalPadding = + mediaQuery.padding.copyWith(left: 0.0, right: 0.0); + // Consume the main axis padding with SliverPadding. + effectivePadding = scrollDirection == Axis.vertical + ? mediaQueryVerticalPadding + : mediaQueryHorizontalPadding; + // Leave behind the cross axis padding. + sliver = MediaQuery( + data: mediaQuery.copyWith( + padding: scrollDirection == Axis.vertical + ? mediaQueryHorizontalPadding + : mediaQueryVerticalPadding, + ), + child: sliver, + ); + } + } + + if (effectivePadding != null) + sliver = + UnboundedSliverPadding(padding: effectivePadding, sliver: sliver); + return [sliver]; + } +} +``` + +接下来首先是实现 `UnboundedViewport` ,一样的套路: + +- 首先基于 `Viewport` 的基础上,通过 `createRenderObject` 将 `RenderViewPort` 修改为我们的 `UnboundedRenderViewport` +- 基于 `RenderViewport` 增加 `performLayout` 和 `layoutChildSequence` 的自定义逻辑,实际上就是增加一个 `unboundedSize` 参数,这个参数通过 child 的 `RenderSliver` 里去统计得到 + +```dart + +class UnboundedViewport = Viewport with UnboundedViewportMixin; +mixin UnboundedViewportMixin on Viewport { + @override + RenderViewport createRenderObject(BuildContext context) { + return UnboundedRenderViewport( + axisDirection: axisDirection, + crossAxisDirection: crossAxisDirection ?? + Viewport.getDefaultCrossAxisDirection(context, axisDirection), + anchor: anchor, + offset: offset, + cacheExtent: cacheExtent, + ); + } +} + +class UnboundedRenderViewport = RenderViewport + with UnboundedRenderViewportMixin; +mixin UnboundedRenderViewportMixin on RenderViewport { + @override + bool get sizedByParent => false; + + double _unboundedSize = double.infinity; + + @override + void performLayout() { + BoxConstraints constraints = this.constraints; + if (axis == Axis.horizontal) { + _unboundedSize = constraints.maxHeight; + size = Size(constraints.maxWidth, 0); + } else { + _unboundedSize = constraints.maxWidth; + size = Size(0, constraints.maxHeight); + } + + super.performLayout(); + + switch (axis) { + case Axis.vertical: + offset.applyViewportDimension(size.height); + break; + case Axis.horizontal: + offset.applyViewportDimension(size.width); + break; + } + } + + @override + double layoutChildSequence({ + required RenderSliver? child, + required double scrollOffset, + required double overlap, + required double layoutOffset, + required double remainingPaintExtent, + required double mainAxisExtent, + required double crossAxisExtent, + required GrowthDirection growthDirection, + required RenderSliver? advance(RenderSliver child), + required double remainingCacheExtent, + required double cacheOrigin, + }) { + crossAxisExtent = _unboundedSize; + var firstChild = child; + + final result = super.layoutChildSequence( + child: child, + scrollOffset: scrollOffset, + overlap: overlap, + layoutOffset: layoutOffset, + remainingPaintExtent: remainingPaintExtent, + mainAxisExtent: mainAxisExtent, + crossAxisExtent: crossAxisExtent, + growthDirection: growthDirection, + advance: advance, + remainingCacheExtent: remainingCacheExtent, + cacheOrigin: cacheOrigin, + ); + + double unboundedSize = 0; + while (firstChild != null) { + if (firstChild.geometry is UnboundedSliverGeometry) { + final UnboundedSliverGeometry childGeometry = + firstChild.geometry as UnboundedSliverGeometry; + unboundedSize = math.max(unboundedSize, childGeometry.crossAxisSize); + } + firstChild = advance(firstChild); + } + if (axis == Axis.horizontal) { + size = Size(size.width, unboundedSize); + } else { + size = Size(unboundedSize, size.height); + } + + return result; + } +} +``` + +接下来我们继承 `SliverGeometry` 自定义一个 `UnboundedSliverGeometry` ,主要就是增加了一个 `crossAxisSize` 参数,用来记录当前统计到的副轴高度,从而让上面的 `ViewPort` 可以获取得到。 + +```dart +class UnboundedSliverGeometry extends SliverGeometry { + UnboundedSliverGeometry( + {SliverGeometry? existing, required this.crossAxisSize}) + : super( + scrollExtent: existing?.scrollExtent ?? 0.0, + paintExtent: existing?.paintExtent ?? 0.0, + paintOrigin: existing?.paintOrigin ?? 0.0, + layoutExtent: existing?.layoutExtent, + maxPaintExtent: existing?.maxPaintExtent ?? 0.0, + maxScrollObstructionExtent: + existing?.maxScrollObstructionExtent ?? 0.0, + hitTestExtent: existing?.hitTestExtent, + visible: existing?.visible, + hasVisualOverflow: existing?.hasVisualOverflow ?? false, + scrollOffsetCorrection: existing?.scrollOffsetCorrection, + cacheExtent: existing?.cacheExtent, + ); + + final double crossAxisSize; +} +``` + +如下代码所示,最终我们基于 `SliverList` 实现一个 `UnboundedSliverList` ,这也是核心逻辑,主要是实现 `performLayout` 部分的代码,我们需要在原来代码的基础上,在某些节点加上自定义的逻辑,用于统计参与布局的每个 Item 的高度,从而得到一个最大值。 + +> 代码看起来很长,但是其实我们新增的很少。 + +```dart +class UnboundedSliverList = SliverList with UnboundedSliverListMixin; +mixin UnboundedSliverListMixin on SliverList { + @override + RenderSliverList createRenderObject(BuildContext context) { + final SliverMultiBoxAdaptorElement element = + context as SliverMultiBoxAdaptorElement; + return UnboundedRenderSliverList(childManager: element); + } +} + +class UnboundedRenderSliverList extends RenderSliverList { + UnboundedRenderSliverList({ + required RenderSliverBoxChildManager childManager, + }) : super(childManager: childManager); + + // See RenderSliverList::performLayout + @override + void performLayout() { + final SliverConstraints constraints = this.constraints; + childManager.didStartLayout(); + childManager.setDidUnderflow(false); + + final double scrollOffset = + constraints.scrollOffset + constraints.cacheOrigin; + assert(scrollOffset >= 0.0); + final double remainingExtent = constraints.remainingCacheExtent; + assert(remainingExtent >= 0.0); + final double targetEndScrollOffset = scrollOffset + remainingExtent; + BoxConstraints childConstraints = constraints.asBoxConstraints(); + int leadingGarbage = 0; + int trailingGarbage = 0; + bool reachedEnd = false; + + if (constraints.axis == Axis.horizontal) { + childConstraints = childConstraints.copyWith(minHeight: 0); + } else { + childConstraints = childConstraints.copyWith(minWidth: 0); + } + + double unboundedSize = 0; + + // should call update after each child is laid out + updateUnboundedSize(RenderBox? child) { + if (child == null) { + return; + } + unboundedSize = math.max( + unboundedSize, + constraints.axis == Axis.horizontal + ? child.size.height + : child.size.width); + } + + unboundedGeometry(SliverGeometry geometry) { + return UnboundedSliverGeometry( + existing: geometry, + crossAxisSize: unboundedSize, + ); + } + + // This algorithm in principle is straight-forward: find the first child + // that overlaps the given scrollOffset, creating more children at the top + // of the list if necessary, then walk down the list updating and laying out + // each child and adding more at the end if necessary until we have enough + // children to cover the entire viewport. + // + // It is complicated by one minor issue, which is that any time you update + // or create a child, it's possible that the some of the children that + // haven't yet been laid out will be removed, leaving the list in an + // inconsistent state, and requiring that missing nodes be recreated. + // + // To keep this mess tractable, this algorithm starts from what is currently + // the first child, if any, and then walks up and/or down from there, so + // that the nodes that might get removed are always at the edges of what has + // already been laid out. + + // Make sure we have at least one child to start from. + if (firstChild == null) { + if (!addInitialChild()) { + // There are no children. + geometry = unboundedGeometry(SliverGeometry.zero); + childManager.didFinishLayout(); + return; + } + } + + // We have at least one child. + + // These variables track the range of children that we have laid out. Within + // this range, the children have consecutive indices. Outside this range, + // it's possible for a child to get removed without notice. + RenderBox? leadingChildWithLayout, trailingChildWithLayout; + + RenderBox? earliestUsefulChild = firstChild; + + // A firstChild with null layout offset is likely a result of children + // reordering. + // + // We rely on firstChild to have accurate layout offset. In the case of null + // layout offset, we have to find the first child that has valid layout + // offset. + if (childScrollOffset(firstChild!) == null) { + int leadingChildrenWithoutLayoutOffset = 0; + while (earliestUsefulChild != null && + childScrollOffset(earliestUsefulChild) == null) { + earliestUsefulChild = childAfter(earliestUsefulChild); + leadingChildrenWithoutLayoutOffset += 1; + } + // We should be able to destroy children with null layout offset safely, + // because they are likely outside of viewport + collectGarbage(leadingChildrenWithoutLayoutOffset, 0); + // If can not find a valid layout offset, start from the initial child. + if (firstChild == null) { + if (!addInitialChild()) { + // There are no children. + geometry = unboundedGeometry(SliverGeometry.zero); + childManager.didFinishLayout(); + return; + } + } + } + + // Find the last child that is at or before the scrollOffset. + earliestUsefulChild = firstChild; + for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!; + earliestScrollOffset > scrollOffset; + earliestScrollOffset = childScrollOffset(earliestUsefulChild)!) { + // We have to add children before the earliestUsefulChild. + earliestUsefulChild = + insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true); + updateUnboundedSize(earliestUsefulChild); + if (earliestUsefulChild == null) { + final SliverMultiBoxAdaptorParentData childParentData = + firstChild!.parentData! as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = 0.0; + + if (scrollOffset == 0.0) { + // insertAndLayoutLeadingChild only lays out the children before + // firstChild. In this case, nothing has been laid out. We have + // to lay out firstChild manually. + firstChild!.layout(childConstraints, parentUsesSize: true); + earliestUsefulChild = firstChild; + updateUnboundedSize(earliestUsefulChild); + leadingChildWithLayout = earliestUsefulChild; + trailingChildWithLayout ??= earliestUsefulChild; + break; + } else { + // We ran out of children before reaching the scroll offset. + // We must inform our parent that this sliver cannot fulfill + // its contract and that we need a scroll offset correction. + geometry = unboundedGeometry(SliverGeometry( + scrollOffsetCorrection: -scrollOffset, + )); + return; + } + } + + final double firstChildScrollOffset = + earliestScrollOffset - paintExtentOf(firstChild!); + // firstChildScrollOffset may contain double precision error + if (firstChildScrollOffset < -precisionErrorTolerance) { + // Let's assume there is no child before the first child. We will + // correct it on the next layout if it is not. + geometry = unboundedGeometry(SliverGeometry( + scrollOffsetCorrection: -firstChildScrollOffset, + )); + final SliverMultiBoxAdaptorParentData childParentData = + firstChild!.parentData! as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = 0.0; + return; + } + + final SliverMultiBoxAdaptorParentData childParentData = + earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = firstChildScrollOffset; + assert(earliestUsefulChild == firstChild); + leadingChildWithLayout = earliestUsefulChild; + trailingChildWithLayout ??= earliestUsefulChild; + } + + assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance); + + // If the scroll offset is at zero, we should make sure we are + // actually at the beginning of the list. + if (scrollOffset < precisionErrorTolerance) { + // We iterate from the firstChild in case the leading child has a 0 paint + // extent. + while (indexOf(firstChild!) > 0) { + final double earliestScrollOffset = childScrollOffset(firstChild!)!; + // We correct one child at a time. If there are more children before + // the earliestUsefulChild, we will correct it once the scroll offset + // reaches zero again. + earliestUsefulChild = + insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true); + updateUnboundedSize(earliestUsefulChild); + assert(earliestUsefulChild != null); + final double firstChildScrollOffset = + earliestScrollOffset - paintExtentOf(firstChild!); + final SliverMultiBoxAdaptorParentData childParentData = + firstChild!.parentData! as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = 0.0; + // We only need to correct if the leading child actually has a + // paint extent. + if (firstChildScrollOffset < -precisionErrorTolerance) { + geometry = unboundedGeometry(SliverGeometry( + scrollOffsetCorrection: -firstChildScrollOffset, + )); + return; + } + } + } + + // At this point, earliestUsefulChild is the first child, and is a child + // whose scrollOffset is at or before the scrollOffset, and + // leadingChildWithLayout and trailingChildWithLayout are either null or + // cover a range of render boxes that we have laid out with the first being + // the same as earliestUsefulChild and the last being either at or after the + // scroll offset. + + assert(earliestUsefulChild == firstChild); + assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset); + + // Make sure we've laid out at least one child. + if (leadingChildWithLayout == null) { + earliestUsefulChild!.layout(childConstraints, parentUsesSize: true); + updateUnboundedSize(earliestUsefulChild); + leadingChildWithLayout = earliestUsefulChild; + trailingChildWithLayout = earliestUsefulChild; + } + + // Here, earliestUsefulChild is still the first child, it's got a + // scrollOffset that is at or before our actual scrollOffset, and it has + // been laid out, and is in fact our leadingChildWithLayout. It's possible + // that some children beyond that one have also been laid out. + + bool inLayoutRange = true; + RenderBox? child = earliestUsefulChild; + int index = indexOf(child!); + double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child); + bool advance() { + // returns true if we advanced, false if we have no more children + // This function is used in two different places below, to avoid code duplication. + assert(child != null); + if (child == trailingChildWithLayout) inLayoutRange = false; + child = childAfter(child!); + if (child == null) inLayoutRange = false; + index += 1; + if (!inLayoutRange) { + if (child == null || indexOf(child!) != index) { + // We are missing a child. Insert it (and lay it out) if possible. + child = insertAndLayoutChild( + childConstraints, + after: trailingChildWithLayout, + parentUsesSize: true, + ); + updateUnboundedSize(child); + if (child == null) { + // We have run out of children. + return false; + } + } else { + // Lay out the child. + child!.layout(childConstraints, parentUsesSize: true); + updateUnboundedSize(child!); + } + trailingChildWithLayout = child; + } + assert(child != null); + final SliverMultiBoxAdaptorParentData childParentData = + child!.parentData! as SliverMultiBoxAdaptorParentData; + childParentData.layoutOffset = endScrollOffset; + assert(childParentData.index == index); + endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!); + return true; + } + + // Find the first child that ends after the scroll offset. + while (endScrollOffset < scrollOffset) { + leadingGarbage += 1; + if (!advance()) { + assert(leadingGarbage == childCount); + assert(child == null); + // we want to make sure we keep the last child around so we know the end scroll offset + collectGarbage(leadingGarbage - 1, 0); + assert(firstChild == lastChild); + final double extent = + childScrollOffset(lastChild!)! + paintExtentOf(lastChild!); + geometry = unboundedGeometry( + SliverGeometry( + scrollExtent: extent, + paintExtent: 0.0, + maxPaintExtent: extent, + ), + ); + return; + } + } + + // Now find the first child that ends after our end. + while (endScrollOffset < targetEndScrollOffset) { + if (!advance()) { + reachedEnd = true; + break; + } + } + + // Finally count up all the remaining children and label them as garbage. + if (child != null) { + child = childAfter(child!); + while (child != null) { + trailingGarbage += 1; + child = childAfter(child!); + } + } + + // At this point everything should be good to go, we just have to clean up + // the garbage and report the geometry. + + collectGarbage(leadingGarbage, trailingGarbage); + + assert(debugAssertChildListIsNonEmptyAndContiguous()); + double estimatedMaxScrollOffset; + if (reachedEnd) { + estimatedMaxScrollOffset = endScrollOffset; + } else { + estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset( + constraints, + firstIndex: indexOf(firstChild!), + lastIndex: indexOf(lastChild!), + leadingScrollOffset: childScrollOffset(firstChild!), + trailingScrollOffset: endScrollOffset, + ); + assert(estimatedMaxScrollOffset >= + endScrollOffset - childScrollOffset(firstChild!)!); + } + final double paintExtent = calculatePaintOffset( + constraints, + from: childScrollOffset(firstChild!)!, + to: endScrollOffset, + ); + final double cacheExtent = calculateCacheOffset( + constraints, + from: childScrollOffset(firstChild!)!, + to: endScrollOffset, + ); + final double targetEndScrollOffsetForPaint = + constraints.scrollOffset + constraints.remainingPaintExtent; + geometry = unboundedGeometry( + SliverGeometry( + scrollExtent: estimatedMaxScrollOffset, + paintExtent: paintExtent, + cacheExtent: cacheExtent, + maxPaintExtent: estimatedMaxScrollOffset, + // Conservative to avoid flickering away the clip during scroll. + hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || + constraints.scrollOffset > 0.0, + ), + ); + + // We may have started the layout while scrolled to the end, which would not + // expose a new child. + if (estimatedMaxScrollOffset == endScrollOffset) + childManager.setDidUnderflow(true); + childManager.didFinishLayout(); + } +} +``` + +别看上面这段代码很长,其实很多都是 `RenderSliverList` 自己的源码,如下图所示,真正我们修改添加的只有这么点: + +- 在开始前增加 `updateUnboundedSize` 和 `unboundedGeometry` 用于记录布局高度和生成 `UnboundedSliverGeometry` +- 将所有原来的 `SliverGeometry ` 修改为 `UnboundedSliverGeometry` +- 在所有涉及 `layout` 的位置后面调用 `updateUnboundedSize` ,因为 child 在布局之后我们就可以获取到它的 `Size` ,然后我们统计得到他们的最大值,就可以通过 `UnboundedSliverGeometry` 返回给 `ViewPort` 。 + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image7.png) + +最后如下代码所示,将 `UnboundedListView` 添加到一开始的垂直 `ListView `里,运行之后可以看到,随着横向滑动,列表的自身高度在发生变化。 + +```dart +return Scaffold( + appBar: AppBar( + title: new Text("ControllerDemoPage"), + ), + extendBody: true, + body: Container( + color: Colors.white, + child: ListView( + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: IntrinsicHeight( + child: Row( + children: List.generate(50, (index) { + return Padding( + padding: EdgeInsets.all(2), + child: Container( + alignment: Alignment.bottomCenter, + color: Colors.blue, + child: Text(List.generate( + math.Random().nextInt(10), (index) => "TEST\n") + .toString()), + ), + ); + }), + ), + ), + ), + UnboundedListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 100, + itemBuilder: (context, index) { + print('$index'); + return Padding( + padding: EdgeInsets.all(2), + child: Container( + height: index * 1.0 + 10, + width: 50, + color: Colors.blue, + ), + ); + }), + Container( + height: 1000, + color: Colors.green, + ), + ], + ), + ), +); +``` + + + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image8.gif) + +那么这是否达到了我们的需求?如下代码所示,假如我将代码修改成如下所示,运行之后可以看到,此时的横向列表变成了参差不齐的状态。 + +```dart +UnboundedListView.builder( + scrollDirection: Axis.horizontal, + itemCount: 100, + itemBuilder: (context, index) { + print('$index'); + return Container( + padding: EdgeInsets.all(2), + child: Container( + width: 50, + color: Colors.blue, + alignment: Alignment.bottomCenter, + child: Text(List.generate( + math.Random().nextInt(15), (index) => "TEST\n") + .toString()), + ), + ); + }), +``` + + + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image9.png) + +但是这时候我们无法用类似 `IntrinsicHeight` 的方式来解决,因为 `ListView` 里的 Item 都是动态处理的,**也就是布局时需要处理特定便宜范围内的 Item 添加和销毁**,具体在 `performLayout` 里会通过 `scrollOffset` 和 `targetEndScrollOffset` 等来确定布局 Item 的范围。 + +> 这样就导致我们通过 `firstChild` 链表结构去访问的时候,我们无法在 `layout` 之前获取到 child ,因为此时它还没有被 add 到链表里,同时也受限于 `insertAndLayoutLeadingChild` 和 `insertAndLayoutChild` 的耦合实现和私有方法限制,这里不方便简单重写支持。 + +但是「天无绝人之路」,既然我们不能在 child `layout` 之前处理,那么我们可以在 `layout` 之后做多一次冗余布局,如下代码所示: + +- 我们首先将 `unboundedSize` 提取为 `UnboundedRenderSliverList` 里的全局变量 +- 在 `didFinishLayout` 之前,通过 `firstChild` 链表结构,重新通过 `layout(childConstraints.tighten(height: unboundedSize)` 布局多一次 + +```dart + double unboundedSize = 0; + + // See RenderSliverList::performLayout + @override + void performLayout() { + + ···· + var tmpChild = firstChild; + while (tmpChild != null) { + tmpChild.layout(childConstraints.tighten(height: unboundedSize), + parentUsesSize: true); + tmpChild = childAfter(tmpChild); + } + + childManager.didFinishLayout(); + ···· + } +``` + +运行之后可以看到,此时列表已经全部对齐,而损耗就是 child 会有 double 布局的情况,对于此处性能损耗,对比 `SingleChildScrollView` 的实现,可以根据实际场景来取舍使用哪种逻辑,**当然,为了性能考虑非必要还是给横向 `ListView` 一个高度,这样的实现才是最优解**。 + +![](http://img.cdn.guoshuyu.cn/20230425_N24/image10.png) + +好了,本篇小技巧到这里就解决了,不知道对于类似实现,你是否还有什么想法,如果你有更好的解决方案,欢迎留言讨论。 + +> 完整代码可见:https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/un_bounded_listview.dart \ No newline at end of file diff --git a/README.md b/README.md index 5206782..d9b7086 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ - [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md) - [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md) - [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md) + - [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md) - **Dart** - [Dart 2.12 发布,稳定空安全声明和FFI版本,Dart 未来的计划](Dart-212.md) - [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md) @@ -101,6 +102,7 @@ - [Dart 2.17 发布的新特性](Dart-217.md) - [Dart 2.18 发布,Objective-C 和 Swift interop](Dart-218.md) - [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md) + - [Dart 3 发布,快来看看有什么更新吧](Dart-300.md) * [番外](FWREADME.md) @@ -179,6 +181,10 @@ * [ Flutter 小技巧之 3.7 更灵活的编译变量支持](Flutter-N19.md) * [面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯”](Flutter-GPT.md) * [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md) + * [Flutter 小技巧之霓虹灯文本的「故障」效果的实现](Flutter-N21.md) + * [Flutter 小技巧之横竖列表的自适应大小布局支持](Flutter-N24.md) + * [Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC](Flutter-IOW.md) + * [Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher](Flutter-310Win.md) [Flutter 工程化选择](GCH.md) diff --git a/SUMMARY.md b/SUMMARY.md index d55e800..92b1873 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -60,6 +60,7 @@ - [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md) - [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md) - [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md) + - [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md) - **Dart** - [Dart 2.12 发布,稳定空安全声明和FFI版本,Dart 未来的计划](Dart-212.md) - [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md) @@ -68,6 +69,7 @@ - [Dart 2.17 发布的新特性](Dart-217.md) - [Dart 2.18 发布,Objective-C 和 Swift interop](Dart-218.md) - [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md) + - [Dart 3 发布,快来看看有什么更新吧](Dart-300.md) * [番外](FWREADME.md) @@ -219,6 +221,14 @@ * [Flutter 小技巧之实现一个精美的动画相册效果](Flutter-N20.md) + * [Flutter 小技巧之霓虹灯文本的「故障」效果的实现](Flutter-N21.md) + + * [Flutter 小技巧之横竖列表的自适应大小布局支持](Flutter-N24.md) + + * [Flutter 3.10 之 Flutter Web 路线已定,可用性进一步提升,快来尝鲜 WasmGC](Flutter-IOW.md) + + * [Flutter 3.10 适配之单例 Window 弃用,一起来了解 View.of 和 PlatformDispatcher](Flutter-310Win.md) + * [Flutter 工程化选择](GCH.md) * [Flutter 工程化框架选择——搞定 Flutter 动画](Z1.md) * [Flutter 工程化框架选择 — 搞定 UI 生产力](Z3.md) diff --git a/UPDATE.md b/UPDATE.md index f6d5e22..c352d5c 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -13,6 +13,7 @@ - [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md) - [Flutter 3.3 正式发布,快来看看有什么新功能吧](Flutter-330.md) - [Flutter 3.7 正式发布,快来看看有什么新功能吧](Flutter-370.md) +- [ Flutter 3.10 发布,快来看看有什么更新吧](Flutter-310.md) @@ -27,4 +28,5 @@ - [Dart 2.17 发布的新特性](Dart-217.md) - [Dart 2.18 发布,Objective-C 和 Swift interop](Dart-218.md) - [Flutter - Dart 3α 新特性 Record 和 Patterns 的提前预览讲解](Dart-300a.md) +- [Dart 3 发布,快来看看有什么更新吧](Dart-300.md)