What's this
FlutterのFocusNodeについてまとめてみるます。
下記の FocusNode
のドキュメントをまとめたものです。
FocusNodeをより理解するために、TextFormFieldでの使われ方についても触れます。
FocusNode
キーボードのフォーカを取得したり、キーボードのイベントを処理するために使用されるオブジェクト。
FocusNode は ChangeNotifier であるため、フォーカスの変更を受け取ることができるListnerを登録できる。
FocusTree
FocusNodesは、フォーカスに関する階層を表現した FocusTree
を形成する。
debugDumpFocusTree()
を使うことで、デバッグコンソールに FocusTree を表示することができます。また、debugDescribeFocusTree()
で FocusTreeを文字列として取得することもlできる。このFocusTreeは次のfocusScopeNodeを理解する上での理解につながります。
I/flutter (13953): FocusManager#dcb9b
I/flutter (13953): │ primaryFocus: FocusNode#6804e([PRIMARY FOCUS])
I/flutter (13953): │
I/flutter (13953): └─rootScope: FocusScopeNode#86af5(Root Focus Scope [IN FOCUS PATH])
I/flutter (13953): │ IN FOCUS PATH
I/flutter (13953): │ focusedChildren: FocusScopeNode#44017(Navigator Scope [IN FOCUS
I/flutter (13953): │ PATH])
I/flutter (13953): │
I/flutter (13953): └─Child 1: FocusNode#bd472([IN FOCUS PATH])
I/flutter (13953): │ context: Focus
I/flutter (13953): │ NOT FOCUSABLE
I/flutter (13953): │ IN FOCUS PATH
I/flutter (13953): │
I/flutter (13953): └─Child 1: FocusNode#e16bc(Shortcuts [IN FOCUS PATH])
I/flutter (13953): │ context: Focus
I/flutter (13953): │ NOT FOCUSABLE
I/flutter (13953): │
FocusScopeNode
FocusNode は、FocusScopeNode によってまとめられて管理されています。 FocusScopeNodeは、ノードのサブツリーを形成し、探索をノードのグループに制限します。スコープ内では、直近にフォーカスされた FocusNode が記憶されており、 ある FocusNode がフォーカスされた後にフォーカスが解除されると、前の FocusNode が再びフォーカスされます。
Lifecycle
StatefullWidget での FocusNode の生成と破棄に関してはFlutterのCookbookのドキュメントの例がわかりやすいので、リンクを下記に載せておきます。
Focus Traversal
Focus Traversal とは、特定の順序で、あるウィジェットから次のウィジェットにフォーカスを移すこと。
前のウィジェットにフォーカスを与えるには、 nextFocus
メソッドまたは previousFocus
メソッドを呼び出します。また、特定の方向にフォーカスを与えるには、 focusInDirection
メソッドを呼び出します。
TextFormFieldのFocusの方法
4つの方法を紹介します。
1. ユーザがTextFormFiledをタップする
コードでは制御ではないですが、描画されているTextFormFiledはユーザがタップすることで、タップしたTextFormFieldにフォーカスが当たります。
2. autoFocusをTrueにする
テキストフィールドが表示されたらすぐにフォーカスを行う。(他にフォーカスがされていない場合)
ex
return TextFormField(autofocus: true);
autoFocus bool
TextFormFieldの内部で行われている、 EditableText
Widgetのコードを見ることでautofocusの値がどう使われているかを見てみます。
flutter/lib/src/widgets/editable_text.dart
@override
void didChangeDependencies() {
super.didChangeDependencies();
....
if (!_didAutoFocus && widget.autofocus) {
_didAutoFocus = true;
SchedulerBinding.instance!.addPostFrameCallback((_) {
if (mounted) {
FocusScope.of(context).autofocus(widget.focusNode);
}
});
}
EditableText の didChangeDependencies 内で autofocus の値に応じてFocusScope.of(context).autofocus(widget.focusNode);
が呼ばれている。
FocusScope.of(context)
で context における FocusScope を取得して、FocusScope の autofocus メソッドを実行することで、 引数で指定した focusNode にフォーカスを当てるのと、FocusTree への追加が行われている。
3. FocusNode の requestFocus メソッド
FocusNode
を生成して、TextFormFieldに渡して、フォーカスしたい箇所で requestFocus
メソッドを実行する
class _MyHomePageState extends State<MyHomePage> {
late FocusNode myFocusNode;
@override
void initState() {
super.initState();
myFocusNode = FocusNode();
}
@override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Form Test'),
),
body: Center(
child: TextFormField(
focusNode: myFocusNode,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
myFocusNode.requestFocus();
},
),
);
}
}
4. FocusScopeNode の nextFocus メソッド
context の FocusScopeNode を取得して、FocusScope 内で Focus Traversal に基づいて、直近のFocusNodeに対してFocusを行う。
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Form Test'),
),
body: Center(
child: TextFormField(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final focusScopeNode = FocusScope.of(context);
focusScopeNode.nextFocus();
},
),
);
}
}
ちなみにTextFormFieldにFocusNodeを指定しなくても、TextFieldの内部でFocusNodeが生成されます。
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
...
FocusNode? _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
...
TextFormFieldのUnfocusの方法
Focusと同じ方法で可能です。
1. ユーザがフォーカスされているフォームをタップする
2. フォカスされている focusNode の unfocus メソッドを呼ぶ
3. focusScopeNode の unfocus メソッドを呼ぶ
キーイベントの取得
Focus Widget で TextFormField をラップすることで、 Focus Widget はもっとも近いFocusNodeを取得して、FocusNode の keyEventの取得と制御を行うことができます。
制御するには、 FocusOnKeyCallback の 返り値の KeyEventResult の値を適切に変化させることで入力を拒否したすることができます。
Focus(
onKey: (FocusNode node, RawKeyEvent event) {
print(node);
print(event);
return KeyEventResult.ignored;
},
child: TextFormField(),
),
また、直接 focusNode の onKey にコールバック関数を登録することで、 keyEvent を取得することもできます。
focusNode.onKey = (FocusNode node, RawKeyEvent event) {
print('$node, $key');
return KeyEventResult.ignored;
};
FlutterのIssue
Flutter の iOS で DeleteKey のイベントを取得することができません。
SMSの暗証番号の入力などで、複数のフォームを並べてからの1つ前のフォームに戻るときに DeleteKey のイベントのハンドリングが必要になると思いますが、ハンドリングすることができません。
解決策としてIssue内でいろいろな方法が提案されていますが、個人的には zero-width unicode character の \u200b
を先頭に入れてハンドリングする方法がしっくり来ました。