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