本文翻譯自
原文地址:How text editing works internally in Flutter
原作者:Suragch
【個人閱讀心得】這篇文章由表及里介紹了Flutter中文本繪制和編輯的處理流程篮洁,講了各個層級所負責的內(nèi)容裳扯,能夠幫助讀者對文本處理有個大致認知坯屿,原來文本繪制和編輯在底層是很復雜的系統(tǒng)抓韩,包括如何和系統(tǒng)鍵盤打交道,怎樣布局輸入框里的內(nèi)容羽峰,以及怎么處理和底層框架交互等趟咆。想要更深入的理解添瓷,可以一步一步按照文章中介紹的層級去閱讀源碼梅屉,此外文中的引文Flutter文本渲染對渲染過程進行了詳細講解,非常推薦延伸閱讀一下鳞贷。得益于這篇文章中的Text and selection
一節(jié)坯汤,我們的小伙伴優(yōu)化了TextField輸入精度計算,解決了中文輸入最大限制個數(shù)計算不準確的困擾搀愧。此外惰聂,翻譯不準確的地方歡迎大家指正。
寫給那些喜歡鉆研細節(jié)的人咱筛。
由于Flutter僅支持水平文本布局搓幌,因此在為了給傳統(tǒng)蒙古文字創(chuàng)建垂直布局的widget時,我不得不深入研究文本渲染系統(tǒng)的底層源碼迅箩。在本文中溉愁,我將分享我發(fā)現(xiàn)的,關(guān)于Flutter中文本編輯widget工作方式的知識饲趋。
文本渲染(Text rendering)
通常我們看到的關(guān)于Text widget是這樣子的:
Text(
'Hello world',
style: TextStyle(fontSize: 30),
),
然而拐揭,它的下面還有很多層級。
Widget層(Widgets layer)
當你使用Text
widget時奕塑,它實際創(chuàng)建的是一個RichText widget堂污。
與Text(將String作為數(shù)據(jù)參數(shù))不同,RickText采用TextSpan(更準確的說是InlineSpan, 稍后會介紹更多相關(guān)內(nèi)容)龄砰,TextSpan需要String和一個TextStyle作為參數(shù)盟猖,因此在Text創(chuàng)建RichText之前讨衣,它需要你提供任意TextStyle并將其與APP默認的TextStyle組合,該默認TextStyle是從BuildContext中獲取的式镐,下面是上圖的更詳細版本:
由于Text僅采用單個字符串和style樣式作為參數(shù)值依,因此它在TextSpan中提供給RichText的字符串只有一種樣式,但是TextSpan很有趣碟案,就像多子節(jié)點widget一樣愿险,它能將更多的文本對象作為子節(jié)點。這意味著你可以使用它來構(gòu)建整個文本樹价说,其中每個節(jié)點都可以有各自的字符串和樣式辆亏,
但是這很難推導出來,因為通常你只有一個父TextSpan和一堆子節(jié)點
你可以將TextSpan對象傳遞給Text.rich
構(gòu)造函數(shù)鳖目,或者直接傳遞給TextRich widget扮叨,但是,如果將其直接傳遞給TextRich widget的話领迈,該樣式則不會和build context中的默認樣式組合彻磁。
閱讀掌握Flutter中的文本樣式一文,以獲得使用TextSpan設(shè)置文本樣式的實際示例狸捅。
RichText本身是MultiChildRenderObjectWidget的子類衷蜓,RichText內(nèi)部有多個子節(jié)點的原因是,你可以在一行的文本內(nèi)部散布其他的widget尘喝,上面的圖片顯示的是一個TextSpan樹磁浇,但實際上RichText內(nèi)置的是InlineSpan,InlineSpan既可以是TextSpan也可以是WidgetSpan朽褪,它是他們的父類置吓。出于本文的目的,我將僅處理TextSpan缔赠。
渲染層(Rendering layer)
Widget只是最終用于創(chuàng)建RenderObject的藍圖衍锚,RichText創(chuàng)建的渲染對象叫做RenderParagraph
RenderParagraph會計算文本的大小,當然也會處理其他事情嗤堰,比如命中測試以及它的任何子widget的布局戴质。
繪制層(Painting layer)
RenderParagraph并不直接繪制文本,而是創(chuàng)建一個TextPainter來管理這些工作梁棠。
與他的名字相反置森,TextPainter實際上也不繪制文本,但是它會管理周圍所有繪制相關(guān)的工作符糊,包括要繪制的畫布Canvas以及從TextSpan樹上提取樣式和字符串凫海。
此外,TextSpan還可以處理插入符的的位置男娄,這對于Text widget不那么重要行贪,但是對于接下來要講的文本編輯將會很重要漾稀。
基礎(chǔ)層(Foundation layer)
每個人都以為穿過重重困難,即將真正繪制文本的時候建瘫,實際則不止于此崭捍。
在Flutter框架的最底層,你將會看到一個ParagraphBuilder和一個Paragraph對象啰脚,TextPainter內(nèi)部會創(chuàng)建ParagraphBuilder殷蛇,而ParagraphBuilder又會用于生成Paragraph。
ParagraphBuilder中包含一點邏輯橄浓,但是Paragraph幾乎不包含任何邏輯粒梦,這兩個對象將大部分工作傳給Flutter engine。
Flutter引擎層(Flutter engine layer)
Flutter引擎是用C/C++寫的荸实,因此對于使用Dart的Flutter開發(fā)者匀们,通常不會直接使用,處理文本繪制的庫叫做LibTxt准给。
LibTxt當前已經(jīng)被Skia的SkParagraph模塊替換泄朴,可以關(guān)注這個issue跟蹤進展。
總結(jié)
下面的圖片展示了渲染工作的所有圖層
閱讀Flutter文本渲染這篇文章露氮,深入了解以上內(nèi)容祖灰。【譯者按:強烈推薦閱讀沦辙,之后也會翻譯出來或者總結(jié)學習筆記】
文本編輯(Text editing)
我的一個問題是文本渲染和文本編輯在架構(gòu)層上有多少是共享的夫植,答案是從TextPainter向下的所有內(nèi)容,這就很方便了油讯,意味著我們只需要了解它上層的內(nèi)容。
Material和Cupertino層(Material and Cupertino layers)
當你想要在Flutter中輸入和編輯的時候延欠,TextField可能是用戶首先想到的widghet陌兑。
TextField(
decoration: InputDecoration(
hintText: 'Search',
),
),
然而,TextField只是組成Material庫的一部分由捎,這是widget層更上的一層兔综,與之對應(yīng)的在Cupertino庫中widget稱為CupertinoTextField
假如你使用TextField.adaptive
構(gòu)造函數(shù)的話,它將會在iOS和macOS創(chuàng)建CupertinoTextField狞玛,但是在其它平臺創(chuàng)建TextField软驰。
你可能也想知道TextFormField,但這僅是一個TextField心肪,包裝了一些邏輯锭亏,使保存和驗證更加容易。據(jù)我所知硬鞍,沒有CupertinoTextFormField慧瘤。
與Text這種無狀態(tài)(stateless)widget不同戴已,TextField和CupertinoTextField是有狀態(tài)的(statefull)widget。這是因為他們需要持續(xù)跟蹤諸如TextEditingController锅减、焦點(focus)糖儡、鼠標懸停(mouse hovering)、手勢(gestures)等之類的東西怔匣。
無論使用哪種握联,TextField和Cupertino TextField都將最終創(chuàng)建一個EditableText widget。
然后我們就進入到了widget層每瞒。
Widget層(Widgets layer)
在這一層拴疤,你將不會再具有某些上一層提供的功能,例如占位符文本和標簽独泞,不過這里有很多其他屬性呐矾。
EditableText管理文本和選擇區(qū)域,與鍵盤懦砂、光標和滾動事件進行通信蜒犯。
文本和選擇區(qū)域(Text and selection)
你可能之前使用過TextEditingController,盡管它本身并不是一個widget荞膘,但是它屬于widget層罚随,用來處理EditableText。繼承自ValueNotifier羽资,當TextEditingValue發(fā)生改變的時候淘菩,通知它的監(jiān)聽者。
TextEditingValue對象有三個部分組成屠升,文本潮改、選擇區(qū)域和編輯區(qū)域(text, selection, and composing)。
- text:這里是用戶已經(jīng)輸入的任何字符串腹暖。
- selection:這是一個TextSelection對象汇在,通過它你可以知道當前所選擇的光標位置和選擇范圍,除了選擇的開始和結(jié)束值外脏答,TextSelection也包含文本方向和光標在換行處的精確位置糕殉。
- composing:這是你正在編輯單詞的TextRange(僅包含開始和結(jié)束的偏移量)。你知道當你在輸入某些內(nèi)容的時候殖告,鍵盤會提出一些建議嗎阿蝶?如果你選擇鍵盤建議,則帶下劃線的文本將會被你選擇的鍵盤建議文本替換掉黄绩。
與系統(tǒng)鍵盤進行通信(Communicating with the system keyboard)
當彈出系統(tǒng)軟鍵盤時羡洁,它屬于底層系統(tǒng),F(xiàn)lutter的Editable widget需要一種與該系統(tǒng)通信的方式宝与,無論是獲取信息(用戶使用鍵盤輸入)還是發(fā)送信息(如更改選擇區(qū)域焚廊,或者讓鍵盤消失)冶匹。
下面是架構(gòu)示意圖:
- EditableText實現(xiàn)TextInputClient,當用戶使用系統(tǒng)鍵盤輸入文本時咆瘟,它可以接收更新嚼隘。
- TextInputClient創(chuàng)建一個TextInputConnection對象,它是一個用于將消息發(fā)送給系統(tǒng)鍵盤的接口袒餐。
- 異步消息傳遞全部通過底層的TextInput服務(wù)飞蛹,該服務(wù)通過平臺channel與底層系統(tǒng)進行通信。
- 在native側(cè)灸眼,plugin插件將會處理往返于TextInput的消息卧檐,每個平臺都有自己唯一的插件,用于Android系統(tǒng)的Android插件焰宣,用于iOS系統(tǒng)的iOS插件霉囚,該插件將與本機系統(tǒng)輸入控件進行通信(比如鍵盤)。
- Flutter端和插件端各自維護自己的TextEditingValue版本匕积,并且需要保持同步盈罐。
光標(Cursor)
EditableText使用了兩個AnimationController對象為光標設(shè)置動畫。一個用于標準的光標閃爍(通過動畫的透明度實現(xiàn))闪唆,另一個用于浮動光標盅粪,這時iOS系統(tǒng)中的標準樣式,你可以在下面視頻中看到
滾動(Scrolling)
在EditableText的build
方法中悄蕾,內(nèi)容由Scrollable widget進行包裹了一層票顾,這允許文本垂直滾動以顯示多行文本,水平滾動以顯示單行文本帆调。傳入ScrollController可以進行進一步的定制滾動行為奠骄。
創(chuàng)建渲染對象(Creating a render object)
EditableText創(chuàng)建一個稱為RenderEditable渲染對象。
渲染層(Rendering layer)
RenderEditable管理命中測試贷帮、文本戚揭、和光標或者選擇區(qū)域,以及從字符到字符撵枢、單詞到單詞的移動。最后精居,它使用TextPainter繪制文本和選擇區(qū)域锄禽。
這樣就完成了和開頭內(nèi)容的呼應(yīng)!如你所見靴姿,TextPainter創(chuàng)建了一個Paragraph對象沃但,該對象將將繪制工作傳給Flutter引擎。
總覽
下圖是上面內(nèi)容的總結(jié):
你可以看看它與更大的Flutter架構(gòu)是如何匹配的:
最后
很高興你能看完佛吓,假如發(fā)現(xiàn)任何錯誤宵晚,請在評論中告訴我垂攘,這樣其他讀者也能看到哈打。