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()),
)