This commit is contained in:
guoshuyu 2021-12-23 14:52:35 +08:00
parent 1f1c2a84f3
commit b04d552857
4 changed files with 495 additions and 161 deletions

247
Flutter-TE.md Normal file
View File

@ -0,0 +1,247 @@
> **看完本篇,你不仅会了解到 TextField 的实现和构成,还可以学到很多之前不常用的“奇怪”知识**
在 Flutter 里 `TextField` 是一个比较复杂的控件,而在整个 `TextField` 里嵌套了许多不同实现的控件,它们组成了我们常用的输入框效果,**如下图所示是关于 `TextField` 的主要构成部分**,也是本篇主要讲解的内容。
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image1)
## FocusTrapArea
`FocusTrapArea` 大家可能会比较陌生,这个是最近的版本里才出现的控件,`FocusTrapArea` 本身并没有特别,它仅仅是在 `RenderObject` tree 里塞进去了一个 `FocusNode`
它的出现主要是为了 Web/Desktop 平台,通过增加了 `FocusTrapArea` 之后,在 Web/Desktop 平台执行 `TextEditingController.clear` 的时候,`TextField` 还能继续保持之前获得的焦点。
> 具体可见 Flutter 的 issues [#86154](https://github.com/flutter/flutter/issues/86154) 、[#86041](https://github.com/flutter/flutter/pull/86041)
正常效果 | 非正常效果 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [![stable](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image2)](https://user-images.githubusercontent.com/140617/125034739-3c363f80-e091-11eb-8907-9fb256816c2d.gif) | [![master](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image3)](https://user-images.githubusercontent.com/140617/125034754-40faf380-e091-11eb-9cfc-9a87777df863.gif) |
## MouseRegion
顾名思义是用于处理鼠标相关事件,主要用于响应鼠标独占的 Pointer事件比如鼠标进入/离开控件区域、光标显示效果等等。
## IgnorePointer
它在 `TextField` 里主要用于处理**当前输入框是否可用的的状态**,比如当 `widget.enabled` 或者 `widget.decoration?.enabled``false` 时,`IgnorePointer` 就会屏蔽整个区域内的手势事件,从而让 `TextField` 会无法点击输入。
## TextSelectionGestureDetectorBuilder
关于 `TextSelectionGestureDetectorBuilder` 大家应该比较少接触,而在 `TextField` 里使用的是它的子类 `_TextFieldSelectionGestureDetectorBuilder`
> **它主要是处理 `TextField` 内针对 `EditableText` 的点击、滑动、长按等事件,例如单击弹起键盘,长按弹出选择复制/粘贴框等等**
`TextSelectionGestureDetectorBuilder` 的内部主要是通过 `editableTextKey` 这个 `GlobalKey` 去获取到 `EditableTextState `,从而将各种手势事件和 `EditableText` 里的行为关联起来。
> 该控件内部使用的是 `TextSelectionGestureDetector`
例如在 `_TextFieldSelectionGestureDetectorBuilder` 中,可以看到 `onSingleTapUp` 的处理流程:
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image4)
如上代码所示:
- 1、收起已经弹出的 Toolbar (一个 `Overlay`,也就是复制/粘贴之类的弹框);
- 2、根据不同平台选择响应事件
- 3、执行弹出键盘操作
- 4、回调点击事件
所以可以看到,**这里其实是先执行弹出键盘,然后再回调点击的 callback**,所以如果你需要在点击弹出键盘前,针对 `TextField` 作一些处理,那么 `TextField``onTap` 其实并不合适,因为它是已经弹出了。
**最后 `_TextFieldSelectionGestureDetectorBuilder` 会调用 `buildGestureDetector` 方法生成一个监听和处理触摸的控件,用于嵌套 child**。
## InputDecorator
关于 `InputDecorator` 的内部参数解析这里就不多说,以前在书里已经有详细介绍过,用过 `TextField` 的大家对于 `InputDecorator` 应该也不会陌生,在 **`TextField``InputDecorator` 的实现是和 `AnimatedBuilder` 一起组成使用**。
因为在 `TextField``FocusNode``TextEditingController` 都是 `ChangeNotifier``Listenable`) ,所以它们可以被用于 `AnimatedBuilder``animation`
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image5)
**也就是当 `FocusNode``TextEditingController` 这两者发生改变的时候,会让 `InputDecorator` 重新 `rebuild` 从而改变渲染效果**,例如:输入框输入内容时、焦点发生改变时修改输入框的背景颜色。
> 注意别搞混了 `InputDecorator``InputDecoration``InputDecoration` 是用来配置 `InputDecorator`
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image6)
所以可以看到 `InputDecorator` 有很丰富的参数和配置,开发者可以通过 `InputDecoration` 来配置很丰富的输入框 UI 效果,**但是如果刚好出现某些位置,或者某些缝隙不满足产品诡异的需求时,那恭喜你,你开启了 Flutter 高级开发的修炼之路**。
为什么呢?
简单来说 `InputDecorator` 的实现是在内部是一个自定义的 `RenderBox`,其中和 layout 相关部分就有 600 多行的代码,也就是根据 `InputDecoration``icon`、`prefixIcon`、`suffix` 等参数,进行定位布局,计算位置方向,根据基线调整位置等等。
> 另外` InputDecorator` 里的动画效果主要是通过内部的 `AnimatedOpacity` 等完成。
所以对于 `InputDecorator` 来说,如果你对于某些位置或者边界效果不满意,要么你就重构一个自己的实现,要么可能就要选择“委曲求全”。
## RepaintBoundary
为什么 `TextField` 内部会有一个 `RepaintBoundary` 首先 `RepaintBoundary` 是干嘛的?
之前在 [《Flutter 画面渲染的全面解析》](https://juejin.cn/post/6844904104452440072) 详细介绍过这部分的知识,这简单不严谨地说就是: **`RepaintBoundary` 主要是用于形成一个 `Layer`,得到一个独立的绘制区域**。
常见的就是 `Navigator` 的页面跳转,内部基础实现都有一个 `RepaintBoundary` 来保证每个区域都是独立的绘制区域。
> 另外说到 `Navigator`就不得不说每个页面也都有自己的 `FocusScope` 也就是我们常用的 `FocusScope.of(context)` 等用于键盘和焦点处理。
`TextField` 内部有一个 `RepaintBoundary` ,是因为 `TextField` 本身是一个需要频繁更新的控件,而 `TextField` 里的内容变化一般很少需要触发父布局的重绘,**所以 `RepaintBoundary` 的存在让 `TextField` 可以实现性能更好的局部绘制**。
## UnmanagedRestorationScope
`UnmanagedRestorationScope` 大家可能比较少用到,它本身是一个 `InheritedWidget` ,主要是往下共享一个 `RestorationBucket` ,而 **`RestorationBucket` 主要是和实现状态的保存/恢复有关系**。
例如应用因为低内存在后台被回收时,可以通过它在重新回到 App 时恢复指定的数据,举个例子:
```dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// Give your RootRestorationScope an id, defaults to null.
restorationScopeId: 'root',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
// Our state should be mixed-in with RestorationMixin
class _HomePageState extends State<HomePage> with RestorationMixin {
// For each state, we need to use a restorable property
final RestorableInt _index = RestorableInt(0);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Index is ${_index.value}')),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index.value,
onTap: (i) => setState(() => _index.value = i),
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home'
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications),
label: 'Notifications'
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings'
),
],
),
);
}
@override
// The restoration bucket id for this page,
// let's give it the name of our page!
String get restorationId => 'home_page';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
// Register our property to be saved every time it changes,
// and to be restored every time our app is killed by the OS!
registerForRestoration(_index, 'nav_bar_index');
}
}
```
如上代码所示:
- 首先给 `MaterialApp` 配置 `restorationScopeId`(必须配置才算开启该功能)。
- 使用 `RestorableInt` 用于配置和保存 `BottomNavigationBar``index`
- 在 `State` 混入 `RestorationMixin` 并且在 `restoreState` 方法里恢复 `index` 的状态;
其中默认 `MaterialApp` 内部用到了 `RootRestorationScope` 而`RootRestorationScope` 的内部就是 `UnmanagedRestorationScope`;上述例子运行后通过打开模拟器开发者设置里的 *`Don't keep activities`* 就可以看到效果。
> 以上示例来自 [《Introduction to State Restoration in Flutter》](https://dev.to/pedromassango/what-is-state-restoration-and-how-to-use-it-in-flutter-5blm) 。
回到 `TextField`,在 `_TextFieldState` 里就混入了 `RestorationMixin`,然后使用 `RestorableTextEditingController` 用于用于恢复 `TextEditingController`
> 因为输入框的内容默认保存在了 `TextEditingController``TextEditingValue` 里,所以这里用的是 `RestorableTextEditingController`
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image7)
一般情况下是使用 `MaterialApp` 内部默认自带了一个 `RootRestorationScope` ,所以我们只需要给 `MaterialApp` 设置 `restorationScopeId`,而 **`TextFild` 通过内置 `UnmanagedRestorationScope` 相关的逻辑,最终实现了文本内容的保存与恢复**。
## EditableText
`EditableText` 就不用多说了,`TextField` 的本体,内部主要通过 `Scrollable` 来实现滑动,同样的它也用了对应的 `restorationId` 来实现恢复和缓存。
**首先注意到可以滑动这一点,可以看到对于 `EditableText` 来说,它其实是一个 “ViewPort”是根据 `ViewportOffset` 来实现滑动效果**。
而对于 `EditableText` 内部,**它使用了 `CompositedTransformTarget` 来实现 Toolbar 和输入框的联动**,也就是输入控件和长按“粘贴/复制”弹出框之间的关联。
**所以这里简单介绍下 `CompositedTransformTarget`,它通常和 `CompositedTransformFollower` 一起被用于控件之间的联动效果**。
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image8)
如上图所示,常见内置的 `Slider`,在滑动的弹出部分实现,就是通过 `CompositedTransformTarget``CompositedTransformFollower` 的结合实现,**它可以让一个控件跟随另外一个控件而无需计算位置,它们之间主要是通过 `LayerLink` 链接在一起**。
回到 `TextField`,其实除了 “复制/粘贴” 的 Toolbar ,关于 selection 选中区域的内容,`EditableText` 内部也是通过类似的方式实现,只是这里是直接通过 `LeaderLayer` 而不是通过它的封装 `CompositedTransformTarget` 去实现。
> 对于使用 `CompositedTransformTarget` 有兴趣的可以参考https://juejin.cn/post/6946416845537116190
当然使用 `CompositedTransformTarget` 还是会有“比较大”的性能开销,不建议大规模频繁使用,因为毕竟它属于一个 `pushLayer` 的操作。
另外 `EditableText` 内部绘制内容的部分,主要就是大家都知道的 `TextPainter` ,这部分就没什么特别,暂时不详细展开。
**所以本篇主要是通过介绍 `TextField` 的组成,以及解释内部各组成部分的作用,让开发者可以更清晰的了解 Flutter 里常用的文本输入框的实现,当遇上问题或者需求时,可以快速定位和解决问题**,例如:
- ”粘贴/复制“ 的 Toolbar 是哪里弹出;
- Toolbar 是如何定位和布局;
- 点击 `TextField` 是如何弹出键盘和处理手势事件;
- `TextField` 如何做到局部绘制;
- ...
最后介绍一个简单的问题,之前有人刚好问我:***如何在 Flutter 上实现类似微信聊天输入框从一行到多行的输入框效果***,如下图代码所示,就是这么简单:
```dart
TextField(
focusNode: _focusNode,
maxLines: 7,
minLines: 1,
decoration:
const InputDecoration(border: OutlineInputBorder()),
)
```
![](http://img.cdn.guoshuyu.cn/20211223_Flutter-TE/image9)

247
Flutter-TE.md.bak Normal file
View File

@ -0,0 +1,247 @@
> **看完本篇,你不仅会了解到 TextField 的实现和构成,还可以学到很多之前不常用的“奇怪”知识**
在 Flutter 里 `TextField` 是一个比较复杂的控件,而在整个 `TextField` 里嵌套了许多不同实现的控件,它们组成了我们常用的输入框效果,**如下图所示是关于 `TextField` 的主要构成部分**,也是本篇主要讲解的内容。
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8a95fd32e21848ada08a37cea579c362~tplv-k3u1fbpfcp-watermark.image?)
## FocusTrapArea
`FocusTrapArea` 大家可能会比较陌生,这个是最近的版本里才出现的控件,`FocusTrapArea` 本身并没有特别,它仅仅是在 `RenderObject` tree 里塞进去了一个 `FocusNode`
它的出现主要是为了 Web/Desktop 平台,通过增加了 `FocusTrapArea` 之后,在 Web/Desktop 平台执行 `TextEditingController.clear` 的时候,`TextField` 还能继续保持之前获得的焦点。
> 具体可见 Flutter 的 issues [#86154](https://github.com/flutter/flutter/issues/86154) 、[#86041](https://github.com/flutter/flutter/pull/86041)
正常效果 | 非正常效果 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [![stable](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d50256024d7a4836b2f1299382f49768~tplv-k3u1fbpfcp-zoom-1.image)](https://user-images.githubusercontent.com/140617/125034739-3c363f80-e091-11eb-8907-9fb256816c2d.gif) | [![master](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b3f81800d0914b3ca377aebd61fed6af~tplv-k3u1fbpfcp-zoom-1.image)](https://user-images.githubusercontent.com/140617/125034754-40faf380-e091-11eb-9cfc-9a87777df863.gif) |
## MouseRegion
顾名思义是用于处理鼠标相关事件,主要用于响应鼠标独占的 Pointer事件比如鼠标进入/离开控件区域、光标显示效果等等。
## IgnorePointer
它在 `TextField` 里主要用于处理**当前输入框是否可用的的状态**,比如当 `widget.enabled` 或者 `widget.decoration?.enabled``false` 时,`IgnorePointer` 就会屏蔽整个区域内的手势事件,从而让 `TextField` 会无法点击输入。
## TextSelectionGestureDetectorBuilder
关于 `TextSelectionGestureDetectorBuilder` 大家应该比较少接触,而在 `TextField` 里使用的是它的子类 `_TextFieldSelectionGestureDetectorBuilder`
> **它主要是处理 `TextField` 内针对 `EditableText` 的点击、滑动、长按等事件,例如单击弹起键盘,长按弹出选择复制/粘贴框等等**
`TextSelectionGestureDetectorBuilder` 的内部主要是通过 `editableTextKey` 这个 `GlobalKey` 去获取到 `EditableTextState `,从而将各种手势事件和 `EditableText` 里的行为关联起来。
> 该控件内部使用的是 `TextSelectionGestureDetector`
例如在 `_TextFieldSelectionGestureDetectorBuilder` 中,可以看到 `onSingleTapUp` 的处理流程:
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d682475d9dcf4fba846868b101c63639~tplv-k3u1fbpfcp-watermark.image?)
如上代码所示:
- 1、收起已经弹出的 Toolbar (一个 `Overlay`,也就是复制/粘贴之类的弹框);
- 2、根据不同平台选择响应事件
- 3、执行弹出键盘操作
- 4、回调点击事件
所以可以看到,**这里其实是先执行弹出键盘,然后再回调点击的 callback**,所以如果你需要在点击弹出键盘前,针对 `TextField` 作一些处理,那么 `TextField``onTap` 其实并不合适,因为它是已经弹出了。
**最后 `_TextFieldSelectionGestureDetectorBuilder` 会调用 `buildGestureDetector` 方法生成一个监听和处理触摸的控件,用于嵌套 child**。
## InputDecorator
关于 `InputDecorator` 的内部参数解析这里就不多说,以前在书里已经有详细介绍过,用过 `TextField` 的大家对于 `InputDecorator` 应该也不会陌生,在 **`TextField``InputDecorator` 的实现是和 `AnimatedBuilder` 一起组成使用**。
因为在 `TextField``FocusNode``TextEditingController` 都是 `ChangeNotifier``Listenable`) ,所以它们可以被用于 `AnimatedBuilder``animation`
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d6e5626d2abf430c80a5e4e562b49d2c~tplv-k3u1fbpfcp-watermark.image?)
**也就是当 `FocusNode``TextEditingController` 这两者发生改变的时候,会让 `InputDecorator` 重新 `rebuild` 从而改变渲染效果**,例如:输入框输入内容时、焦点发生改变时修改输入框的背景颜色。
> 注意别搞混了 `InputDecorator``InputDecoration``InputDecoration` 是用来配置 `InputDecorator`
![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4e5a6ea0dd3e4b549219852ae9a1728c~tplv-k3u1fbpfcp-watermark.image?)
所以可以看到 `InputDecorator` 有很丰富的参数和配置,开发者可以通过 `InputDecoration` 来配置很丰富的输入框 UI 效果,**但是如果刚好出现某些位置,或者某些缝隙不满足产品诡异的需求时,那恭喜你,你开启了 Flutter 高级开发的修炼之路**。
为什么呢?
简单来说 `InputDecorator` 的实现是在内部是一个自定义的 `RenderBox`,其中和 layout 相关部分就有 600 多行的代码,也就是根据 `InputDecoration``icon`、`prefixIcon`、`suffix` 等参数,进行定位布局,计算位置方向,根据基线调整位置等等。
> 另外` InputDecorator` 里的动画效果主要是通过内部的 `AnimatedOpacity` 等完成。
所以对于 `InputDecorator` 来说,如果你对于某些位置或者边界效果不满意,要么你就重构一个自己的实现,要么可能就要选择“委曲求全”。
## RepaintBoundary
为什么 `TextField` 内部会有一个 `RepaintBoundary` 首先 `RepaintBoundary` 是干嘛的?
之前在 [《Flutter 画面渲染的全面解析》](https://juejin.cn/post/6844904104452440072) 详细介绍过这部分的知识,这简单不严谨地说就是: **`RepaintBoundary` 主要是用于形成一个 `Layer`,得到一个独立的绘制区域**。
常见的就是 `Navigator` 的页面跳转,内部基础实现都有一个 `RepaintBoundary` 来保证每个区域都是独立的绘制区域。
> 另外说到 `Navigator`就不得不说每个页面也都有自己的 `FocusScope` 也就是我们常用的 `FocusScope.of(context)` 等用于键盘和焦点处理。
`TextField` 内部有一个 `RepaintBoundary` ,是因为 `TextField` 本身是一个需要频繁更新的控件,而 `TextField` 里的内容变化一般很少需要触发父布局的重绘,**所以 `RepaintBoundary` 的存在让 `TextField` 可以实现性能更好的局部绘制**。
## UnmanagedRestorationScope
`UnmanagedRestorationScope` 大家可能比较少用到,它本身是一个 `InheritedWidget` ,主要是往下共享一个 `RestorationBucket` ,而 **`RestorationBucket` 主要是和实现状态的保存/恢复有关系**。
例如应用因为低内存在后台被回收时,可以通过它在重新回到 App 时恢复指定的数据,举个例子:
```dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// Give your RootRestorationScope an id, defaults to null.
restorationScopeId: 'root',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
// Our state should be mixed-in with RestorationMixin
class _HomePageState extends State<HomePage> with RestorationMixin {
// For each state, we need to use a restorable property
final RestorableInt _index = RestorableInt(0);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Index is ${_index.value}')),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index.value,
onTap: (i) => setState(() => _index.value = i),
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home'
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications),
label: 'Notifications'
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings'
),
],
),
);
}
@override
// The restoration bucket id for this page,
// let's give it the name of our page!
String get restorationId => 'home_page';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
// Register our property to be saved every time it changes,
// and to be restored every time our app is killed by the OS!
registerForRestoration(_index, 'nav_bar_index');
}
}
```
如上代码所示:
- 首先给 `MaterialApp` 配置 `restorationScopeId`(必须配置才算开启该功能)。
- 使用 `RestorableInt` 用于配置和保存 `BottomNavigationBar``index`
- 在 `State` 混入 `RestorationMixin` 并且在 `restoreState` 方法里恢复 `index` 的状态;
其中默认 `MaterialApp` 内部用到了 `RootRestorationScope` 而`RootRestorationScope` 的内部就是 `UnmanagedRestorationScope`;上述例子运行后通过打开模拟器开发者设置里的 *`Don't keep activities`* 就可以看到效果。
> 以上示例来自 [《Introduction to State Restoration in Flutter》](https://dev.to/pedromassango/what-is-state-restoration-and-how-to-use-it-in-flutter-5blm) 。
回到 `TextField`,在 `_TextFieldState` 里就混入了 `RestorationMixin`,然后使用 `RestorableTextEditingController` 用于用于恢复 `TextEditingController`
> 因为输入框的内容默认保存在了 `TextEditingController``TextEditingValue` 里,所以这里用的是 `RestorableTextEditingController`
![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f75592dae3b54a4aa2a517f6f9197637~tplv-k3u1fbpfcp-watermark.image?)
一般情况下是使用 `MaterialApp` 内部默认自带了一个 `RootRestorationScope` ,所以我们只需要给 `MaterialApp` 设置 `restorationScopeId`,而 **`TextFild` 通过内置 `UnmanagedRestorationScope` 相关的逻辑,最终实现了文本内容的保存与恢复**。
## EditableText
`EditableText` 就不用多说了,`TextField` 的本体,内部主要通过 `Scrollable` 来实现滑动,同样的它也用了对应的 `restorationId` 来实现恢复和缓存。
**首先注意到可以滑动这一点,可以看到对于 `EditableText` 来说,它其实是一个 “ViewPort”是根据 `ViewportOffset` 来实现滑动效果**。
而对于 `EditableText` 内部,**它使用了 `CompositedTransformTarget` 来实现 Toolbar 和输入框的联动**,也就是输入控件和长按“粘贴/复制”弹出框之间的关联。
**所以这里简单介绍下 `CompositedTransformTarget`,它通常和 `CompositedTransformFollower` 一起被用于控件之间的联动效果**。
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/658acbd4157f439baa33791f20580a19~tplv-k3u1fbpfcp-zoom-1.image)
如上图所示,常见内置的 `Slider`,在滑动的弹出部分实现,就是通过 `CompositedTransformTarget``CompositedTransformFollower` 的结合实现,**它可以让一个控件跟随另外一个控件而无需计算位置,它们之间主要是通过 `LayerLink` 链接在一起**。
回到 `TextField`,其实除了 “复制/粘贴” 的 Toolbar ,关于 selection 选中区域的内容,`EditableText` 内部也是通过类似的方式实现,只是这里是直接通过 `LeaderLayer` 而不是通过它的封装 `CompositedTransformTarget` 去实现。
> 对于使用 `CompositedTransformTarget` 有兴趣的可以参考https://juejin.cn/post/6946416845537116190
当然使用 `CompositedTransformTarget` 还是会有“比较大”的性能开销,不建议大规模频繁使用,因为毕竟它属于一个 `pushLayer` 的操作。
另外 `EditableText` 内部绘制内容的部分,主要就是大家都知道的 `TextPainter` ,这部分就没什么特别,暂时不详细展开。
**所以本篇主要是通过介绍 `TextField` 的组成,以及解释内部各组成部分的作用,让开发者可以更清晰的了解 Flutter 里常用的文本输入框的实现,当遇上问题或者需求时,可以快速定位和解决问题**,例如:
- ”粘贴/复制“ 的 Toolbar 是哪里弹出;
- Toolbar 是如何定位和布局;
- 点击 `TextField` 是如何弹出键盘和处理手势事件;
- `TextField` 如何做到局部绘制;
- ...
最后介绍一个简单的问题,之前有人刚好问我:***如何在 Flutter 上实现类似微信聊天输入框从一行到多行的输入框效果***,如下图代码所示,就是这么简单:
```dart
TextField(
focusNode: _focusNode,
maxLines: 7,
minLines: 1,
decoration:
const InputDecoration(border: OutlineInputBorder()),
)
```
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58a5b4f0beac4ad1b8a86f1b84283ef9~tplv-k3u1fbpfcp-watermark.image?)

View File

@ -1,161 +0,0 @@
在移动开发中图文混排是十分常见的业务需求,如下图效果所示,本篇将介绍在 Flutter 中的图文混排效果与实现原理。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1a3d468ff0d4?w=640&h=960&f=gif&s=3253050)
事实上针对如上所示的图文混排需求Flutter 官方提供了十分便捷的实现方式: **`WidgetSpan`** 。
如下代码所示,**通过 `Text.rich` 接入 `TextSpan``WidgetSpan` 就可以快速实现图文混排的需求,并且可以看出 `WidgetSpan` 不止支持图片控件**,它可以接入任何你需要的 `Widget` ,比如 `Card` 、`InkWell` 等等。
```
Text.rich(TextSpan(
children: <InlineSpan>[
TextSpan(text: 'Flutter is'),
WidgetSpan(
child: SizedBox(
width: 120,
height: 50,
child: Card(
color: Colors.blue,
child: Center(child: Text('Hello World!'))),
)),
WidgetSpan(
child: SizedBox(
width: size > 0 ? size : 0,
height: size > 0 ? size : 0,
child: new Image.asset(
"static/gsy_cat.png",
fit: BoxFit.cover,
),
)),
TextSpan(text: 'the best!'),
],
)
```
也就是说 **`WidgetSpan` 支持在文本中插入任意控件**,这大大提升了 Flutter 中富文本的自定义效果,比如上述演示效果中随意改变图片的大小。
**那为什么 `WidgetSpan` 可以如何方便地实现文本和 Widget 混合效果呢?这就要从 `Text` 的实现说起**。
## 实现原理
我们常用的 `Text` 控件其实只是 `RichText` 的封装,而 `RichText` 的实现如下图所示,主要可以分为三部分:**`MultiChildRenderObjectWidget`** 、 **`MultiChildRenderObjectElement`** 和 **`RenderParagraph`** 。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf20cd793c622?w=820&h=376&f=png&s=18080)
正如我们知道的, Flutter 控件一般是由 `Widget`、`Element` 和 `RenderObeject` 三部分组成,而在 `RichText` 中也是如此,其中:
- `RenderParagraph` 主要是负责文本绘制、布局相关;
- `RichText` 继承 `MultiChildRenderObjectWidget` 主要是需要通过 `MultiChildRenderObjectElement` 来处理 `WidgetSpan` 中 children 控件的插入和管理。
#### 那 `WidgetSpan` 究竟是如何混入在文本绘制中呢?
在前面的使用中,我们首先是传入了一个 `TextSpan``RichText` ,并在 `TextSpan``children` 中拼接我们需要的内容,那就从 `RichText` 开始挖掘其中的原理。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf27cec3d02a3?w=1656&h=1196&f=png&s=296044)
如上代码所示,这里我们首先看 `RichText` 的入口,可以看到 `RichText` 开始是有一个 `_extractChildren` 方法,这个方法主要是将传入 `TextSpan``children` 里,所有的 `WidgetSpan` 通过 `visitChildren` 方法给递归筛选出来,然后传入给父类 `MultiChildRenderObjectWidget`
> 为什么需要这么做?在 [《十六、详解自定义布局实战》](https://mp.weixin.qq.com/s/zwKG0ehMRPoRidRPtGGUpQ) 中介绍过,`MultiChildRenderObjectWidget` 的 children 最终会通过 `MultiChildRenderObjectElement` 作为桥梁,然后被插入到需要管理和绘制的 child 链表结构中,这样在 `RenderObject` 中方便管理和访问。
另外我们知道 `RichText` 传入的 `text` 其实是一个 `InlineSpan` ,而 `TextSpan` 就是 `InlineSpan` 的子类,`WidgetSpan` 也是 `InlineSpan` 的子类实现,它们的关系如下图所示:
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf329b2f84541?w=435&h=384&f=png&s=12383)
对于 `InlineSpan` 系列我们主要关注两个方法:**`visitChildren` 和 `build`** 方法,它的子类 `TextSpan``WidgetSpan` 都对这两个方法有自己对应的实现。
```
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions });
bool visitChildren(InlineSpanVisitor visitor);
```
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1f88c35526f4?w=725&h=361&f=png&s=66530)
接着看 `RenderParagraph` ,如上代码所示,`RichText` 中的 `text``InlineSpan` 会继续被传入到 `RenderParagraph` 中,`RenderParagraph` 继承了 `RenderBox` 并混入的 `ContainerRenderObjectMixin``RenderBoxContainerDefaultsMixin` 等。
> 混入的对象这部分在内容在 [《十六、详解自定义布局实战》](https://mp.weixin.qq.com/s/zwKG0ehMRPoRidRPtGGUpQ) 也介绍过,这里只需要知道通过混入它们, `RenderParagraph` 就可以获得前面通过 `WidgetSpan` 传入到 `MultiChildRenderObjectElement` 的 children 链表,并且布局计算大小等。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1f9f41f02c1e?w=700&h=103&f=png&s=22593)
之后 `RenderParagraph` 中的 `text` 之后会被放置到 `TextPainter` 中使用,并且通过 `_extractPlaceholderSpans` 方法将所有的 `PlaceholderSpans` 筛选出来。
`TextPainter` 主要用于实现文本的绘制,这里我们暂时不多分析,**而 `_extractPlaceholderSpans` 挑选出来的所有 `PlaceholderSpans` ,其实就是 `WidgetSpan`**。
> `WidgetSpan` 是通过继承 `PlaceholderSpans` 从而实现了 `InlineSpan`,而目前暂时 `PlaceholderSpans` 实现的类只有 `WidgetSpan`
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf3a24fb5fa55?w=1660&h=1310&f=png&s=276358)
挑选出来的 `List<PlaceholderSpan>` 们会在 `RenderParagraph` 计算宽高等方法中被用到,比如 `computeMaxIntrinsicWidth` 方法等,**其中主要有 `_canComputeIntrinsics``_computeChildrenWidthWithMaxIntrinsics` 、`_layoutText` 三个关键**方法,这三个方法结合处理了 `RenderParagraph` 中 Span 的尺寸和布局等。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf442a6aec39f?w=1174&h=382&f=png&s=70717)
- **`_canComputeIntrinsics`** `_canComputeIntrinsics` 主要判断了 `PlaceholderSpan` 只支持的 `baseline` 配置。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf43051ecb143?w=1630&h=876&f=png&s=197844)
- **`_computeChildrenWidthWithMaxIntrinsics`** `_computeChildrenWidthWithMaxIntrinsics` 中会**通过 `PlaceholderSpan` 去对应得到 `PlaceholderDimensions`**,得到的 `PlaceholderDimensions` 会用于后续如 `WidgetSpan` 的大小绘制信息。
> 这个 `PlaceholderDimensions` 会通过 `setPlaceholderDimensions` 方法设置到 `TextPainter` 里面, 这样 `TextPainter``layout` 的时候,就会将 `PlaceholderDimensions` 赋予 `WidgetSpan` 大小信息。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf47d04cfad49?w=1986&h=618&f=png&s=167597)
- **`_layoutText`**: `_layoutText` 方法会调用 `_textPainter.layout` 从而执行 `_text.build` 方法,这个方法就会触发 `children` 中的 `WidgetSpan` 去执行 `build`
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf49c154770a6?w=1666&h=372&f=png&s=77258)
所以如下代码所示,`_textPainter.layout` 会执行 Span 的 `build` 方法,将 `PlaceholderDimensions` 设置到 `WidgetSpan` 里面,然后还有**通过 `_paragraph.getBoxesForPlaceholders()` 方法获取到控件绘制需要的 `left`、`right` 等信息**,这些信息来源是基于上面 `text.build` 的执行。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2747536db400?w=1287&h=519&f=png&s=131061)
> _paragraph.getBoxesForPlaceholders() 获取到的 `TextBox` 信息,是基于后面我们介绍在 Span 里提交的 `addPlaceholder` 方法获取。
这些信息会在 `setParentData` 方法中被设置到 `TextParentData` 里,关于 `ParentData` 及其子类的作用,在[《十六、详解自定义布局实战》](https://mp.weixin.qq.com/s/zwKG0ehMRPoRidRPtGGUpQ) 同样有所介绍,这里就不赘述了,简单理解就是 `WidgetSpan` 绘制的时候所需要的 `offset` 位置信息会由它们提供。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2714de8544c5?w=765&h=373&f=png&s=60124)
之后如下代码所示, `WidgetSpan``build` 方法被执行,这里会有一个 `placeholderCount` `placeholderCount` 默认是从 0 开始,而在执行 `addPlaceholder` 方法时会通过 `_placeholderCount++` 自增,这样下一个 `WidgetSpan` 就会拿到下一个 `PlaceholderDimensions` 用于设置大小。
> `addPlaceholder` 之后会执行到 Flutter Engine 中的流程了。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1adb94b94f3b?w=1154&h=673&f=png&s=126401)
最终 `RenderParagrash``paint` 方法会执行 `_textPainter.paint` 并把确定了大小和位置的 child 提交绘制。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2692f63b5cbc?w=673&h=671&f=png&s=85782)
是不是有点晕,结合下图所示,总结起来其实就是:
- `RichText` 中传入 `TextSpan` `TextSpan` 的 children 中使用 `WidgetSpan` `WidgetSpan` 里的 `Widget` 们会转成 `MultiChildRenderObjectElement``children` 处理后得到一个 child 链表结构;
- 之后 `TextSpan` 进入 `RenderParagrash` ,会抽取出对应 `PlaceholderSpan``WidgetSpan`),然后通过转化为 `PlaceholderDimensions` 保存大小等信息;
- 之后进去 `TextPainter` 会触发 `InlineSpan``build` 方法,从而将前面得到的 `PlaceholderDimensions` 传递到 `WidgetSpan` 中;
- `WidgetSpan` 中的控件信息通过 `addPlaceholder` 会被传递到 `Paragraph`
- 之后 `TextPainter` 中通过 `addPlaceholder` 的信息获取,调用 `_paragraph.getBoxesForPlaceholders()` 获取去控件绘制需要的 `offset`
- 有了大小和位置,最终文本中插入的控件,会在 `RenderParagrash``paint` 方法被绘制。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2834036c5a2a?w=2342&h=1414&f=png&s=151074)
**`RichText` 中插入控件的管理巧妙的依托到 `MultiChildRenderObjectWidget` 中,从而复用了原本控件的管理逻辑,之后依托引擎计算位置从而绘制完成。**
至此,简简单单的 `WidgetSpan` 的实现原理解析完成~
### 资源推荐
- Github https://github.com/CarGuo
- 开源 Flutter 完整项目https://github.com/CarGuo/GSYGithubAppFlutter
- 开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo
- 开源 Fluttre 实战电子书项目https://github.com/CarGuo/GSYFlutterBook
- 开源 React Native 项目https://github.com/CarGuo/GSYGithubApp
![](https://user-gold-cdn.xitu.io/2020/3/13/170d20b33b114566?w=2569&h=2518&f=jpeg&s=856467)

View File

@ -109,6 +109,7 @@
* [Flutter 里的语法糖解析,知其所然方能潇洒舞剑](Flutter-SU.md)
* [Flutter 实现完美的双向聊天列表效果,滑动列表的知识点](Flutter-SC.md)
* [Flutter 启动页的前世今生适配历程](Flutter-LA.md)
* [Flutter 快速解析 TextField 的内部原理](Flutter-TE.md)