本文翻译自
原文地址:How text editing works internally in Flutter
原作者:Suragch
【个人阅读心得】这篇文章由表及里介绍了Flutter中文本绘制和编辑的处理流程,讲了各个层级所负责的内容,能够帮助读者对文本处理有个大致认知,原来文本绘制和编辑在底层是很复杂的系统,包括如何和系统键盘打交道,怎样布局输入框里的内容,以及怎么处理和底层框架交互等。想要更深入的理解,可以一步一步按照文章中介绍的层级去阅读源码,此外文中的引文Flutter文本渲染对渲染过程进行了详细讲解,非常推荐延伸阅读一下。得益于这篇文章中的Text and selection
一节,我们的小伙伴优化了TextField输入精度计算,解决了中文输入最大限制个数计算不准确的困扰。此外,翻译不准确的地方欢迎大家指正。
写给那些喜欢钻研细节的人。
由于Flutter仅支持水平文本布局,因此在为了给传统蒙古文字创建垂直布局的widget时,我不得不深入研究文本渲染系统的底层源码。在本文中,我将分享我发现的,关于Flutter中文本编辑widget工作方式的知识。
文本渲染(Text rendering)
通常我们看到的关于Text widget是这样子的:
Text(
'Hello world',
style: TextStyle(fontSize: 30),
),
然而,它的下面还有很多层级。
Widget层(Widgets layer)
当你使用Text
widget时,它实际创建的是一个RichText widget。
与Text(将String作为数据参数)不同,RickText采用TextSpan(更准确的说是InlineSpan, 稍后会介绍更多相关内容),TextSpan需要String和一个TextStyle作为参数,因此在Text创建RichText之前,它需要你提供任意TextStyle并将其与APP默认的TextStyle组合,该默认TextStyle是从BuildContext中获取的,下面是上图的更详细版本:
由于Text仅采用单个字符串和style样式作为参数,因此它在TextSpan中提供给RichText的字符串只有一种样式,但是TextSpan很有趣,就像多子节点widget一样,它能将更多的文本对象作为子节点。这意味着你可以使用它来构建整个文本树,其中每个节点都可以有各自的字符串和样式,
但是这很难推导出来,因为通常你只有一个父TextSpan和一堆子节点
你可以将TextSpan对象传递给Text.rich
构造函数,或者直接传递给TextRich widget,但是,如果将其直接传递给TextRich widget的话,该样式则不会和build context中的默认样式组合。
阅读掌握Flutter中的文本样式一文,以获得使用TextSpan设置文本样式的实际示例。
RichText本身是MultiChildRenderObjectWidget的子类,RichText内部有多个子节点的原因是,你可以在一行的文本内部散布其他的widget,上面的图片显示的是一个TextSpan树,但实际上RichText内置的是InlineSpan,InlineSpan既可以是TextSpan也可以是WidgetSpan,它是他们的父类。出于本文的目的,我将仅处理TextSpan。
渲染层(Rendering layer)
Widget只是最终用于创建RenderObject的蓝图,RichText创建的渲染对象叫做RenderParagraph
RenderParagraph会计算文本的大小,当然也会处理其他事情,比如命中测试以及它的任何子widget的布局。
绘制层(Painting layer)
RenderParagraph并不直接绘制文本,而是创建一个TextPainter来管理这些工作。
与他的名字相反,TextPainter实际上也不绘制文本,但是它会管理周围所有绘制相关的工作,包括要绘制的画布Canvas以及从TextSpan树上提取样式和字符串。
此外,TextSpan还可以处理插入符的的位置,这对于Text widget不那么重要,但是对于接下来要讲的文本编辑将会很重要。
基础层(Foundation layer)
每个人都以为穿过重重困难,即将真正绘制文本的时候,实际则不止于此。
在Flutter框架的最底层,你将会看到一个ParagraphBuilder和一个Paragraph对象,TextPainter内部会创建ParagraphBuilder,而ParagraphBuilder又会用于生成Paragraph。
ParagraphBuilder中包含一点逻辑,但是Paragraph几乎不包含任何逻辑,这两个对象将大部分工作传给Flutter engine。
Flutter引擎层(Flutter engine layer)
Flutter引擎是用C/C++写的,因此对于使用Dart的Flutter开发者,通常不会直接使用,处理文本绘制的库叫做LibTxt。
LibTxt当前已经被Skia的SkParagraph模块替换,可以关注这个issue跟踪进展。
总结
下面的图片展示了渲染工作的所有图层
阅读Flutter文本渲染这篇文章,深入了解以上内容。【译者按:强烈推荐阅读,之后也会翻译出来或者总结学习笔记】
文本编辑(Text editing)
我的一个问题是文本渲染和文本编辑在架构层上有多少是共享的,答案是从TextPainter向下的所有内容,这就很方便了,意味着我们只需要了解它上层的内容。
Material和Cupertino层(Material and Cupertino layers)
当你想要在Flutter中输入和编辑的时候,TextField可能是用户首先想到的widghet。
TextField(
decoration: InputDecoration(
hintText: 'Search',
),
),
然而,TextField只是组成Material库的一部分,这是widget层更上的一层,与之对应的在Cupertino库中widget称为CupertinoTextField
假如你使用TextField.adaptive
构造函数的话,它将会在iOS和macOS创建CupertinoTextField,但是在其它平台创建TextField。
你可能也想知道TextFormField,但这仅是一个TextField,包装了一些逻辑,使保存和验证更加容易。据我所知,没有CupertinoTextFormField。
与Text这种无状态(stateless)widget不同,TextField和CupertinoTextField是有状态的(statefull)widget。这是因为他们需要持续跟踪诸如TextEditingController、焦点(focus)、鼠标悬停(mouse hovering)、手势(gestures)等之类的东西。
无论使用哪种,TextField和Cupertino TextField都将最终创建一个EditableText widget。
然后我们就进入到了widget层。
Widget层(Widgets layer)
在这一层,你将不会再具有某些上一层提供的功能,例如占位符文本和标签,不过这里有很多其他属性。
EditableText管理文本和选择区域,与键盘、光标和滚动事件进行通信。
文本和选择区域(Text and selection)
你可能之前使用过TextEditingController,尽管它本身并不是一个widget,但是它属于widget层,用来处理EditableText。继承自ValueNotifier,当TextEditingValue发生改变的时候,通知它的监听者。
TextEditingValue对象有三个部分组成,文本、选择区域和编辑区域(text, selection, and composing)。
- text:这里是用户已经输入的任何字符串。
- selection:这是一个TextSelection对象,通过它你可以知道当前所选择的光标位置和选择范围,除了选择的开始和结束值外,TextSelection也包含文本方向和光标在换行处的精确位置。
- composing:这是你正在编辑单词的TextRange(仅包含开始和结束的偏移量)。你知道当你在输入某些内容的时候,键盘会提出一些建议吗?如果你选择键盘建议,则带下划线的文本将会被你选择的键盘建议文本替换掉。
与系统键盘进行通信(Communicating with the system keyboard)
当弹出系统软键盘时,它属于底层系统,Flutter的Editable widget需要一种与该系统通信的方式,无论是获取信息(用户使用键盘输入)还是发送信息(如更改选择区域,或者让键盘消失)。
下面是架构示意图:
- EditableText实现TextInputClient,当用户使用系统键盘输入文本时,它可以接收更新。
- TextInputClient创建一个TextInputConnection对象,它是一个用于将消息发送给系统键盘的接口。
- 异步消息传递全部通过底层的TextInput服务,该服务通过平台channel与底层系统进行通信。
- 在native侧,plugin插件将会处理往返于TextInput的消息,每个平台都有自己唯一的插件,用于Android系统的Android插件,用于iOS系统的iOS插件,该插件将与本机系统输入控件进行通信(比如键盘)。
- Flutter端和插件端各自维护自己的TextEditingValue版本,并且需要保持同步。
光标(Cursor)
EditableText使用了两个AnimationController对象为光标设置动画。一个用于标准的光标闪烁(通过动画的透明度实现),另一个用于浮动光标,这时iOS系统中的标准样式,你可以在下面视频中看到
滚动(Scrolling)
在EditableText的build
方法中,内容由Scrollable widget进行包裹了一层,这允许文本垂直滚动以显示多行文本,水平滚动以显示单行文本。传入ScrollController可以进行进一步的定制滚动行为。
创建渲染对象(Creating a render object)
EditableText创建一个称为RenderEditable渲染对象。
渲染层(Rendering layer)
RenderEditable管理命中测试、文本、和光标或者选择区域,以及从字符到字符、单词到单词的移动。最后,它使用TextPainter绘制文本和选择区域。
这样就完成了和开头内容的呼应!如你所见,TextPainter创建了一个Paragraph对象,该对象将将绘制工作传给Flutter引擎。
总览
下图是上面内容的总结:
你可以看看它与更大的Flutter架构是如何匹配的:
最后
很高兴你能看完,假如发现任何错误,请在评论中告诉我,这样其他读者也能看到。