0
点赞
收藏
分享

微信扫一扫

Flutter 快速解析 TextField 的内部原理


看完本篇,你不仅会了解到 TextField 的实现和构成,还可以学到很多之前不常用的“奇怪”知识


在 Flutter 里 ​​TextField​​​ 是一个比较复杂的控件,而在整个 ​​TextField​​ 里嵌套了许多不同实现的控件,它们组成了我们常用的输入框效果,如下图所示是关于 TextField 的主要构成部分,也是本篇主要讲解的内容。

Flutter 快速解析 TextField 的内部原理_控件

FocusTrapArea

​FocusTrapArea​​​ 大家可能会比较陌生,这个是最近的版本里才出现的控件,​​FocusTrapArea​​​ 本身并没有特别,它仅仅是在 ​​RenderObject​​​ tree 里塞进去了一个 ​​FocusNode​​。

它的出现主要是为了 Web/Desktop 平台,通过增加了 ​​FocusTrapArea​​​ 之后,在 Web/Desktop 平台执行 ​​TextEditingController.clear​​​ 的时候,​​TextField​​ 还能继续保持之前获得的焦点。


具体可见 Flutter 的 issues : ​​#86154​​ 、​​#86041​​


正常效果

非正常效果

​​Flutter 快速解析 TextField 的内部原理_控件_02​​

​​Flutter 快速解析 TextField 的内部原理_控件_03​​

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​​ 的处理流程:

Flutter 快速解析 TextField 的内部原理_ico_04

如上代码所示:

  • 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​​。

Flutter 快速解析 TextField 的内部原理_前端_05

也就是当 FocusNode​TextEditingController​ 这两者发生改变的时候,会让 ​InputDecorator​ 重新 ​rebuild​ 从而改变渲染效果,例如:输入框输入内容时、焦点发生改变时修改输入框的背景颜色。


注意别搞混了 ​​InputDecorator​​​ 和 ​​InputDecoration​​​,​​InputDecoration​​​ 是用来配置 ​​InputDecorator​​。


Flutter 快速解析 TextField 的内部原理_控件_06

所以可以看到 ​​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​​ 。


Flutter 快速解析 TextField 的内部原理_前端_07

一般情况下是使用 ​​MaterialApp​​​ 内部默认自带了一个 ​​RootRestorationScope​​​ ,所以我们只需要给 ​​MaterialApp​​​ 设置 ​​restorationScopeId​​​,而 ​TextFild 通过内置 ​UnmanagedRestorationScope​ 相关的逻辑,最终实现了文本内容的保存与恢复

EditableText

​EditableText​​​ 就不用多说了,​​TextField​​​ 的本体,内部主要通过 ​​Scrollable​​​ 来实现滑动,同样的它也用了对应的 ​​restorationId​​ 来实现恢复和缓存。

首先注意到可以滑动这一点,可以看到对于 EditableText 来说,它其实是一个 “ViewPort”,是根据 ​ViewportOffset​ 来实现滑动效果

而对于 ​​EditableText​​ 内部,它使用了 CompositedTransformTarget 来实现 Toolbar 和输入框的联动,也就是输入控件和长按“粘贴/复制”弹出框之间的关联。

所以这里简单介绍下 CompositedTransformTarget,它通常和 ​CompositedTransformFollower​ 一起被用于控件之间的联动效果

Flutter 快速解析 TextField 的内部原理_Android_08

如上图所示,常见内置的 ​​Slider​​​,在滑动的弹出部分实现,就是通过 ​​CompositedTransformTarget​​​ 和 ​​CompositedTransformFollower​​ 的结合实现,它可以让一个控件跟随另外一个控件而无需计算位置,它们之间主要是通过 LayerLink 链接在一起

回到 ​​TextField​​​,其实除了 “复制/粘贴” 的 Toolbar ,关于 selection 选中区域的内容,​​EditableText​​​ 内部也是通过类似的方式实现,只是这里是直接通过 ​​LeaderLayer​​​ 而不是通过它的封装 ​​CompositedTransformTarget​​ 去实现。


对于使用 ​​CompositedTransformTarget​​ 有兴趣的可以参考:


当然使用 ​​CompositedTransformTarget​​​ 还是会有“比较大”的性能开销,不建议大规模频繁使用,因为毕竟它属于一个 ​​pushLayer​​ 的操作。

另外 ​​EditableText​​​ 内部绘制内容的部分,主要就是大家都知道的 ​​TextPainter​​ ,这部分就没什么特别,暂时不详细展开。

所以本篇主要是通过介绍 TextField 的组成,以及解释内部各组成部分的作用,让开发者可以更清晰的了解 Flutter 里常用的文本输入框的实现,当遇上问题或者需求时,可以快速定位和解决问题,例如:

  • ”粘贴/复制“ 的 Toolbar 是哪里弹出;
  • Toolbar 是如何定位和布局;
  • 点击​​TextField​​ 是如何弹出键盘和处理手势事件;
  • ​TextField​​ 如何做到局部绘制;
  • ...

最后介绍一个简单的问题,之前有人刚好问我:如何在 Flutter 上实现类似微信聊天输入框从一行到多行的输入框效果,如下图代码所示,就是这么简单:

TextField(
focusNode: _focusNode,
maxLines: 7,
minLines: 1,
decoration:
const InputDecoration(border: OutlineInputBorder()),
)

Flutter 快速解析 TextField 的内部原理_Android_09

举报

相关推荐

0 条评论