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

作者:闲鱼技术——岑彧

之前的系列文章介绍了协议层渲染层的实现,大家可以知道Mural是基于Flutter TextField进行渲染层的设计与实现,然后对其底层的渲染逻辑进行改造,从而对富文本编辑能力进行支持。也正因如此,在文本编辑交互方面,逻辑基本和它是保持一致的。但是我们在改造过程中发现,其实在交互方面,Flutter有很多相比起Native缺失的功能,本文会围绕放大镜模式选区反向选择两个比较重要的交互点来展开说明。为了读者更便于理解,本文将会以官方代码来进行讲解,因为这些优化思路是普适通用的,不与富文本耦合的。

放大镜模式

背景与现状

对于原生控件,不管是Android侧的EditText,还是iOS侧的UITextField,都是默认支持放大镜模式的。将用户进行文本选择时,用户可以通过放大镜来进行精确的光标定位和选区移动。如下图所示:

放大镜在用户抓取正确的选择手柄后出现

放大镜在用户抓取正确的选择手柄后出现

这无疑会对用户体验起到很大的改善作用,但是目前Flutter提供的TextField控件里并没有对该模式进行支持,早在2017年就有人提出了相关issue。Mural的UI渲染层和Flutter TextField除了在文本的渲染机制上不同之外,其他的交互逻辑是基本保持一致的。所以我们决定模拟Android和iOS双端的放大镜交互,在Flutter文本编辑器中进行放大镜模式的支持。

交互分析

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

光标定位

  1. 1. 对于Android来说,点击EditText进行聚焦之后,通常光标下方会出现一个把手:通过拖曳这个把手来进行光标的定位,而放大镜随着拖曳开始而出现,拖曳结束消失。如图所示:

Droplet looking thing is the pointer

Droplet looking thing is the pointer

  1. 1. 对于iOS来说,点击UITextField进行聚焦之后,长按,光标会变成一个浮动游标,然后可以直接进行拖曳,便可以进行光标的定位,而放大镜随着拖曳开始而出现,拖曳结束消失。如图所示:

iOS光标定位

iOS光标定位

选区移动

  1. 1. 对于Android来说,选区移动和光标定位非常相似,通过双击或者长按EditText可以选中最近的词,然后选区的左右两端会出现两个把手,以及选区上方会出现一个Toolbar,可以对选中的文本进行复制剪切等操作。拖拽这两个把手就可以进行选区的移动,拖曳开始时Toolbar会消失,放大镜出现,拖曳结束时放大镜消失,Toolbar重新出现。

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

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

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

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

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,便可以让放大镜吸附在光标上方。定位代码如下:

CupertinoMagnifier

CupertinoMagnifier

可以看到,我们需要判定是把放大镜吸附到左边的把手上,还是右边的把手上,而当选区为光标模式时,光标属于左边的把手。这个问题我们可以在TextSelectionOverlay中的用于展示把手组件的TextSelectionHandleOverlay组件中解决。在把手组件的_handleDragStart中把当前的currentTextSelectionHandleType更新为当前正在交互的把手类型就可以实现。伪代码在后续介绍逻辑部分一并给出。

可以看到Follower组件中还有一个offset参数,这个用于控制Target和Follower的相对位置。可以看到我们向左偏移了半个放大镜宽度,向上偏移了放大镜高度再加上一个距离。这样就可以让放大镜悬浮在光标或者单边选区正上方。

2.如何在放大镜内展示屏幕上指定区域内的内容?

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

CupertinoMagnifier

CupertinoMagnifier

deltaOffsetFromFocusPoint这个参数跟第一个问题中提到的相对位置有关,需要先确定两者的相对位置,然后计算出对应的deltaOffsetFromFocusPoint,让其刚好可以以光标为放大镜展示内容的中心来进行展示。

3.如何处理双端放大镜的不同交互?

对于双端相同的交互,即选区出现时出现Toolbar,拖动选区时隐藏Toolbar,展示Magnifier,拖动结束时隐藏Magnifier,展示Toolbar。我们同样可以在TextSelectionOverlay中的展示把手组件的TextSelectionHandleOverlay进行改造实现,在_handleDragStart和_handleDragEnd(新增方法)中显示和隐藏逻辑。部分代码如下:

交互

交互

而对于双端不同的交互,在Android中,因为光标定位可以看做选区定位的一种特殊场景,光标下方的把手即选区中的左边把手。无需特殊处理,而对于iOS来说,UITextField通过长按然后拖动来进行光标的定位。所以我们需要对iOS进行特殊处理,长按开始时展示放大镜,长按结束时隐藏放大镜。我们对TextSelectionGestureDetectorBuilder进行改造即可。部分代码如下:

区别

区别

效果展示

放大镜

放大镜

选区支持反向选择

背景与现状

在平时的使用中我们注意到,iOS的UITextField是支持反选的,即在操作右边把手时,可以一直往左边拖动,超过左边把手时,把手的位置会进行一个互换,可以继续操作左边的把手。而Android很多厂商也支持了这一特性。但是我们发现在Flutter TextField中,这个操作是被禁止使用的。

原因

原因

所以我们决定在富文本编辑器中支持选区的反向选择。

反向选择

反向选择

交互分析

对iOS以及一些支持反向选择的Android机型的交互进行分析之后,以右边把手往左边移动为例,有两种交互。一种是在左右把手交汇的时候交换两个把手的位置,继续往前选择移动的是左边样式的把手。还有一种交互是,左右把手交汇的时候不改变两个把手的位置,在拖动结束之后,如果发现右边把手在左边把手的前面,再进行交换。

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

代码实现

可能很多读者会猜想,是不是在背景中介绍到那行代码给删掉,就可以实现这个Feature的支持。一开始和大家的想法一样,但是出现了很多问题,接下来会进行具体实现和分析。

上面有说到,去除掉TextField之后,出现了一些问题。第一个就是,两个把手交汇的时候,两个把手都消失了,变成了光标形态。原因是因为在Flutter TextField中,选区把手和光标把手(仅Android,iOS光标形态没有把手)是在同一个地方实现的,当左右选区交汇时,会自动切换成光标形态,导致无法进行反选。

如何在选区交汇时不切换为光标形态?

我们当然不可能删除这个规则,因为在设定中,本来光标就是收缩态的选区,如果完全删除,那光标态也不可能存在了,因为左右选区收缩到一起时,一定会展示左右两个把手,这就有点舍本求末了。

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

1.设定标记位表示把手拖动状态

设定变量

设定变量

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

展开态

展开态

解决了这个问题,我们还剩下一个问题,反选完成之后,如何交换两个把手。

如何在反选完成之后保证正确的选区把手样式?

我们需要在在TextSelectionOverlay中的展示把手组件的TextSelectionHandleOverlay进行实现,新增一个_handleDragEnd方法,交换selection的baseOffset和extentOffset

反选

反选

效果展示

反向选择

反向选择

总结与展望

纵观整个系列文章,我们从协议层、渲染层、自定义扩展以及交互体验优化等方面,详细介绍如何实现一个功能完善、可扩展、高性能的Flutter富文本编辑器。目前Mural已经在闲鱼的多个场景落地,整体的体验也有了不错的提升。

未来会继续在基础能力、交互体验、性能等方面更深入的完善富文本编辑器的能力:

在基础能力方面,跟随富文本编辑器的业界标准,提供更加丰富的富文本组件和扩展Plugin能力;完善单元测试覆盖,保证稳定性。

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

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

猜你喜欢

转载自juejin.im/post/7109285121429078023
今日推荐