文本编辑在Flutter中的工作方式

本文翻译自
原文地址:How text editing works internally in Flutter
原作者:Suragch

【个人阅读心得】这篇文章由表及里介绍了Flutter中文本绘制和编辑的处理流程,讲了各个层级所负责的内容,能够帮助读者对文本处理有个大致认知,原来文本绘制和编辑在底层是很复杂的系统,包括如何和系统键盘打交道,怎样布局输入框里的内容,以及怎么处理和底层框架交互等。想要更深入的理解,可以一步一步按照文章中介绍的层级去阅读源码,此外文中的引文Flutter文本渲染对渲染过程进行了详细讲解,非常推荐延伸阅读一下。得益于这篇文章中的Text and selection一节,我们的小伙伴优化了TextField输入精度计算,解决了中文输入最大限制个数计算不准确的困扰。此外,翻译不准确的地方欢迎大家指正。


写给那些喜欢钻研细节的人。

hellowo01.png

由于Flutter仅支持水平文本布局,因此在为了给传统蒙古文字创建垂直布局的widget时,我不得不深入研究文本渲染系统的底层源码。在本文中,我将分享我发现的,关于Flutter中文本编辑widget工作方式的知识。

文本渲染(Text rendering)

通常我们看到的关于Text widget是这样子的:

Text(
  'Hello world',
  style: TextStyle(fontSize: 30),
),

然而,它的下面还有很多层级。

Widget层(Widgets layer)

当你使用Textwidget时,它实际创建的是一个RichText widget。

text_richtext02.png

与Text(将String作为数据参数)不同,RickText采用TextSpan(更准确的说是InlineSpan, 稍后会介绍更多相关内容),TextSpan需要String和一个TextStyle作为参数,因此在Text创建RichText之前,它需要你提供任意TextStyle并将其与APP默认的TextStyle组合,该默认TextStyle是从BuildContext中获取的,下面是上图的更详细版本:

richtext03.png

由于Text仅采用单个字符串和style样式作为参数,因此它在TextSpan中提供给RichText的字符串只有一种样式,但是TextSpan很有趣,就像多子节点widget一样,它能将更多的文本对象作为子节点。这意味着你可以使用它来构建整个文本树,其中每个节点都可以有各自的字符串和样式,

textspan04.png

但是这很难推导出来,因为通常你只有一个父TextSpan和一堆子节点

textspan05.png

你可以将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

renderParagraph06.png

RenderParagraph会计算文本的大小,当然也会处理其他事情,比如命中测试以及它的任何子widget的布局。

绘制层(Painting layer)

RenderParagraph并不直接绘制文本,而是创建一个TextPainter来管理这些工作。

textpainter07.png

与他的名字相反,TextPainter实际上也不绘制文本,但是它会管理周围所有绘制相关的工作,包括要绘制的画布Canvas以及从TextSpan树上提取样式和字符串。

此外,TextSpan还可以处理插入符的的位置,这对于Text widget不那么重要,但是对于接下来要讲的文本编辑将会很重要。

基础层(Foundation layer)

每个人都以为穿过重重困难,即将真正绘制文本的时候,实际则不止于此。

在Flutter框架的最底层,你将会看到一个ParagraphBuilder和一个Paragraph对象,TextPainter内部会创建ParagraphBuilder,而ParagraphBuilder又会用于生成Paragraph。

paragraph08.png

ParagraphBuilder中包含一点逻辑,但是Paragraph几乎不包含任何逻辑,这两个对象将大部分工作传给Flutter engine。

Flutter引擎层(Flutter engine layer)

Flutter引擎是用C/C++写的,因此对于使用Dart的Flutter开发者,通常不会直接使用,处理文本绘制的库叫做LibTxt。

libtxt09.png

LibTxt当前已经被Skia的SkParagraph模块替换,可以关注这个issue跟踪进展。

总结

下面的图片展示了渲染工作的所有图层

layer10.png

阅读Flutter文本渲染这篇文章,深入了解以上内容。【译者按:强烈推荐阅读,之后也会翻译出来或者总结学习笔记】

文本编辑(Text editing)

我的一个问题是文本渲染和文本编辑在架构层上有多少是共享的,答案是从TextPainter向下的所有内容,这就很方便了,意味着我们只需要了解它上层的内容。

Material和Cupertino层(Material and Cupertino layers)

当你想要在Flutter中输入和编辑的时候,TextField可能是用户首先想到的widghet。

TextField(
  decoration: InputDecoration(
    hintText: 'Search',
  ),
),

然而,TextField只是组成Material库的一部分,这是widget层更上的一层,与之对应的在Cupertino库中widget称为CupertinoTextField

textfield11.png

假如你使用TextField.adaptive构造函数的话,它将会在iOS和macOS创建CupertinoTextField,但是在其它平台创建TextField。

textfieldadaptive12.png

你可能也想知道TextFormField,但这仅是一个TextField,包装了一些逻辑,使保存和验证更加容易。据我所知,没有CupertinoTextFormField。

与Text这种无状态(stateless)widget不同,TextField和CupertinoTextField是有状态的(statefull)widget。这是因为他们需要持续跟踪诸如TextEditingController、焦点(focus)、鼠标悬停(mouse hovering)、手势(gestures)等之类的东西。

无论使用哪种,TextField和Cupertino TextField都将最终创建一个EditableText widget。

editabletext13.png

然后我们就进入到了widget层。

Widget层(Widgets layer)

在这一层,你将不会再具有某些上一层提供的功能,例如占位符文本和标签,不过这里有很多其他属性。

EditableText管理文本和选择区域,与键盘、光标和滚动事件进行通信。

文本和选择区域(Text and selection)

你可能之前使用过TextEditingController,尽管它本身并不是一个widget,但是它属于widget层,用来处理EditableText。继承自ValueNotifier,当TextEditingValue发生改变的时候,通知它的监听者。

TextEditingValue对象有三个部分组成,文本、选择区域和编辑区域(text, selection, and composing)。

texteditingvalue14.png
  • text:这里是用户已经输入的任何字符串。
  • selection:这是一个TextSelection对象,通过它你可以知道当前所选择的光标位置和选择范围,除了选择的开始和结束值外,TextSelection也包含文本方向和光标在换行处的精确位置。
  • composing:这是你正在编辑单词的TextRange(仅包含开始和结束的偏移量)。你知道当你在输入某些内容的时候,键盘会提出一些建议吗?如果你选择键盘建议,则带下划线的文本将会被你选择的键盘建议文本替换掉。
composing15.png

与系统键盘进行通信(Communicating with the system keyboard)
当弹出系统软键盘时,它属于底层系统,Flutter的Editable widget需要一种与该系统通信的方式,无论是获取信息(用户使用键盘输入)还是发送信息(如更改选择区域,或者让键盘消失)。

下面是架构示意图:


keyboard16.png
  • EditableText实现TextInputClient,当用户使用系统键盘输入文本时,它可以接收更新。
  • TextInputClient创建一个TextInputConnection对象,它是一个用于将消息发送给系统键盘的接口。
  • 异步消息传递全部通过底层的TextInput服务,该服务通过平台channel与底层系统进行通信。
  • 在native侧,plugin插件将会处理往返于TextInput的消息,每个平台都有自己唯一的插件,用于Android系统的Android插件,用于iOS系统的iOS插件,该插件将与本机系统输入控件进行通信(比如键盘)。
  • Flutter端和插件端各自维护自己的TextEditingValue版本,并且需要保持同步。

光标(Cursor)
EditableText使用了两个AnimationController对象为光标设置动画。一个用于标准的光标闪烁(通过动画的透明度实现),另一个用于浮动光标,这时iOS系统中的标准样式,你可以在下面视频中看到

cursor17.png

滚动(Scrolling)
在EditableText的build方法中,内容由Scrollable widget进行包裹了一层,这允许文本垂直滚动以显示多行文本,水平滚动以显示单行文本。传入ScrollController可以进行进一步的定制滚动行为。

创建渲染对象(Creating a render object)
EditableText创建一个称为RenderEditable渲染对象。

rendereditable18.png

渲染层(Rendering layer)

RenderEditable管理命中测试、文本、和光标或者选择区域,以及从字符到字符、单词到单词的移动。最后,它使用TextPainter绘制文本和选择区域。

textpainter19.png

这样就完成了和开头内容的呼应!如你所见,TextPainter创建了一个Paragraph对象,该对象将将绘制工作传给Flutter引擎。

总览

下图是上面内容的总结:

textsummary20.png

你可以看看它与更大的Flutter架构是如何匹配的:

flutterarchitecural21.png

最后

很高兴你能看完,假如发现任何错误,请在评论中告诉我,这样其他读者也能看到。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容