Flutter 快速解析 TextField 的内部原理
❝
「看完本篇,你不仅会了解到 TextField 的实现和构成,还可以学到很多之前不常用的“奇怪”知识」。
❞
在 Flutter 里 TextField 是一个比较复杂的控件,而在整个 TextField 里嵌套了许多不同实现的控件,它们组成了我们常用的输入框效果,「如下图所示是关于 TextField 的主要构成部分」,也是本篇主要讲解的内容。
FocusTrapArea
FocusTrapArea 大家可能会比较陌生,这个是最近的版本里才出现的控件,FocusTrapArea 本身并没有特别,它仅仅是在 RenderObject tree 里塞进去了一个 FocusNode。
它的出现主要是为了 Web/Desktop 平台,通过增加了 FocusTrapArea 之后,在 Web/Desktop 平台执行 TextEditingController.clear 的时候,TextField 还能继续保持之前获得的焦点。
❝
具体可见 Flutter 的 issues : #86154 、#86041
❞
正常效果:
非正常效果:
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的处理流程:
如上代码所示:
- 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。
「也就是当 FocusNode 和 TextEditingController 这两者发生改变的时候,会让 InputDecorator 重新 rebuild 从而改变渲染效果」,例如:输入框输入内容时、焦点发生改变时修改输入框的背景颜色。
❝
注意别搞混了InputDecorator和InputDecoration,InputDecoration是用来配置InputDecorator。
❞
所以可以看到 InputDecorator 有很丰富的参数和配置,开发者可以通过 InputDecoration 来配置很丰富的输入框 UI 效果,「但是如果刚好出现某些位置,或者某些缝隙不满足产品诡异的需求时,那恭喜你,你开启了 Flutter 高级开发的修炼之路」。
为什么呢?
简单来说 InputDecorator 的实现是在内部是一个自定义的 RenderBox,其中和 layout 相关部分就有 600 多行的代码,也就是根据 InputDecoration 的 icon、prefixIcon、suffix 等参数,进行定位布局,计算位置方向,根据基线调整位置等等。
❝
另外InputDecorator 里的动画效果主要是通过内部的 AnimatedOpacity 等完成。
❞
所以对于 InputDecorator 来说,如果你对于某些位置或者边界效果不满意,要么你就重构一个自己的实现,要么可能就要选择“委曲求全”。
RepaintBoundary
为什么 TextField 内部会有一个 RepaintBoundary ?首先 RepaintBoundary 是干嘛的?
之前在 《Flutter 画面渲染的全面解析》 详细介绍过这部分的知识,这简单不严谨地说就是: 「RepaintBoundary 主要是用于形成一个 Layer,得到一个独立的绘制区域」。
常见的就是 Navigator 的页面跳转,内部基础实现都有一个 RepaintBoundary 来保证每个区域都是独立的绘制区域。
❝
另外说到Navigator就不得不说每个页面也都有自己的FocusScope, 也就是我们常用的FocusScope.of(context)等用于键盘和焦点处理。
❞
在TextField内部有一个RepaintBoundary,是因为TextField本身是一个需要频繁更新的控件,而TextField里的内容变化一般很少需要触发父布局的重绘,「所以RepaintBoundary的存在让TextField可以实现性能更好的局部绘制」。
UnmanagedRestorationScope
UnmanagedRestorationScope 大家可能比较少用到,它本身是一个 InheritedWidget ,主要是往下共享一个 RestorationBucket ,而 「RestorationBucket 主要是和实现状态的保存/恢复有关系」。
例如应用因为低内存在后台被回收时,可以通过它在重新回到 App 时恢复指定的数据,举个例子:
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》 。
❞
回到 TextField,在 _TextFieldState 里就混入了 RestorationMixin,然后使用 RestorableTextEditingController 用于用于恢复 TextEditingController 。
❝
因为输入框的内容默认保存在了TextEditingController的TextEditingValue里,所以这里用的是RestorableTextEditingController。
❞
一般情况下是使用 MaterialApp 内部默认自带了一个 RootRestorationScope ,所以我们只需要给 MaterialApp 设置 restorationScopeId,而 「TextFild 通过内置 UnmanagedRestorationScope相关的逻辑,最终实现了文本内容的保存与恢复」。
EditableText
EditableText 就不用多说了,TextField 的本体,内部主要通过 Scrollable 来实现滑动,同样的它也用了对应的 restorationId 来实现恢复和缓存。
「首先注意到可以滑动这一点,可以看到对于 ``EditableText 来说,它其实是一个 “ViewPort”,是根据 ViewportOffset 来实现滑动效果」。
而对于 EditableText 内部,「它使用了 CompositedTransformTarget 来实现 Toolbar 和输入框的联动」,也就是输入控件和长按“粘贴/复制”弹出框之间的关联。
「所以这里简单介绍下 CompositedTransformTarget,它通常和 CompositedTransformFollower一起被用于控件之间的联动效果」。
如上图所示,常见内置的 Slider,在滑动的弹出部分实现,就是通过 CompositedTransformTarget 和 CompositedTransformFollower 的结合实现,「它可以让一个控件跟随另外一个控件而无需计算位置,它们之间主要是通过 LayerLink 链接在一起」。
回到 TextField,其实除了 “复制/粘贴” 的 Toolbar ,关于 selection 选中区域的内容,EditableText 内部也是通过类似的方式实现,只是这里是直接通过 LeaderLayer 而不是通过它的封装 CompositedTransformTarget 去实现。
❝
对于使用CompositedTransformTarget有兴趣的可以参考:Flutter 组件 | 手牵手,一起走 CompositedTransformFollower 与 CompositedTransformTarget - 掘金
❞
当然使用 CompositedTransformTarget 还是会有“比较大”的性能开销,不建议大规模频繁使用,因为毕竟它属于一个 pushLayer 的操作。
另外 EditableText 内部绘制内容的部分,主要就是大家都知道的 TextPainter ,这部分就没什么特别,暂时不详细展开。
「所以本篇主要是通过介绍 TextField 的组成,以及解释内部各组成部分的作用,让开发者可以更清晰的了解 Flutter 里常用的文本输入框的实现,当遇上问题或者需求时,可以快速定位和解决问题」,例如:
- ”粘贴/复制“ 的 Toolbar 是哪里弹出;
- Toolbar 是如何定位和布局;
- 点击 TextField 是如何弹出键盘和处理手势事件;
- TextField 如何做到局部绘制;
- …
最后介绍一个简单的问题,之前有人刚好问我:「如何在 Flutter 上实现类似微信聊天输入框从一行到多行的输入框效果」,如下图代码所示,就是这么简单:
TextField(
focusNode: _focusNode,
maxLines: 7,
minLines: 1,
decoration:
const InputDecoration(border: OutlineInputBorder()),
)



