本系列文章主要介绍Flutter富文本编辑的设计和实现,从协议层、渲染层、自定义扩展以及体验优化等方面,详细介绍如何实现一个功能完善、可扩展、高性能的Flutter富文本编辑器,以及闲鱼在实践过程中遇到的问题和我们的一些解法。

开篇

协议篇文章,我们介绍了Flutter富文本编辑器协议层的设计。以Slate为例,介绍了协议层设计的几个重要的概念:嵌套Model、Opeartion、Normalizing;站在Slate的肩膀上,让我们有了一个强壮、设计完善的富文本协议层,接下来就让我们看看渲染层是如何实现的;

让我们回顾一下Mural整体的架构设计分层:

渲染层主要工作是将协议Model转换成Widget渲染到屏幕上,以及处理选区、光标的计算和绘制,处理用户的手势交互、键盘交互等一系列工作;

Textfield的渲染实现

首先让我们来看下Flutter的TextField是如何渲染的:

如上图所示,Textfield继承自StatefulWidget,会build嵌套的Widget tree,其中有几个比较关键的Widget:

TextSelectionGestureDetector处理手势交互相关的逻辑,比如单击移动光标、长按选择文字展示Toolbar等等;

另一个比较重要的Widget——EditableText;EditableText在build的时候,通过buildTextSpan方法,根据TextEditingValue的普通文本以及composing部分,创建一个Textspan对象给_Editable;最终RenderEditable通过TextPainter将文本绘制到canvas上;

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将文本绘制到屏幕上;

EditorUtilsbuildChildren方法实现如下:

光标&选区渲染

光标和选区是富文本编辑器渲染层另外一个需要处理的难点;

与原生TextField相比,Mural在处理光标和选区处理更加复杂;TextField所有输入文本都绘制在一个TextPainter,前面我们说过,Mural每个Element都是一个独立的段落,对应一个RenderObject;在Mural中,我们需要计算用户手势操作不同段落的光标位置以及段落之间的选区计算;

要实现Mural的光标和选区渲染,需要解决如下问题:

  1. 1. 多Element点击获取TextPosition;

  2. 2. TextPosition to MuralPoint;

  3. 3. 光标位置计算;

多Element点击获取TextPosition

如上图所示,当用户点击绿色光点位置之后,首先我们可以根据点击事件确认被点击是哪一个Element所渲染的RenderObject;

首先我们通过globalToLocal方法将手势回调的globalPosition转换为相对于Mural的localPosition;接下来遍历MuralRenderEditable的child,寻找包含localPosition的child;

如上面介绍的,Element渲染的RenderObject实现了RenderEditorInlineBox抽象类,也就可以通过getPositionForOffset方法获取到相对于当前TextPainter的TextPosition;

TextPosition to MuralPoint

接下来就要解决第二个问题,如何将TextPosition转换为协议对于光标、选区位置的描述;

以上图为例,点击之后,TextPosition的Offset为12,而Slate协议是如何描述这样一个光标位置呢?如上图所示,变成了Path[0,2]offset2Point

光标位置计算

接下来就是光标位置计算,通过TextPainter的getOffsetForCaret方法,获取选中Element对应RenderObject的光标位置,然后转换成相对于Mural全局的Offset;

整体过程梳理如下:

支持WidgetSpan

在实现自定义表情的过程中,我们发现在展示状态,复杂的WidgetSpan渲染是不存在问题的,但是在编辑状态支持WidgetSpan遇到了一系列问题;

简单一点的做法就是,在编辑状态将表情变成中括号包裹的文字,变成一个不可编辑的inline&void类型的Element;

但我们目标是实现一个所见即所得的富文本编辑器,为了在编辑状态支持WidgetSpan,需要解决如下几个问题:

  1. 1. Element到WidgetSpan渲染;

  2. 2. TextValue与Native同步问题;

  3. 3. 光标、选区TextBox计算问题;

Element到WidgetSpan渲染

我们定义了MuralCustomElement这样一个自定义Element的抽象类,如果要实现自定义表情Element的渲染,需要继承自它:

其中自定义表情长度计算与Emoji不同的一点,我们认为自定义表情始终长度为一;

因为是Inline&Void类型,所以isInlineisVoid都返回true

TextValue与Native同步问题

Flutter文本输入组件的基本原理,就是在Native侧创建一个TextField组件,通过TextInputConnection实现双端事件交互以及TextValue同步等逻辑;

当用户操作键盘进行文字的输入删除、键盘收起、移动光标等操作,会同步到Flutter侧;同样的,在Flutter进行插入、复制、手势导致Selection变化等操作,通过调用TextInputConnectionsetEditingState同步给Native侧的组件;

当我们输入一个表情的时候,从Flutter角度看,我们输入了一个特殊的长度为1的字符,这个时候我们就需要将这个TextValue的变化同步给Native;

我们参考PlaceholderSpan的实现,使用字符\uFFFC同步给Native;

光标、选区TextBox计算问题

如果我们不做任何处理会发现,当包含WidgetSpan的时候,光标的位置总会计算Offset为零;深入了解代码发现问题所在:

我们需要处理WidgetSpan的codeUnitAtVisitor以及getSpanForPositionVisitor 方法:

自定义表情作为WidgetSpan的例子,其实是相对简单的;对于WidgetSpan嵌套WidgetSpan,嵌套的WidgetSpan可以被选择、光标移动的场景,要怎么实现呢?大家可以想一想。

键盘交互问题

当用户键盘输入的时候,Engine侧会通过message channel发送TextInputClient.updateEditingState事件,将最新的TextEditingValue同步到Flutter侧;

对于TextField来说,更新的过程比较简单,整体更新TextValue即可;但对于Mural来说,每一次TextValue的更新,都进行一次TextValue到Slate Model的转换,频繁执行导致编辑状态下的卡顿,性能大大下降;我们采用了diff的方式,判断用户输入、删除内容,进而调用Commond更新Model,刷新界面渲染;

我们需要对于换行符做特殊的处理,正如之前提到过的,Element是不包含换行符的,每一次换行都会新增一个新的Element节点;

另外一个需要处理的问题就是移动光标的处理,如:iOS的长按移动光标、Android的横扫键盘移动光标以及第三方输入法移动光标的键盘操作;这里的处理方案,iOS主要是处理TextInputClient.updateFloatingCursor事件,根据Offset计算光标位置,Android以及第三方输入法的操作,主要是在TextInputClient.updateEditingState同步处理。

扩展能力

扩展能力是我们设计之初就非常重视的能力,为接入方提供简单、强大的自定义扩展能力,支持复杂、不断变化的业务诉求;接下来我们就以自定义主题和撤销功能的实现,来看一看Mural在扩展能力方面的设计。

自定义Node——主题能力

如上面视频演示的,当输入两个#中间包含字符,则变成一个主题的样式,点击可以跳转到对应的主题落地页;可以对主题进行编辑,如果删掉其中一个#,则变成普通的文本。

要实现这样一个自定义主题,我们需要实现以下几个步骤:自定义Element、自定义Normalizing;

首先是定义Element:

接下来就轮到强大的自定义Normalizing出场了,通过自定义规则,处理主题Node节点校验:

只需要这样简单两步,就实现了主题能力的支持;业务还可以根据自己的需求定制更加复杂的场景,比如有序段落等等。

Plugin扩展——实现撤销功能

如上面图所示,我们实现了一个简单的Plugin层的扩展——撤销功能;在前面讲到协议层设计的时候,我们讨论过Slate的精简的Opeartion设计,每一次交互的Commond,最终都会拆解成一个或者多个Opeartion执行;我们可以通过以下三步实现plugin的扩展:

  1. 1. 重写Operation的apply方法,通过过滤、合并等操作,记录Opeartion执行的历史;

  2. 2. 实现Opeartion的reverse方法;

  3. 3. 根据Opeartion执行历史,调用Opeartion的reverse方法,执行reverse操作;

总结

通过两篇文章,我们介绍了富文本编辑器协议层、渲染层设计和实现,完成了一个功能完善的Flutter富文本编辑器;接下来我们会介绍Flutter富文本编辑器体验优化方面闲鱼的一些实践和挑战。

打造Flutter高性能富文本编辑器——渲染篇相关推荐

  1. 打造Flutter高性能富文本编辑器——协议篇

    闲鱼作为一个二手闲置交易平台,卖家发布商品产出优质的供给尤为重要:商品发布器希望拥有富文本编辑能力,让用户简单便捷的方式产出更加优质的内容:Flutter本身没有富文本编辑器的能力的,只有最基础的文本 ...

  2. 工作90:富文本编辑器使用篇wangedit

    WangEdit组件 <template lang="html"><div class="editor"><div ref=&qu ...

  3. c语言实现英文文本编辑器_用flutter实现富文本编辑器(二)

    上一回咱们说到RichText是如何实现TextSpan和WidgetSpan混排的,这次我们把RichText和TextField合并起来 这是我目前修改的文件,把rich前缀去掉就是原来的名字 . ...

  4. React 版本的真开箱即用的富文本编辑器wysiwyg

    React 版本的真开箱即用的富文本编辑器 这篇文章推荐的react 版的富文本编辑器名字叫做wysiwyg,第一时间你可能觉得这个名字起的很烂,很难记,但是当你知道它的全称后就很好记了,全称为 Wh ...

  5. Flutter从0到1实现高性能、多功能的富文本编辑器(模块分析篇)

    通过阅读本文,您将了解到 了解富文本编辑器需要拥有的功能 知道编写富文本编辑器需要的代码模块 学会定义富文本配置JSON,并将其解析为富文本 前言: 经过前面两篇文章的基础知识铺垫,我们终于要进入到专 ...

  6. 现代富文本编辑器Quill的内容渲染机制

    DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师. 官方网站:devui.design Ng组件库:ng-devui(欢 ...

  7. 【实践】简洁大方的summernote 富文本编辑器插件的用发——导入篇

    首先在这里吐槽一下,网上不少教程实在太坑人,错误的代码也敢发上来真的是误人子弟,这篇文章是我踩了无数个坑写上来的,可能也会有不足之处所以自己以后可能也会进行更正. 好吧,先说说最近的情况,忙着学校的期 ...

  8. flutter 富文本编辑器选择图片模糊_Flutter 到底香不香?看完这几个开源项目再做决定...

    Flutter 自 2015 年推出以来,凭借着其极高的开发交付效率,优秀的多平台能力,以及强大的 UI 表现力,受到了许多开发者们的推崇.虽然 Flutter 的确仍旧存在一些问题,但依然是不少开发 ...

  9. vue项目+富文本编辑器ueditor - 资源篇

    资源地址: git源码 · 解说地址 git源码:源码下载地址 · [基于 vue-cli 2.x 的完整 DEMO] ueditor插件Demo演示地址 说明: 支持 vue-cli 2.x 支持 ...

最新文章

  1. 异常处理汇总-后端系列
  2. FastDFS+Nginx实现文件服务器(转载)
  3. 48. Rotate Image
  4. 【PAT乙级】1062 最简分数 (20 分)
  5. Redis的事务操作
  6. mysql linux centos7_MySQL在Linux centos7环境下安装教程详解(图)
  7. 使用JacpFX和JavaFX2构建富客户端
  8. python sklearn 梯度下降法_科学网—Python_机器学习_总结4:随机梯度下降算法 - 李军的博文...
  9. 转:Linux 2.4.x内核软中断机制
  10. Java序列化的这三个坑千万要小心
  11. LeetCode 410. Split Array Largest Sum
  12. 目前web渗透的思路
  13. 黑客编程之初识ShellCode技术
  14. 基于51单片机的电容电感电阻RLC测量仪protues仿真
  15. 360云服务器合作,360云主机速度(云服务器)
  16. 细粒度粗粒度_粗粒度基准
  17. kindle电子书和新闻推送及RSS资源…
  18. C#使用struct直接转换下位机数据的示例代码
  19. Mybatis-9.28
  20. 开源Go语言数值算法库 An open numerical library purely based on Go programming language

热门文章

  1. Cobbler原理与实战
  2. Java中hashCode的作用
  3. Vehicle Re-Identification in Context
  4. “最强地级市”,再添C9!哈尔滨工业大学苏州研究院
  5. 弹窗与Log信息打印_Android Studio学习笔记(3)
  6. php带旋转动画刷新页面,CSS_CSS实现弹簧效果的旋转加载动画,先看看效果,像是弹簧在伸缩 - phpStudy...
  7. 支教志愿者和支教旅行者·《乖摸摸头》·一
  8. 如何去除微信小程序里面的button按钮边框
  9. python变量名由什么组成_Python变量名可以由数字、大、小写英文字母和 下划线组成。...
  10. cocoapods安装配置