看完本篇,您不僅會(huì)了解到 TextField 的實(shí)現(xiàn)和構(gòu)成,還可以學(xué)到很多之前不常用的"奇怪"知識(shí)。
在 Flutter 里TextField是一個(gè)比較復(fù)雜的控件,而在整個(gè)TextField里嵌套了許多不同實(shí)現(xiàn)的控件,它們組成了我們常用的輸入框效果,如下圖所示是關(guān)于TextField的主要構(gòu)成部分,也是本篇主要講解的內(nèi)容。
FocusTrapArea
FocusTrapArea大家可能會(huì)比較陌生,這個(gè)是最近的版本里才出現(xiàn)的控件,FocusTrapArea本身并沒有特別,它僅僅是在RenderObject tree里塞進(jìn)去了一個(gè)FocusNode。 它的出現(xiàn)主要是為了 Web/Desktop 平臺(tái),通過增加了FocusTrapArea之后,在 Web/Desktop 平臺(tái)執(zhí)行TextEditingController.clear的時(shí)候,TextField 還能繼續(xù)保持之前獲得的焦點(diǎn)。
具體可見Flutter的issues:#86154、#86041
MouseRegion
顧名思義是用于處理鼠標(biāo)相關(guān)事件,主要用于響應(yīng)鼠標(biāo)獨(dú)占的 Pointer 事件,比如:鼠標(biāo)進(jìn)入/離開控件區(qū)域、光標(biāo)顯示效果等等。
IgnorePointer
它在TextField里主要用于處理當(dāng)前輸入框是否可用的的狀態(tài),比如當(dāng)widget.enabled或者widget.decoration?.enabled為false時(shí),IgnorePointer就會(huì)屏蔽整個(gè)區(qū)域內(nèi)的手勢(shì)事件,從而讓TextField會(huì)無法點(diǎn)擊輸入。TextSelectionGestureDetectorBuilder
關(guān)于TextSelectionGestureDetectorBuilder大家應(yīng)該比較少接觸,而在TextField里使用的是它的子類_TextFieldSelectionGestureDetectorBuilder:
它主要是處理TextField內(nèi)針對(duì)EditableText的點(diǎn)擊、滑動(dòng)、長按等事件,例如單擊彈起鍵盤,長按彈出選擇復(fù)制/粘貼框等等。
在 TextSelectionGestureDetectorBuilder 的內(nèi)部主要是通過 editableTextKey 這個(gè) GlobalKey 去獲取到 EditableTextState,從而將各種手勢(shì)事件和 EditableText 里的行為關(guān)聯(lián)起來。
該控件內(nèi)部使用的是TextSelectionGestureDetector。
例如在 _TextFieldSelectionGestureDetectorBuilder 中,可以看到 onSingleTapUp 的處理流程:
如上代碼所示:
1、收起已經(jīng)彈出的 Toolbar (一個(gè) Overlay,也就是復(fù)制/粘貼之類的彈框);2、根據(jù)不同平臺(tái)選擇響應(yīng)事件;3、執(zhí)行彈出鍵盤操作;4、回調(diào)點(diǎn)擊事件。所以可以看到,這里其實(shí)是先執(zhí)行彈出鍵盤,然后再回調(diào)點(diǎn)擊的 callback,所以如果您需要在點(diǎn)擊彈出鍵盤前,針對(duì) TextField 作一些處理,那么 TextField 的 onTap 其實(shí)并不合適,因?yàn)樗且呀?jīng)彈出了。 最后 _TextFieldSelectionGestureDetectorBuilder 會(huì)調(diào)用 buildGestureDetector 方法生成一個(gè)監(jiān)聽和處理觸摸的控件,用于嵌套 child。
???????????InputDecorator???????????
關(guān)于 InputDecorator 的內(nèi)部參數(shù)解析這里就不多說,以前在書里已經(jīng)有詳細(xì)介紹過,用過 TextField 的大家對(duì)于 InputDecorator 應(yīng)該也不會(huì)陌生,在 TextField 里 InputDecorator 的實(shí)現(xiàn)是和 AnimatedBuilder 一起組成使用。
因?yàn)樵?TextField 里 FocusNode 和 TextEditingController 都是 ChangeNotifier (Listenable),所以它們可以被用于 AnimatedBuilder 的animation。
也就是當(dāng) FocusNode 和 TextEditingController 這兩者發(fā)生改變的時(shí)候,會(huì)讓 InputDecorator 重新 rebuild 從而改變渲染效果,例如: 輸入框輸入內(nèi)容時(shí)、焦點(diǎn)發(fā)生改變時(shí)修改輸入框的背景顏色。
注意別搞混了InputDecorator和InputDecoration,InputDecoration 是用來配置InputDecorator。
所以可以看到InputDecorator有很豐富的參數(shù)和配置,開發(fā)者可以通過InputDecoration來配置很豐富的輸入框 UI 效果,但是如果剛好出現(xiàn)某些位置,或者某些縫隙不滿足產(chǎn)品詭異的需求時(shí),那恭喜您,您開啟了 Flutter 高級(jí)開發(fā)的修煉之路。
為什么呢? 簡單來說 InputDecorator 的實(shí)現(xiàn)是在內(nèi)部是一個(gè)自定義的 RenderBox,其中和 layout 相關(guān)部分就有 600 多行的代碼,也就是根據(jù) InputDecoration 的 icon、prefixIcon、suffix 等參數(shù),進(jìn)行定位布局,計(jì)算位置方向,根據(jù)基線調(diào)整位置等等。
另外 InputDecorator里的動(dòng)畫效果主要是通過內(nèi)部的AnimatedOpacity等完成。
所以對(duì)于 InputDecorator 來說,如果您對(duì)于某些位置或者邊界效果不滿意,要么您就重構(gòu)一個(gè)自己的實(shí)現(xiàn),要么可能就要選擇 "委曲求全"。
RepaintBoundary
為什么 TextField 內(nèi)部會(huì)有一個(gè) RepaintBoundary?首先 RepaintBoundary 是干嘛的?
之前在《Flutter 畫面渲染的全面解析》詳細(xì)介紹過這部分的知識(shí),這簡單不嚴(yán)謹(jǐn)?shù)卣f就是:RepaintBoundary 主要是用于形成一個(gè) Layer,得到一個(gè)獨(dú)立的繪制區(qū)域。
常見的就是 Navigator 的頁面跳轉(zhuǎn),內(nèi)部基礎(chǔ)實(shí)現(xiàn)都有一個(gè) RepaintBoundary 來保證每個(gè)區(qū)域都是獨(dú)立的繪制區(qū)域。
另外說到Navigator 就不得不說每個(gè)頁面也都有自己的FocusScope,也就是我們常用的FocusScope.of(context)等用于鍵盤和焦點(diǎn)處理。在TextField 內(nèi)部有一個(gè) RepaintBoundary,是因?yàn)?TextField 本身是一個(gè)需要頻繁更新的控件,而 TextField 里的內(nèi)容變化一般很少需要觸發(fā)父布局的重繪,所以 RepaintBoundary 的存在讓 TextField 可以實(shí)現(xiàn)性能更好的局部繪制。
UnmanagedRestorationScope
UnmanagedRestorationScope 大家可能比較少用到,它本身是一個(gè) InheritedWidget,主要是往下共享一個(gè) RestorationBucket,而 RestorationBucket 主要是和實(shí)現(xiàn)狀態(tài)的保存/恢復(fù)有關(guān)系。
例如應(yīng)用因?yàn)榈蛢?nèi)存在后臺(tái)被回收時(shí),可以通過它在重新回到 App 時(shí)恢復(fù)指定的數(shù)據(jù),舉個(gè)例子:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
// Give your RootRestorationScope an id, defaults to null.
restorationScopeId: 'root',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
_HomePageState createState() => _HomePageState();
}
// Our state should be mixed-in with RestorationMixin
class _HomePageState extends State<HomePage> with RestorationMixin {
// For each state, we need to use a restorable property
final RestorableInt _index = RestorableInt(0);
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Index is ${_index.value}')),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index.value,
onTap: (i) => setState(() => _index.value = i),
items:
[ BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home'
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications),
label: 'Notifications'
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings'
),
],
),
);
}
// The restoration bucket id for this page,
// let's give it the name of our page!
String get restorationId => 'home_page';
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
// Register our property to be saved every time it changes,
// and to be restored every time our app is killed by the OS!
registerForRestoration(_index, 'nav_bar_index');
}
}
如上代碼所示:
-
首先給 MaterialApp 配置 restorationScopeId (必須配置才算開啟該功能)。
-
使用 RestorableInt 用于配置和保存 BottomNavigationBar 的 index;
-
在 State 混入 RestorationMixin 并且在 restoreState 方法里恢復(fù) index 的狀態(tài);
以上示例來自《Introduction to State Restoration in Flutter》。
回到 TextField,在 _TextFieldState 里就混入了 RestorationMixin,然后使用 RestorableTextEditingController 用于用于恢復(fù) TextEditingController。
因?yàn)檩斎肟虻膬?nèi)容默認(rèn)保存在了TextEditingController的TextEditingValue里,所以這里用的是RestorableTextEditingController。
一般情況下是使用 MaterialApp 內(nèi)部默認(rèn)自帶了一個(gè) RootRestorationScope,所以我們只需要給 MaterialApp 設(shè)置 restorationScopeId,而 TextFild 通過內(nèi)置 UnmanagedRestorationScope 相關(guān)的邏輯,最終實(shí)現(xiàn)了文本內(nèi)容的保存與恢復(fù)。
EditableText
EditableText 就不用多說了,TextField 的本體,內(nèi)部主要通過 Scrollable 來實(shí)現(xiàn)滑動(dòng),同樣的它也用了對(duì)應(yīng)的 restorationId 來實(shí)現(xiàn)恢復(fù)和緩存。
首先注意到可以滑動(dòng)這一點(diǎn),可以看到對(duì)于 EditableText 來說,它其實(shí)是一個(gè) "ViewPort",是根據(jù) ViewportOffset 來實(shí)現(xiàn)滑動(dòng)效果。
而對(duì)于 EditableText 內(nèi)部,它使用了 CompositedTransformTarget 來實(shí)現(xiàn) Toolbar 和輸入框的聯(lián)動(dòng),也就是輸入控件和長按 "粘貼/復(fù)制" 彈出框之間的關(guān)聯(lián)。
所以這里簡單介紹下 CompositedTransformTarget,它通常和 CompositedTransformFollower 一起被用于控件之間的聯(lián)動(dòng)效果。
如上圖所示,常見內(nèi)置的 Slider,在滑動(dòng)的彈出部分實(shí)現(xiàn),就是通過 CompositedTransformTarget 和 CompositedTransformFollower 的結(jié)合實(shí)現(xiàn),它可以讓一個(gè)控件跟隨另外一個(gè)控件而無需計(jì)算位置,它們之間主要是通過 LayerLink 鏈接在一起。 回到 TextField,其實(shí)除了 "復(fù)制/粘貼"的 Toolbar,關(guān)于 selection 選中區(qū)域的內(nèi)容,EditableText 內(nèi)部也是通過類似的方式實(shí)現(xiàn),只是這里是直接通過 LeaderLayer 而不是通過它的封裝 CompositedTransformTarget 去實(shí)現(xiàn)。
當(dāng)然使用 CompositedTransformTarget 還是會(huì)有 "比較大" 的性能開銷,不建議大規(guī)模頻繁使用,因?yàn)楫吘顾鼘儆谝粋€(gè) pushLayer 的操作。
另外 EditableText 內(nèi)部繪制內(nèi)容的部分,主要就是大家都知道的 TextPainter,這部分就沒什么特別,暫時(shí)不詳細(xì)展開。
所以本篇主要是通過介紹 TextField 的組成,以及解釋內(nèi)部各組成部分的作用,讓開發(fā)者可以更清晰的了解 Flutter 里常用的文本輸入框的實(shí)現(xiàn),當(dāng)遇上問題或者需求時(shí),可以快速定位和解決問題,例如:
-
"粘貼/復(fù)制"的 Toolbar 是哪里彈出;
- Toolbar 是如何定位和布局;
- 點(diǎn)擊 TextField 是如何彈出鍵盤和處理手勢(shì)事件;
- TextField 如何做到局部繪制;
- ...
最后介紹一個(gè)簡單的問題,之前有人剛好問我: 如何在 Flutter 上實(shí)現(xiàn)類似微信聊天輸入框從一行到多行的輸入框效果,如下圖代碼所示,就是這么簡單:
TextField(
focusNode: _focusNode,
maxLines: 7,
minLines: 1,
decoration:
const InputDecoration(border: OutlineInputBorder()),
)
原文標(biāo)題:Flutter 快速解析 TextField 的內(nèi)部原理 | 開發(fā)者說·DTalk
文章出處:【微信公眾號(hào):谷歌開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
顯示
+關(guān)注
關(guān)注
0文章
440瀏覽量
45172 -
參數(shù)
+關(guān)注
關(guān)注
11文章
1838瀏覽量
32276 -
模擬器
+關(guān)注
關(guān)注
2文章
877瀏覽量
43260
原文標(biāo)題:Flutter 快速解析 TextField 的內(nèi)部原理 | 開發(fā)者說·DTalk
文章出處:【微信號(hào):Google_Developers,微信公眾號(hào):谷歌開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論