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
❞

正常效果:
2

非正常效果:
3

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。

5

「也就是当 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一起被用于控件之间的联动效果」。

8

如上图所示,常见内置的 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()),
)
请 注册 或 登录 后评论
    // 作者
    shuyu1991 发布于 声网开发者社区
    • 0
    // 本帖子
    关键词
    // 相关帖子
    Coming soon...
    • 0
    Flutter 快速解析 TextField 的内部原理shuyu1991 发布于 声网开发者社区