Flutter富文本編輯器系列文章3——交互篇
2022-11-29|10:30|發(fā)布在分類(lèi) / 客服知識(shí)| 閱讀:129
2022-11-29|10:30|發(fā)布在分類(lèi) / 客服知識(shí)| 閱讀:129
之前的系列文章介紹了渲染層的實(shí)現(xiàn),大家可以知道Mural是基于Flutter TextField進(jìn)行渲染層的設(shè)計(jì)與實(shí)現(xiàn),然后對(duì)其底層的渲染邏輯進(jìn)行改造,從而對(duì)富文本編輯能力進(jìn)行支持。但是我們?cè)诟脑爝^(guò)程中發(fā)現(xiàn),其實(shí)在交互方面,F(xiàn)lutter有很多相比起Native缺失的功能,本文會(huì)圍繞放大鏡模式和選區(qū)反向選擇兩個(gè)比較重要的交互點(diǎn)來(lái)展開(kāi)說(shuō)明。
本文將會(huì)以官方代碼來(lái)進(jìn)行講解,因?yàn)檫@些優(yōu)化思路是普適通用的,不與富文本耦合的。
放大鏡模式
對(duì)于原生控件,不管是Android側(cè)的EditText,還是iOS側(cè)的UITextField,都是默認(rèn)支持放大鏡模式的。將用戶(hù)進(jìn)行文本選擇時(shí),用戶(hù)可以通過(guò)放大鏡來(lái)進(jìn)行精確的光標(biāo)定位和選區(qū)移動(dòng)。如下圖所示:
這無(wú)疑會(huì)對(duì)用戶(hù)體驗(yàn)起到很大的改善作用,但是目前Flutter提供的TextField控件里并沒(méi)有對(duì)該模式進(jìn)行支持,早在2017年就有人提出了相關(guān)issue。Mural的UI渲染層和Flutter TextField除了在文本的渲染機(jī)制上不同之外,其他的交互邏輯是基本保持一致的。所以我們決定模擬Android和iOS雙端的放大鏡交互,在Flutter文本編輯器中進(jìn)行放大鏡模式的支持。
眾所周知,Android和iOS有著不同的設(shè)計(jì)與交互規(guī)范,文本編輯控件就是一個(gè)很好的例子,不過(guò)他們的交互也有相似的地方,我們將會(huì)求同存異,盡量滿(mǎn)足雙端的設(shè)計(jì)交互規(guī)范。一般來(lái)說(shuō),放大鏡控件通常在兩個(gè)場(chǎng)景會(huì)出現(xiàn),一就是光標(biāo)定位時(shí),二就是在選區(qū)移動(dòng)時(shí)。我們接下來(lái)對(duì)這兩個(gè)場(chǎng)景進(jìn)行分析:
光標(biāo)定位
對(duì)于A(yíng)ndroid來(lái)說(shuō),點(diǎn)擊EditText進(jìn)行聚焦之后,通常光標(biāo)下方會(huì)出現(xiàn)一個(gè)把手:通過(guò)拖曳這個(gè)把手來(lái)進(jìn)行光標(biāo)的定位,而放大鏡隨著拖曳開(kāi)始而出現(xiàn),拖曳結(jié)束消失。如圖所示:
對(duì)于iOS來(lái)說(shuō),點(diǎn)擊UITextField進(jìn)行聚焦之后,長(zhǎng)按,光標(biāo)會(huì)變成一個(gè)浮動(dòng)游標(biāo),然后可以直接進(jìn)行拖曳,便可以進(jìn)行光標(biāo)的定位,而放大鏡隨著拖曳開(kāi)始而出現(xiàn),拖曳結(jié)束消失。如圖所示:
對(duì)于A(yíng)ndroid來(lái)說(shuō),選區(qū)移動(dòng)和光標(biāo)定位非常相似,通過(guò)雙擊或者長(zhǎng)按EditText可以選中最近的詞,然后選區(qū)的左右兩端會(huì)出現(xiàn)兩個(gè)把手,以及選區(qū)上方會(huì)出現(xiàn)一個(gè)Toolbar,可以對(duì)選中的文本進(jìn)行復(fù)制剪切等操作。拖拽這兩個(gè)把手就可以進(jìn)行選區(qū)的移動(dòng),拖曳開(kāi)始時(shí)Toolbar會(huì)消失,放大鏡出現(xiàn),拖曳結(jié)束時(shí)放大鏡消失,Toolbar重新出現(xiàn)。
iOS和Android的選區(qū)移動(dòng)交互比較相似,不同的是,iOS只能通過(guò)雙擊UITextField才能選中最近的詞,因?yàn)殚L(zhǎng)按手勢(shì)用于光標(biāo)定位。以及把手的樣式不一樣。
通過(guò)以上的分析不難發(fā)現(xiàn),放大鏡有三個(gè)特點(diǎn):
在內(nèi)容上,放大鏡會(huì)以光標(biāo)或是單邊選區(qū)為中心,展示固定尺寸的區(qū)域內(nèi)的屏幕上的內(nèi)容。
在位置上,放大鏡會(huì)浮動(dòng)在光標(biāo)或是單邊選區(qū)之上,保持固定的距離。
在邏輯上,放大鏡一般隨著拖曳開(kāi)始而出現(xiàn),拖曳結(jié)束而消失,以及選區(qū)移動(dòng)場(chǎng)景下還需要進(jìn)行Toolbar的隱藏和恢復(fù),但是雙端有一些不同的交互。
其實(shí)還有一些其他的細(xì)節(jié)交互,比如iOS UITextField放大鏡其實(shí)是展示在觸摸點(diǎn)上方而并非光標(biāo)和單邊選區(qū)上方,并且在觸摸區(qū)域和光標(biāo)沒(méi)有重合的時(shí)候,放大鏡就會(huì)消失等。不過(guò)此處暫時(shí)以以上三個(gè)特點(diǎn)為思路來(lái)進(jìn)行實(shí)現(xiàn),后續(xù)會(huì)對(duì)沒(méi)有對(duì)齊的交互進(jìn)行進(jìn)一步的優(yōu)化與對(duì)齊。以上三個(gè)特點(diǎn)可以轉(zhuǎn)化為三個(gè)問(wèn)題與解決方案:
1.如何把放大鏡定位在光標(biāo)或單邊選區(qū)上方?
Flutter還提供了一組叫做CompositedTransformFollower 與 CompositedTransformTarget的組件,他們通過(guò)同一個(gè)LayerLink來(lái)讓Follower與Target的相對(duì)位置保持一致,即Target的位置移動(dòng)時(shí),F(xiàn)ollower也會(huì)跟著一起移動(dòng)。而且TextField中已經(jīng)存在startHandleLayerLink和endHandleLayerLink用于展示選區(qū)的操作把手組件,所以我們直接使用這兩個(gè)LayerLink,便可以讓放大鏡吸附在光標(biāo)上方。定位代碼如下:
可以看到,我們需要判定是把放大鏡吸附到左邊的把手上,還是右邊的把手上,而當(dāng)選區(qū)為光標(biāo)模式時(shí),光標(biāo)屬于左邊的把手。這個(gè)問(wèn)題我們可以在TextSelectionOverlay中的用于展示把手組件的TextSelectionHandleOverlay組件中解決。在把手組件的_handleDragStart中把當(dāng)前的currentTextSelectionHandleType更新為當(dāng)前正在交互的把手類(lèi)型就可以實(shí)現(xiàn)。偽代碼在后續(xù)介紹邏輯部分一并給出。
可以看到Follower組件中還有一個(gè)offset參數(shù),這個(gè)用于控制Target和Follower的相對(duì)位置??梢钥吹轿覀兿蜃笃屏税雮€(gè)放大鏡寬度,向上偏移了放大鏡高度再加上一個(gè)距離。這樣就可以讓放大鏡懸浮在光標(biāo)或者單邊選區(qū)正上方。
2.如何在放大鏡內(nèi)展示屏幕上指定區(qū)域內(nèi)的內(nèi)容?
首先會(huì)給大家介紹一個(gè)Flutter控件叫做BackdropFilter,他可以接收一個(gè)矩陣,對(duì)位置被該控件蓋?。磟軸處于它下方)的組件產(chǎn)生高斯模糊、傾斜等效果。詳細(xì)的使用和介紹可參考BackdropFilter。我們把這個(gè)控件放到Overlay上,他就可以對(duì)被其蓋住的屏幕部分進(jìn)行映射展示,但是我們并非想對(duì)該控件正下方(z軸)的內(nèi)容做高斯模糊等特效,而是想展示而是光標(biāo)附近的內(nèi)容,即位置處于它下面(y軸)的內(nèi)容。所以我們?cè)趯?duì)傳入的矩陣做translate(偏移),scale(放縮)操作,就可以把光標(biāo)和選區(qū)周?chē)钠聊粌?nèi)容映射到這個(gè)放大鏡中。代碼如下:
deltaOffsetFromFocusPoint這個(gè)參數(shù)跟第一個(gè)問(wèn)題中提到的相對(duì)位置有關(guān),需要先確定兩者的相對(duì)位置,然后計(jì)算出對(duì)應(yīng)的deltaOffsetFromFocusPoint,讓其剛好可以以光標(biāo)為放大鏡展示內(nèi)容的中心來(lái)進(jìn)行展示。
3.如何處理雙端放大鏡的不同交互?
對(duì)于雙端相同的交互,即選區(qū)出現(xiàn)時(shí)出現(xiàn)Toolbar,拖動(dòng)選區(qū)時(shí)隱藏Toolbar,展示Magnifier,拖動(dòng)結(jié)束時(shí)隱藏Magnifier,展示Toolbar。我們同樣可以在TextSelectionOverlay中的展示把手組件的TextSelectionHandleOverlay進(jìn)行改造實(shí)現(xiàn),在_handleDragStart和_handleDragEnd(新增方法)中顯示和隱藏邏輯。部分代碼如下:
而對(duì)于雙端不同的交互,在A(yíng)ndroid中,因?yàn)楣鈽?biāo)定位可以看做選區(qū)定位的一種特殊場(chǎng)景,光標(biāo)下方的把手即選區(qū)中的左邊把手。無(wú)需特殊處理,而對(duì)于iOS來(lái)說(shuō),UITextField通過(guò)長(zhǎng)按然后拖動(dòng)來(lái)進(jìn)行光標(biāo)的定位。所以我們需要對(duì)iOS進(jìn)行特殊處理,長(zhǎng)按開(kāi)始時(shí)展示放大鏡,長(zhǎng)按結(jié)束時(shí)隱藏放大鏡。我們對(duì)TextSelectionGestureDetectorBuilder進(jìn)行改造即可。部分代碼如下:
放大鏡選區(qū)支持反向選擇
在平時(shí)的使用中我們注意到,iOS的UITextField是支持反選的,即在操作右邊把手時(shí),可以一直往左邊拖動(dòng),超過(guò)左邊把手時(shí),把手的位置會(huì)進(jìn)行一個(gè)互換,可以繼續(xù)操作左邊的把手。而Android很多廠(chǎng)商也支持了這一特性。但是我們發(fā)現(xiàn)在Flutter TextField中,這個(gè)操作是被禁止使用的。
所以我們決定在富文本編輯器中支持選區(qū)的反向選擇。
對(duì)iOS以及一些支持反向選擇的Android機(jī)型的交互進(jìn)行分析之后,以右邊把手往左邊移動(dòng)為例,有兩種交互。一種是在左右把手交匯的時(shí)候交換兩個(gè)把手的位置,繼續(xù)往前選擇移動(dòng)的是左邊樣式的把手。還有一種交互是,左右把手交匯的時(shí)候不改變兩個(gè)把手的位置,在拖動(dòng)結(jié)束之后,如果發(fā)現(xiàn)右邊把手在左邊把手的前面,再進(jìn)行交換。
結(jié)合Flutter TextField的改造成本以及用戶(hù)的操作連續(xù)性,我們決定采用第二種交互方式,當(dāng)然iOS端應(yīng)該保持UITextField的第一種方式,這個(gè)會(huì)在后續(xù)進(jìn)行繼續(xù)對(duì)齊和優(yōu)化。
可能很多讀者會(huì)猜想,是不是在背景中介紹到那行代碼給刪掉,就可以實(shí)現(xiàn)這個(gè)Feature的支持。一開(kāi)始和大家的想法一樣,但是出現(xiàn)了很多問(wèn)題,接下來(lái)會(huì)進(jìn)行具體實(shí)現(xiàn)和分析。
上面有說(shuō)到,去除掉TextField之后,出現(xiàn)了一些問(wèn)題。第一個(gè)就是,兩個(gè)把手交匯的時(shí)候,兩個(gè)把手都消失了,變成了光標(biāo)形態(tài)。原因是因?yàn)樵贔lutter TextField中,選區(qū)把手和光標(biāo)把手(僅Android,iOS光標(biāo)形態(tài)沒(méi)有把手)是在同一個(gè)地方實(shí)現(xiàn)的,當(dāng)左右選區(qū)交匯時(shí),會(huì)自動(dòng)切換成光標(biāo)形態(tài),導(dǎo)致無(wú)法進(jìn)行反選。
我們當(dāng)然不可能刪除這個(gè)規(guī)則,因?yàn)樵谠O(shè)定中,本來(lái)光標(biāo)就是收縮態(tài)的選區(qū),如果完全刪除,那光標(biāo)態(tài)也不可能存在了,因?yàn)樽笥疫x區(qū)收縮到一起時(shí),一定會(huì)展示左右兩個(gè)把手,這就有點(diǎn)舍本求末了。
所以在絕大部分情況下我們是需要這個(gè)規(guī)則的,但是又想實(shí)現(xiàn)反選,自然而然會(huì)想到,設(shè)定一個(gè)標(biāo)記位來(lái)標(biāo)識(shí)我們正在操縱選區(qū)把手,當(dāng)處于這種場(chǎng)景下,左右把手交匯時(shí),我們就不將其轉(zhuǎn)化為光標(biāo)形態(tài)。
1.設(shè)定標(biāo)記位表示把手拖動(dòng)狀態(tài)
2.處于該狀態(tài)時(shí),選區(qū)收縮時(shí)展示展開(kāi)態(tài)
解決了這個(gè)問(wèn)題,我們還剩下一個(gè)問(wèn)題,反選完成之后,如何交換兩個(gè)把手。
我們需要在在TextSelectionOverlay中的展示把手組件的TextSelectionHandleOverlay進(jìn)行實(shí)現(xiàn),新增一個(gè)_handleDragEnd方法,交換selection的baseOffset和extentOffset
總結(jié)與展望
縱觀(guān)整個(gè)系列文章,我們從協(xié)議層、渲染層、自定義擴(kuò)展以及交互體驗(yàn)優(yōu)化等方面,詳細(xì)介紹如何實(shí)現(xiàn)一個(gè)功能完善、可擴(kuò)展、高性能的Flutter富文本編輯器。目前Mural已經(jīng)在閑魚(yú)的多個(gè)場(chǎng)景落地,整體的體驗(yàn)也有了不錯(cuò)的提升。
未來(lái)會(huì)繼續(xù)在基礎(chǔ)能力、交互體驗(yàn)、性能等方面更深入的完善富文本編輯器的能力:
這個(gè)問(wèn)題還有疑問(wèn)的話(huà),可以加幕.思.城火星老師免費(fèi)咨詢(xún),微.信號(hào)是為: msc496。
推薦閱讀:
如何利用扣扣進(jìn)行推廣為淘寶店鋪帶來(lái)流量呢?
一個(gè)淘寶買(mǎi)家在店里重復(fù)購(gòu)買(mǎi)很多次,被扣分怎么辦?
更多資訊請(qǐng)關(guān)注幕 思 城。
微信掃碼回復(fù)「666」
別默默看了 登錄\ 注冊(cè) 一起參與討論!