很多時候, flutter 中需要處理輸入的焦點, 我們們今天就來看看控制元件怎麼用
本篇可以視為簡單使用, 而不會深入原始碼去探討怎麼附著, 主要是 Focus 系列控制元件的使用, 和怎麼在多輸入框之間反覆橫跳
環境說明
- 本篇基本基於 flutter sdk 的 1.17.5 版本來看, 其他版本應該大同小異, 但很多東西可能會隨時間變化, 未來是否有效請繼續驗證
- 本篇基本是針對移動端來說的
- 寫本文時, flutter web 的焦點比較迷, 似乎和移動版不太一樣, 所以暫時略過不表
- desktop 版只嘗試了 macOS, 其他的桌面引擎請自行校驗對錯
相關 dart class
flutter 中, 和焦點相關聯類有如下幾個:
FocusNode
: 這個可以說是最常用到的, 核心類之一FocusManager
: 單例類, 整個 flutter 應用的焦點管理核心都是這東西在處理, 包括和原生互動彈出軟鍵盤之類的操作Focus
: 一個 Widget, 用於給控制元件"新增"焦點能力, 包起來就行,InkWell
之類的控制元件能獲取焦點能力都是靠這東西FocusScope
: 一個 Widget, Focus 的子類, 被這東西包起來的所有的子 widget 的FocusNode
都會被自動註冊到這個裡面, 接受統一管理FocusScopeNode
: 這東西本身是 FocusNode 的子類, 但是它主要是給FocusScope
用的,擴充套件了FocusNode
的行為FocusTraversalPolicy
,FocusTraversalGroup
: 這兩個東西是 focus node 的策略, 用於排序哪個是下一個焦點的問題, 這兩個東西本篇應該不講, 有興趣的可以去看官方文件, 目前個人認為應該用不上
FocusNode
這東西講的人很多, 我也就不展開了, 簡單的說一下幾個方法
canRequestFocus
: 是否能請求焦點context
: 焦點"附著"的 widget 的 BuildContexthasFocus
: 是否有焦點unfocus
: 放棄焦點, 如果當前 node 有焦點,並呼叫這個, 就放棄了焦點, 如果同時有軟鍵盤彈起, 則軟鍵盤收起requestFocus
: 請求焦點, 這個方法呼叫後, 會把焦點移到當前
備註: 有很多其他的方法, 對於普通朋友和正常的應用場景很難用到, 作為程式框架有提供, 但是個人觀點不必一定要了解, 只要知道主要方法即可
FocusManager
這東西是一個單例的,通過FocusManager.instance
獲取
有一個常用方法瞭解一下: FocusManager.instance.primaryFocus.unfocus();
, 呼叫一下, 軟鍵盤就下去了
這東西里面基本都是私有方法, 能呼叫的並不多
FocusHighlightMode 這東西是焦點的"模式", 對應觸控和滑鼠鍵盤, 個人認為一般情況下用不到, 移動端就 touch 就可以了
Focus
這東西一般情況下很少能用到, SDK 裡有一些地方會用到, Focus
物件本身內部會維護一個 FocusNode
, 比如按鈕能響應鍵盤迴車之類的焦點就是因為內部有這東西
這個類在 flutter 專案中使用率不算高, 但都是關鍵處
_FocusableActionDetectorState
: 對應 FocusableActionDetector
的狀態, 這個類被用於 CheckBox
, Radio
, Switch
FocusScope
這東西很少見有文件講, 這裡我簡單的解析一下, 這個也可以說是後面使用的重點, 我在實際開發中遇到有輸入框的情況下, 這個控制元件是我的首選
簡單來說, 就是在這東西子控制元件內的 FocusNode
都會被統一維護
這東西構造方法可以傳一些引數, 常用的無非就是 node, canRequestFocus, 之類的.
這裡有一個 skipTraversal, 這個引數後面結合例子來看才能說明白
FocusScopeNode
一般和FocusScope
成對使用
寫程式碼
入門級寫法
嗯, 前面都是概念性的東西, 很多朋友都不想看, 而且也沒啥意思
比如有一個這樣的場景
用 app 來說, 就是 4 個輸入框, 一個個的點選自然可以, 但是如果要使用者體驗好是不是應該可以回車一直下一步, 然後最後一條直接提交呢?
模擬一下這個東西很多人的寫法
嗯, 點評一下, 嗯 很整齊, 那麼... 當你有 10 個的時候怎麼辦呢? 想想就很美
我們改寫下,也許可以這樣?
好的, 算你基礎紮實, 這樣寫自然是可以的.
進階
上面的寫法很 dart, 但是不 flutter, 我們 flutter 的寫法可以改成這樣
import 'package:flutter/material.dart';
class Example3 extends StatefulWidget {
@override
_Example3State createState() => _Example3State();
}
class _Example3State extends State<Example3> {
FocusScopeNode node = FocusScopeNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FocusScope(
node: node,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
for (var i = 0; i < 10; i++) buildTextField(),
],
),
),
),
);
}
TextField buildTextField() {
return TextField(
onEditingComplete: () {
if (node.focusedChild == node.children.last) {
print('submit');
} else {
node.nextFocus();
}
},
);
}
}
複製程式碼
這次連 FocusNode
都不需要自己寫了, 直接用 Scope 裡的
這個 example 的樣子:
這是因為 TextField
是 EditableText
的封裝
然後是在 EditableText 裡, attach 到了 context 上
看到這裡, 是不是發現其實有的東西很簡單, 接下來複雜一下
再進階
import 'package:flutter/material.dart';
class Example3 extends StatefulWidget {
@override
_Example3State createState() => _Example3State();
}
class _Example3State extends State<Example3> {
FocusScopeNode node = FocusScopeNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FocusScope(
node: node,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
for (var i = 0; i < 5; i++) buildTextField(),
Row(
children: <Widget>[
Expanded(
child: TextField(
onEditingComplete: onEdit,
),
),
RaisedButton(
onPressed: () {},
child: Text('假裝獲取驗證碼'),
),
],
),
for (var i = 0; i < 5; i++) buildTextField(),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print(node.traversalChildren.length);
},
child: Icon(Icons.check),
),
);
}
TextField buildTextField() {
return TextField(
onEditingComplete: onEdit,
);
}
void onEdit() {
node.nextFocus();
}
}
複製程式碼
這種偶爾旁邊多了一個按鈕的, 屬於比較常見的方式, 然後上面程式碼突然就不好用了
這時候就需要改程式碼了
floatingActionButton: FloatingActionButton(
onPressed: () {
print(node.children.length); // 12
},
child: Icon(Icons.check),
),
複製程式碼
為啥變 12 了呢, 不是隻有 11 個輸入框嗎?
這裡就和我開始說的對上了, 很多按鈕也有 focus.
那麼怎麼在回車時跳過這個按鈕呢
RaisedButton(
onPressed: () {},
focusNode: FocusNode(skipTraversal: true),
child: Text('假裝獲取驗證碼'),
),
複製程式碼
是的, 就是這樣, 給按鈕手動傳入一個 FocusNode
, 然後 skip 就可以了
完整程式碼:
import 'package:flutter/material.dart';
class Example3 extends StatefulWidget {
@override
_Example3State createState() => _Example3State();
}
class _Example3State extends State<Example3> {
FocusScopeNode node = FocusScopeNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FocusScope(
node: node,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
for (var i = 0; i < 5; i++) buildTextField(),
Row(
children: <Widget>[
Expanded(
child: TextField(
onEditingComplete: onEdit,
),
),
RaisedButton(
onPressed: () {},
focusNode: FocusNode(skipTraversal: true),
child: Text('假裝獲取驗證碼'),
),
],
),
for (var i = 0; i < 5; i++) buildTextField(),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print(node.traversalChildren.length);
},
child: Icon(Icons.check),
),
);
}
TextField buildTextField() {
return TextField(
onEditingComplete: onEdit,
);
}
void onEdit() {
node.nextFocus();
}
}
複製程式碼
所以總結一下步驟
- 將所有的輸入框包在一個
FocusScope
裡, 設定FocusScopeNode
. - 將有焦點但不是輸入框的控制元件設定一個
FocusNode(skipTraversal: true)
- 使用
FocusScopeNode
的nextFocus
方法
後記
本篇到此, 本系列的後續預計要深爬一下原始碼
以上