作者:闲鱼技术——岑彧

之前的系列文章介绍了协议层和烘托层的完成,咱们能够知道Mural是基于Flutter TextField进行烘托层的规划与完成,然后对其底层的烘托逻辑进行改造,从而对富文本编辑才能进行支撑。也正因如此,在文本编辑交互方面,逻辑根本和它是坚持一致的。可是咱们在改造过程中发现,其实在交互方面,Flutter有许多比较起Native缺失的功用,本文会环绕扩大镜形式选区反向挑选两个比较重要的交互点来打开说明。为了读者更便于了解,本文将会以官方代码来进行讲解,由于这些优化思路是普适通用的,不与富文本耦合的。

扩大镜形式

布景与现状

关于原生控件,不管是Android侧的EditText,仍是iOS侧的UITextField,都是默认支撑扩大镜形式的。将用户进行文本挑选时,用户能够经过扩大镜来进行精确的光标定位和选区移动。如下图所示:

Flutter富文本编辑器系列文章3——交互篇

扩大镜在用户抓取正确的挑选手柄后呈现

这无疑会对用户体会起到很大的改进作用,可是现在Flutter供给的TextField控件里并没有对该形式进行支撑,早在2017年就有人提出了相关issue。Mural的UI烘托层和Flutter TextField除了在文本的烘托机制上不同之外,其他的交互逻辑是根本坚持一致的。所以咱们决议模仿Android和iOS双端的扩大镜交互,在Flutter文本编辑器中进行扩大镜形式的支撑。

交互剖析

众所周知,Android和iOS有着不同的规划与交互规范,文本编辑控件便是一个很好的例子,不过他们的交互也有类似的当地,咱们将会求同存异,尽量满足双端的规划交互规范。一般来说,扩大镜控件一般在两个场景会呈现,一便是光标定位时,二便是在选区移动时。咱们接下来对这两个场景进行剖析:

光标定位

  1. 1. 关于Android来说,点击EditText进行聚集之后,一般光标下方会呈现一个把手:经过拖曳这个把手来进行光标的定位,而扩大镜跟着拖曳开端而呈现,拖曳完毕消失。如图所示:

Flutter富文本编辑器系列文章3——交互篇

Droplet looking thing is the pointer

  1. 1. 关于iOS来说,点击UITextField进行聚集之后,长按,光标会变成一个起浮游标,然后能够直接进行拖曳,便能够进行光标的定位,而扩大镜跟着拖曳开端而呈现,拖曳完毕消失。如图所示:

Flutter富文本编辑器系列文章3——交互篇

iOS光标定位

选区移动

  1. 1. 关于Android来说,选区移动和光标定位十分类似,经过双击或者长按EditText能够选中最近的词,然后选区的左右两端会呈现两个把手,以及选区上方会呈现一个Toolbar,能够对选中的文本进行复制剪切等操作。拖拽这两个把手就能够进行选区的移动,拖曳开端时Toolbar会消失,扩大镜呈现,拖曳完毕时扩大镜消失,Toolbar从头呈现。

Flutter富文本编辑器系列文章3——交互篇

Navigate Easier with Android’s Smart Text Selection and Selected Text Magnification

  1. 1. iOS和Android的选区移动交互比较类似,不同的是,iOS只能经过双击UITextField才干选中最近的词,由于长按手势用于光标定位。以及把手的款式不相同。

Flutter富文本编辑器系列文章3——交互篇

iPhone UI Designer Tells The Story Behind iOS Text Selection Patent | Cult of Mac

代码完成

经过以上的剖析不难发现,扩大镜有三个特色:

在内容上,扩大镜会以光标或是单边选区为中心,展现固定尺度的区域内的屏幕上的内容。

在方位上,扩大镜会起浮在光标或是单边选区之上,坚持固定的间隔。

在逻辑上,扩大镜一般跟着拖曳开端而呈现,拖曳完毕而消失,以及选区移动场景下还需求进行Toolbar的躲藏和恢复,可是双端有一些不同的交互。

其实还有一些其他的细节交互,比方iOS UITextField扩大镜其实是展现在触摸点上方而并非光标和单边选区上方,并且在触摸区域和光标没有重合的时分,扩大镜就会消失等。不过此处暂时以以上三个特色为思路来进行完成,后续会对没有对齐的交互进行进一步的优化与对齐。以上三个特色能够转化为三个问题与处理方案:

1.怎么把扩大镜定位在光标或单边选区上方?

Flutter还供给了一组叫做CompositedTransformFollower 与 CompositedTransformTarget的组件,他们经过同一个LayerLink来让Follower与Target的相对方位坚持一致,即Target的方位移动时,Follower也会跟着一同移动。并且TextField中已经存在startHandleLayerLink和endHandleLayerLink用于展现选区的操作把手组件,所以咱们直接运用这两个LayerLink,便能够让扩大镜吸附在光标上方。定位代码如下:

Flutter富文本编辑器系列文章3——交互篇

CupertinoMagnifier

能够看到,咱们需求判定是把扩大镜吸附到左面的把手上,仍是右边的把手上,而当选区为光标形式时,光标归于左面的把手。这个问题咱们能够在TextSelectionOverlay中的用于展现把手组件的TextSelectionHandleOverlay组件中处理。在把手组件的_handleDragStart中把当时的currentTextSelectionHandleType更新为当时正在交互的把手类型就能够完成。伪代码在后续介绍逻辑部分同时给出。

能够看到Follower组件中还有一个offset参数,这个用于操控Target和Follower的相对方位。能够看到咱们向左偏移了半个扩大镜宽度,向上偏移了扩大镜高度再加上一个间隔。这样就能够让扩大镜悬浮在光标或者单边选区正上方。

2.怎么在扩大镜内展现屏幕上指定区域内的内容?

首先会给咱们介绍一个Flutter控件叫做BackdropFilter,他能够接纳一个矩阵,对方位被该控件盖住(即z轴处于它下方)的组件发生高斯含糊、倾斜等作用。具体的运用和介绍可参考BackdropFilter。咱们把这个控件放到Overlay上,他就能够对被其盖住的屏幕部分进行映射展现,可是咱们并非想对该控件正下方(z轴)的内容做高斯含糊等特效,而是想展现而是光标邻近的内容,即方位处于它下面(y轴)的内容。所以咱们在对传入的矩阵做translate(偏移),scale(放缩)操作,就能够把光标和选区周围的屏幕内容映射到这个扩大镜中。代码如下:

Flutter富文本编辑器系列文章3——交互篇

CupertinoMagnifier

deltaOffsetFromFocusPoint这个参数跟第一个问题中提到的相对方位有关,需求先确认两者的相对方位,然后计算出对应的deltaOffsetFromFocusPoint,让其刚好能够以光标为扩大镜展现内容的中心来进行展现。

3.怎么处理双端扩大镜的不同交互?

关于双端相同的交互,即选区呈现时呈现Toolbar,拖动选区时躲藏Toolbar,展现Magnifier,拖动完毕时躲藏Magnifier,展现Toolbar。咱们同样能够在TextSelectionOverlay中的展现把手组件的TextSelectionHandleOverlay进行改造完成,在_handleDragStart和_handleDragEnd(新增方法)中显现和躲藏逻辑。部分代码如下:

Flutter富文本编辑器系列文章3——交互篇

交互

而关于双端不同的交互,在Android中,由于光标定位能够看做选区定位的一种特别场景,光标下方的把手即选区中的左面把手。无需特别处理,而关于iOS来说,UITextField经过长按然后拖动来进行光标的定位。所以咱们需求对iOS进行特别处理,长按开端时展现扩大镜,长按完毕时躲藏扩大镜。咱们对TextSelectionGestureDetectorBuilder进行改造即可。部分代码如下:

Flutter富文本编辑器系列文章3——交互篇

差异

作用展现

Flutter富文本编辑器系列文章3——交互篇

扩大镜

选区支撑反向挑选

布景与现状

在平常的运用中咱们注意到,iOS的UITextField是支撑反选的,即在操作右边把手时,能够一直往左面拖动,超越左面把手时,把手的方位会进行一个交流,能够持续操作左面的把手。而Android许多厂商也支撑了这一特性。可是咱们发现在Flutter TextField中,这个操作是被禁止运用的。

Flutter富文本编辑器系列文章3——交互篇

原因

所以咱们决议在富文本编辑器中支撑选区的反向挑选。

Flutter富文本编辑器系列文章3——交互篇

反向挑选

交互剖析

对iOS以及一些支撑反向挑选的Android机型的交互进行剖析之后,以右边把手往左面移动为例,有两种交互。一种是在左右把手交汇的时分交流两个把手的方位,持续往前挑选移动的是左面款式的把手。还有一种交互是,左右把手交汇的时分不改变两个把手的方位,在拖动完毕之后,假如发现右边把手在左面把手的前面,再进行交流。

结合Flutter TextField的改造本钱以及用户的操作连续性,咱们决议采用第二种交互方法,当然iOS端应该坚持UITextField的第一种方法,这个会在后续进行持续对齐和优化。

代码完成

可能许多读者会猜测,是不是在布景中介绍到那行代码给删掉,就能够完成这个Feature的支撑。一开端和咱们的想法相同,可是呈现了许多问题,接下来会进行具体完成和剖析。

上面有提到,去除掉TextField之后,呈现了一些问题。第一个便是,两个把手交汇的时分,两个把手都消失了,变成了光标形状。原因是由于在Flutter TextField中,选区把手和光标把手(仅Android,iOS光标形状没有把手)是在同一个当地完成的,当左右选区交汇时,会主动切换成光标形状,导致无法进行反选。

怎么在选区交汇时不切换为光标形状?

咱们当然不可能删除这个规则,由于在设定中,原本光标便是收缩态的选区,假如完全删除,那光标态也不可能存在了,由于左右选区收缩到一同时,一定会展现左右两个把手,这就有点舍本求末了。

所以在绝大部分情况下咱们是需求这个规则的,可是又想完成反选,自然而然会想到,设定一个符号位来标识咱们正在操纵选区把手,当处于这种场景下,左右把手交汇时,咱们就不将其转化为光标形状。

1.设定符号位表明把手拖动状态

Flutter富文本编辑器系列文章3——交互篇

设定变量

2.处于该状态时,选区收缩时展现打开态

Flutter富文本编辑器系列文章3——交互篇

打开态

处理了这个问题,咱们还剩下一个问题,反选完成之后,怎么交流两个把手。

怎么在反选完成之后确保正确的选区把手款式?

咱们需求在在TextSelectionOverlay中的展现把手组件的TextSelectionHandleOverlay进行完成,新增一个_handleDragEnd方法,交流selection的baseOffset和extentOffset

Flutter富文本编辑器系列文章3——交互篇

反选

作用展现

Flutter富文本编辑器系列文章3——交互篇

反向挑选

总结与展望

纵观整个系列文章,咱们从协议层、烘托层、自定义扩展以及交互体会优化等方面,具体介绍怎么完成一个功用完善、可扩展、高功能的Flutter富文本编辑器。现在Mural已经在闲鱼的多个场景落地,整体的体会也有了不错的提升。

未来会持续在根底才能、交互体会、功能等方面更深化的完善富文本编辑器的才能:

在根底才能方面,跟从富文本编辑器的业界标准,供给愈加丰厚的富文本组件和扩展Plugin才能;完善单元测试覆盖,确保稳定性。

在交互体会方面,咱们尽量给用户供给iOS和Android的端侧交互体会,优化Flutter现有的一些交互体会问题;可是还有一些功用是没有和双端对齐的,例如iOS的实况本文、三指复制粘贴撤销重做等,这些都正在调研完成以及上线中。

在功能方面,咱们优化了超长文本编辑的卡顿问题,与原生的TextField比较,卡顿有了明显的优化;未来会经过两个思路进行优化功能:判别Model的Dom结构是否改变削减不必要的重复刷新烘托,以及判别选区、ToolBar是否改变削减不必要的重复计算,来提升编辑器的烘托和编辑的功能。