作者:岑彧
之前的系列文章介绍了协议层和渲染层的实现,大家可以知道Mural是基于Flutter TextField进行渲染层的设计与实现,然后对其底层的渲染逻辑进行改造,从而对富文本编辑能力进行支持。但是我们在改造过程中发现,其实在交互方面,Flutter有很多相比起Native缺失的功能,本文会围绕放大镜模式和选区反向选择两个比较重要的交互点来展开说明。
本文将会以官方代码来进行讲解,因为这些优化思路是普适通用的,不与富文本耦合的。
一、 放大镜模式
1. 背景与现状
对于原生控件,不管是Android侧的EditText,还是iOS侧的UITextField,都是默认支持放大镜模式的。将用户进行文本选择时,用户可以通过放大镜来进行精确的光标定位和选区移动。如下图所示:
这无疑会对用户体验起到很大的改善作用,但是目前Flutter提供的TextField控件里并没有对该模式进行支持,早在2017年就有人提出了相关issue。Mural的UI渲染层和Flutter TextField除了在文本的渲染机制上不同之外,其他的交互逻辑是基本保持一致的。所以我们决定模拟Android和iOS双端的放大镜交互,在Flutter文本编辑器中进行放大镜模式的支持。
2. 交互分析
众所周知,Android和iOS有着不同的设计与交互规范,文本编辑控件就是一个很好的例子,不过他们的交互也有相似的地方,我们将会求同存异,尽量满足双端的设计交互规范。一般来说,放大镜控件通常在两个场景会出现,一就是光标定位时,二就是在选区移动时。我们接下来对这两个场景进行分析:
1) 光标定位
对于Android来说,点击EditText进行聚焦之后,通常光标下方会出现一个把手:通过拖曳这个把手来进行光标的定位,而放大镜随着拖曳开始而出现,拖曳结束消失。如图所示:
对于iOS来说,点击UITextField进行聚焦之后,长按,光标会变成一个浮动游标,然后可以直接进行拖曳,便可以进行光标的定位,而放大镜随着拖曳开始而出现,拖曳结束消失。如图所示:
对于Android来说,选区移动和光标定位非常相似,通过双击或者长按EditText可以选中最近的词,然后选区的左右两端会出现两个把手,以及选区上方会出现一个Toolbar,可以对选中的文本进行复制剪切等操作。拖拽这两个把手就可以进行选区的移动,拖曳开始时Toolbar会消失,放大镜出现,拖曳结束时放大镜消失,Toolbar重新出现。
iOS和Android的选区移动交互比较相似,不同的是,iOS只能通过双击UITextField才能选中最近的词,因为长按手势用于光标定位。以及把手的样式不一样。
3. 代码实现
通过以上的分析不难发现,放大镜有三个特点:
• 在内容上,放大镜会以光标或是单边选区为中心,展示固定尺寸的区域内的屏幕上的内容。
• 在位置上,放大镜会浮动在光标或是单边选区之上,保持固定的距离。
• 在逻辑上,放大镜一般随着拖曳开始而出现,拖曳结束而消失,以及选区移动场景下还需要进行Toolbar的隐藏和恢复,但是双端有一些不同的交互。
其实还有一些其他的细节交互,比如iOS UITextField放大镜其实是展示在触摸点上方而并非光标和单边选区上方,并且在触摸区域和光标没有重合的时候,放大镜就会消失等。不过此处暂时以以上三个特点为思路来进行实现,后续会对没有对齐的交互进行进一步的优化与对齐。以上三个特点可以转化为三个问题与解决方案:
1) 如何把放大镜定位在光标或单边选区上方?
Flutter还提供了一组叫做CompositedTransformFollower与CompositedTransformTarget的组件,他们通过同一个LayerLink来让Follower与Target的相对位置保持一致,即Target的位置移动时,Follower也会跟着一起移动。而且TextField中已经存在startHandleLayerLink和endHandleLayerLink用于展示选区的操作把手组件,所以我们直接使用这两个LayerLink,便可以让放大镜吸附在光标上方。定位代码如下:
可以看到,我们需要判定是把放大镜吸附到左边的把手上,还是右边的把手上,而当选区为光标模式时,光标属于左边的把手。
这个问题我们可以在TextSelectionOverlay中的用于展示把手组件的TextSelectionHandleOverlay组件中解决。在把手组件的_handleDragStart中把当前的currentTextSelectionHandleType更新为当前正在交互的把手类型就可以实现。伪代码在后续介绍逻辑部分一并给出。
可以看到Follower组件中还有一个offset参数,这个用于控制Target和Follower的相对位置。可以看到我们向左偏移了半个放大镜宽度,向上偏移了放大镜高度再加上一个距离。这样就可以让放大镜悬浮在光标或者单边选区正上方。
2) 如何在放大镜内展示屏幕上指定区域内的内容?
首先会给大家介绍一个Flutter控件叫做BackdropFilter,他可以接收一个矩阵,对位置被该控件盖住(即z轴处于它下方)的组件产生高斯模糊、倾斜等效果。详细的使用和介绍可参考BackdropFilter。
我们把这个控件放到Overlay上,他就可以对被其盖住的屏幕部分进行映射展示,但是我们并非想对该控件正下方(z轴)的内容做高斯模糊等特效,而是想展示而是光标附近的内容,即位置处于它下面(y轴)的内容。
所以我们在对传入的矩阵做translate(偏移),scale(放缩)操作,就可以把光标和选区周围的屏幕内容映射到这个放大镜中。代码如下:
deltaOffsetFromFocusPoint这个参数跟第一个问题中提到的相对位置有关,需要先确定两者的相对位置,然后计算出对应的deltaOffsetFromFocusPoint,让其刚好可以以光标为放大镜展示内容的中心来进行展示。
3) 如何处理双端放大镜的不同交互?
对于双端相同的交互,即选区出现时出现Toolbar,拖动选区时隐藏Toolbar,展示Magnifier,拖动结束时隐藏Magnifier,展示Toolbar。我们同样可以在TextSelectionOverlay中的展示把手组件的TextSelectionHandleOverlay进行改造实现,在_handleDragStart和_handleDragEnd(新增方法)中显示和隐藏逻辑。部分代码如下:
而对于双端不同的交互,在Android中,因为光标定位可以看做选区定位的一种特殊场景,光标下方的把手即选区中的左边把手。无需特殊处理,而对于iOS来说,UITextField通过长按然后拖动来进行光标的定位。所以我们需要对iOS进行特殊处理,长按开始时展示放大镜,长按结束时隐藏放大镜。我们对TextSelectionGestureDetectorBuilder进行改造即可。部分代码如下:
4. 效果展示
原文为gif
接下篇:https://developer.aliyun.com/article/1225952?groupCode=idlefish