作者:光酒
一、 开篇
协议篇文章,我们介绍了Flutter富文本编辑器协议层的设计。以Slate为例,介绍了协议层设计的几个重要的概念:嵌套Model、Opeartion、Normalizing;站在Slate的肩膀上,让我们有了一个强壮、设计完善的富文本协议层,接下来就让我们看看渲染层是如何实现的;
让我们回顾一下Mural整体的架构设计分层:
渲染层主要工作是将协议Model转换成Widget渲染到屏幕上,以及处理选区、光标的计算和绘制,处理用户的手势交互、键盘交互等一系列工作。
1. Textfield的渲染实现
首先让我们来看下Flutter的TextField是如何渲染的:
如上图所示,Textfield继承自StatefulWidget,会build嵌套的Widget tree,其中有几个比较关键的Widget:
TextSelectionGestureDetector处理手势交互相关的逻辑,比如单击移动光标、长按选择文字展示Toolbar等等。
另一个比较重要的Widget——EditableText;EditableText在build的时候,通过buildTextSpan方法,根据TextEditingValue的普通文本以及composing部分,创建一个Textspan对象给_Editable;最终RenderEditable通过TextPainter将文本绘制到canvas上。
2. Mural的渲染实现
如上图所示,Mural在渲染层的设计上,与原生TextField前面一部分基本是一致的,不同之处从MuralEditable开始,对应到TextField的EditableText。
上面在协议层我们说了,Slate在协议在设计上是与Dom一致的,到Flutter渲染层,就会将Dom树转换成Widget tree,最终渲染到屏幕上。
MuralEditable不再是简单的创建一个TextSpan,而是按照Dom树结构,每一个Element映射成一个Widget;每个Element对应的Widget,创建的RenderObject实现了抽象类:RenderEditorInlineBox。
接下来我们再来看看Element对应的Widget,是怎么处理它的子节点的:
我们以最简单的EditableTextLine为例,包含Leading和Body两部分,Leading负责渲染段落修饰相关的内容,比如有序段落的序号、引用段落前面的装饰竖线等。Body则负责渲染具体的富文本内容,实现了抽象类:RenderEditorTextBox,最终依然将所有的叶子节点转换成InlineSpan,通过TextPainer将文本绘制到屏幕上。
EditorUtils的buildChildren方法实现如下:
3. 光标&选区渲染
光标和选区是富文本编辑器渲染层另外一个需要处理的难点。
与原生TextField相比,Mural在处理光标和选区处理更加复杂;TextField所有输入文本都绘制在一个TextPainter,前面我们说过,Mural每个Element都是一个独立的段落,对应一个RenderObject;在Mural中,我们需要计算用户手势操作不同段落的光标位置以及段落之间的选区计算。
要实现Mural的光标和选区渲染,需要解决如下问题:
• 多Element点击获取TextPosition
• TextPosition to MuralPoint
• 光标位置计算
1) 多Element点击获取TextPosition
如上图所示,当用户点击绿色光点位置之后,首先我们可以根据点击事件确认被点击是哪一个Element所渲染的RenderObject。
首先我们通过globalToLocal方法将手势回调的globalPosition转换为相对于Mural的localPosition;接下来遍历MuralRenderEditable的child,寻找包含localPosition的child。
如上面介绍的,Element渲染的RenderObject实现了RenderEditorInlineBox抽象类,也就可以通过getPositionForOffset方法获取到相对于当前TextPainter的TextPosition。
2) TextPosition to MuralPoint
接下来就要解决第二个问题,如何将TextPosition转换为协议对于光标、选区位置的描述。
以上图为例,点击之后,TextPosition的Offset为12,而Slate协议是如何描述这样一个光标位置呢?如上图所示,变成了Path为[0,2],offset为2的Point。
3) 光标位置计算
接下来就是光标位置计算,通过TextPainter的getOffsetForCaret方法,获取选中Element对应RenderObject的光标位置,然后转换成相对于Mural全局的Offset;
整体过程梳理如下:
接下篇:https://developer.aliyun.com/article/1225971?groupCode=idlefish