之前的系列文章介紹了協議層和渲染層的實現,大家可以知道Mural是基于Flutter TextField進行渲染層的設計與實現,然後對其底層的渲染邏輯進行改造,從而對富文本編輯能力進行支持。但是我們在改造過程中發現,其實在交互方面,Flutter有很多相比起Native缺失的功能,本文會圍繞放大鏡模式和選區反向選擇兩個比較重要的交互點來展開說明。
本文将會以官方代碼來進行講解,因為這些優化思路是普适通用的,不與富文本耦合的。
放大鏡模式對于原生控件,不管是Android側的EditText,還是iOS側的UITextField,都是默認支持放大鏡模式的。将用戶進行文本選擇時,用戶可以通過放大鏡來進行精确的光标定位和選區移動。如下圖所示:
這無疑會對用戶體驗起到很大的改善作用,但是目前Flutter提供的TextField控件裡并沒有對該模式進行支持,早在2017年就有人提出了相關issue。Mural的UI渲染層和Flutter TextField除了在文本的渲染機制上不同之外,其他的交互邏輯是基本保持一緻的。所以我們決定模拟Android和iOS雙端的放大鏡交互,在Flutter文本編輯器中進行放大鏡模式的支持。
衆所周知,Android和iOS有着不同的設計與交互規範,文本編輯控件就是一個很好的例子,不過他們的交互也有相似的地方,我們将會求同存異,盡量滿足雙端的設計交互規範。一般來說,放大鏡控件通常在兩個場景會出現,一就是光标定位時,二就是在選區移動時。我們接下來對這兩個場景進行分析:
光标定位
對于Android來說,點擊EditText進行聚焦之後,通常光标下方會出現一個把手:通過拖曳這個把手來進行光标的定位,而放大鏡随着拖曳開始而出現,拖曳結束消失。如圖所示:
通過以上的分析不難發現,放大鏡有三個特點:
在内容上,放大鏡會以光标或是單邊選區為中心,展示固定尺寸的區域内的屏幕上的内容。
在位置上,放大鏡會浮動在光标或是單邊選區之上,保持固定的距離。
在邏輯上,放大鏡一般随着拖曳開始而出現,拖曳結束而消失,以及選區移動場景下還需要進行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進行改造即可。部分代碼如下:
選區支持反向選擇
在平時的使用中我們注意到,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是否變化減少不必要的重複計算,來提升編輯器的渲染和編輯的性能。
,
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!