14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FlutterAdvent Calendar 2021

Day 10

Flutter FocusNode

Last updated at Posted at 2021-12-10

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 を先頭に入れてハンドリングする方法がしっくり来ました。

14
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?