[読書会]キーボードフォーカスのシステムについて

 キーボード入力をどこに向けるかを制御する方法についての話題です。

目次 index【閲覧時間3分】 click !!>>

1.概要

 Flutterには、キーボード入力をアプリの特定の部分に向けるフォーカスシステムが搭載されています。故に、ユーザーは目的のUI要素をタップまたはクリックすることで、アプリケーションのその部分に入力を「フォーカス」します。その場合、フォーカスがアプリの別の部分に移るまで、キーボードで入力されたテキストはアプリのその部分に流れます。特定のキーボードショートカットによってもフォーカスを移動でき、そのショートカットは通常 Tab にバインドされているため「タブトラバーサル」と呼ばれることもあります。

 ここでは主に次の2点について触れています。
 ・フォーカスシステムで使用される API
 ・フォーカスシステム振舞いや仕組み

1-1.フォーカス利用場面について

 フォーカスシステムの使い方を知っておく必要がある状況の例をいくつか挙げます。

  • キーイベント受取と処理
  • フォーカス必須コンポーネント実装
  • フォーカス変更通知受取
  • タブオーダ(移動順序)定義と変更
  • フォーカス移動を一緒にすべきコントロール群定義
  • フォーカス対象からの除外

2.用語集

 以下は、Flutterが使用している、フォーカスシステムの要素に関する用語です。
これらのコンセプトのいくつかを実装する様々なクラスを以下に紹介します。

  1. Focus tree(フォーカス ツリー):
     ウィジェットツリーを反映するフォーカスノードの木構造を指します。これは、アプリケーション内でフォーカスを受け取ることができるすべてのウィジェットを表現します。(ウィジェットツリー全体をミラーリングするわけではなく)フォーカスが必要なウィジェットに限られます。
  2. Focus node(フォーカス ノード):
     フォーカスツリー内の各々のノードです。
    このノードはフォーカスを受け取ることができ、フォーカスの連鎖の一部である時に「フォーカスがある」と言われます。フォーカスがある時だけ、キー・イベントの処理に参加します。
  3. Primary focus(プライマリ フォーカス):
     フォーカスを持つ、フォーカスツリーのルートから最も遠いフォーカスノード(葉の部分)です。
    キーイベントはこの部分から(プライマリ・フォーカスノードとその祖先に)伝搬を開始します。
  4. Focus chain(フォーカス チェーン):
     (上記プライマリフォーカスノード(葉の部分)から始まり、フォーカスツリーのをたどってフォーカスツリーのルートに至る)フォーカスノード順序付きリストのことです。
  5. Focus scope(フォーカス スコープ):
     フォーカスツリー内で特定のフォーカスノードをグループ化する特別なフォーカスノードです。このグループ化されたノードの中でしかフォーカスを受け取れないように制御します。また、フォーカススコープは、そのサブツリー内で以前にフォーカスを持っていたノードに関する情報保持しています。
     例えば、モーダルダイアログやポップアップメニューの中で、ユーザーがフォーカスできるのはそのダイアログやメニュー内の要素に限りたいという場合に、フォーカススコープを使います。この場合、フォーカススコープが一度設定されると、そのスコープ内のフォーカス可能な要素間でのみタブキーなどを使ったフォーカス移動が行われます。外部の要素にはフォーカスが移らない様に制限されます。
  6. Focus traversal(フォーカス トラバーサルフォーカス移動)):
     あるフォーカス可能なノードから別のノードへ、予測可能な順序で移動するプロセス。
    これは通常アプリケーションで、ユーザーが Tab キーを押して次のフォーカス可能なコントロールやフィールドに移動するときに見られます。

3.FocusNode と FocusScopeNode

 FocusNode FocusScopeNode は、Flutter のフォーカスシステムのメカニズムを実装するオブジェクトです。これらはウィジェットよりも長く存在するオブジェクト(ウィジェットのようにビルドされる度に破棄されず、状態が保持される)で、フォーカス状態や属性を持ち、それらはウィジェットツリーがビルドされるたびに持続します。これにより、これらのノードは フォーカスツリー のデータ構造を形成します。

 当初は、開発者がフォーカスシステムの一部を制御するために使うオブジェクトとして意図されていましたが、時間が経つにつれ、フォーカスシステムの詳細な実装を主に担うようになりました。既存のアプリケーションとの互換性を維持するために、これらのオブジェクトは依然として属性に対する公開インターフェースを提供していますが、一般的にはそれらが最も役立つのは、比較的不透明なハンドルとして機能し、子孫ウィジェットに渡して親ウィジェットに対して requestFocus() を呼び出すためです。つまり、子孫ウィジェットがフォーカスを取得するようにリクエストを送ることができます。

 FocusNode FocusScopeNode 他の属性の設定は、基本的には Focus ウィジェットや FocusScope ウィジェットによって管理されるべきです。但し、それらのウィジェットを使わない場合や、自分でカスタムのフォーカスウィジェットを実装している場合は、これらのノードを直接扱うことができます。 

(1)FocusNode オブジェクト作成のベストプラクティス(制限事項含む)

 これらのオブジェクトを使用する際の注意事項は次の通りです。

  1. 毎回新しいFocusNodeをビルドで生成しないこと!:
     ビルドごとに新しい FocusNode を割り当てないで下さい
     これはメモリ・リークの原因となり、ノードにフォーカスがある時にウィジェットが再構築されると、フォーカスが失われることがあります。
  2. FocusNodeやFocusScopeNodeは、StatefulWidget内で作成すること!:
     FocusNode と FocusScopeNode オブジェクトは、ステートフルウィジェット内で作成して下さい。
     FocusNode と FocusScopeNode は、使い終わったら破棄する必要があるので、ステートフルなウィジェットのステートオブジェクトの中だけに作成して下さい。
     StatefulWidgetの状態stateオブジェクト内で作成し、disposeメソッドをオーバーライドして適切にリソースを解放することが重要です。
     
  3. 複数のウィジェットで同じFocusNodeを使用しないこと!:
     複数のウィジェットに同じ FocusNode を使用しないで下さい
     同じ FocusNode を複数のウィジェットに使用すると、ウィジェットがノードの属性管理の際に競合し、期待した結果が得られない可能性があります。
  4. デバッグ用のラベル(debugLabel)を設定すること!:
     FocusNodeウィジェットにdebugLabel設定することで、フォーカスに関する問題の診断が容易になります。
  5. onKeyEventコールバックをFocusNodeやFocusScopeNodeに設定しないこと!:
     FocusNode または FocusScopeNode Focus または FocusScope ウィジェットによって管理されている場合は、onKeyEvent コールバックFocusNode または FocusScopeNode 設定しないで下さい
     onKeyEvent ハンドラが必要な場合は、リッスンしたいウィジェット・サブツリー周囲新しい Focus ウィジェット追加し、そのウィジェットの onKeyEvent 属性ハンドラに設定します
     プライマリフォーカス取得させたくない場合は、ウィジェットにcanRequestFocus: false設定します。これは、Focus ウィジェットonKeyEvent 属性が、その後のビルド別のものに設定される可能性があり、その場合、ノード設定した onKeyEvent ハンドラ上書きされるためです。 
(悪い例)onKeyEvent コールバックを FocusNode または FocusScopeNode に設定してしまった場合:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('FocusNode Example'),
        ),
        body: const Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  //FocusNode: ウィジェットがキーボード入力を受取る為のオブジェクト。
  FocusNode focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    
    //(悪い例)
    //FocusNode に直接 onKeyEvent を設定 してしまった例:
    focusNode.onKeyEvent = (FocusNode node, KeyEvent event) {
      if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
        if (kDebugMode) {
          print('Enterキーが押されました');
        }
        return KeyEventResult.handled;  //イベントが処理されたことを伝える
      }
      return KeyEventResult.ignored;  //イベントが無視されたことを伝える
    };
  }

  @override
  void dispose() {
    focusNode.dispose(); //FocusNode を破棄
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        FocusScope.of(context).requestFocus(focusNode); //フォーカス取得
      },
      child: Container(
        padding: const EdgeInsets.all(16.0),
        color: Colors.blue,
        child: const Text(
          'タップしてフォーカスを取得し、Enterキーを押してください'
        ),
      ),
    );
  }
}


(良い例)リッスンしたいウィジェット・サブツリーの周囲に新しい Focus ウィジェットを追加し、そのウィジェットの onKeyEvent 属性をハンドラに設定した例:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Focus and Key Event Example'),
        ),
        body: const Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  //FocusNode: ウィジェットがキーボード入力を受取る為のオブジェクト。
  FocusNode focusNode = FocusNode();

  //disposeメソッド: オブジェクトの破棄時に呼ばれる。
  @override
  void dispose() {
    //FocusNodeを手動破棄しメモリリークを防ぐ。
    focusNode.dispose();
    super.dispose();
  }

  //buildメソッド: ウィジェットツリーを構築する。
  @override
  Widget build(BuildContext context) {
    //Focusウィジェット: 子ウィジェットにfocus受取を許可する。
    return Focus(  
      //focusを管理するFocusNodeの指定
      focusNode: focusNode,
      onKeyEvent: (FocusNode node, KeyEvent event) {
        //キーイベントのハンドラを設定: 
        //キーイベントがKeyDownEvent(キー押下時イベント)であり、かつ
        //押下キーがEnterキーであるかをチェック。
        if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
          //Enterキー押下時処理
          if (kDebugMode) {
            //コンソールにメッセージ表示
            print('Enterキーが押されました');
          }
          //イベント処理時に伝える
          return KeyEventResult.handled;
        }
        //イベント無視されたことを伝える
        return KeyEventResult.ignored;
      },
      child: GestureDetector(
        //onTap: タップ時処理を定義するコールバック
        onTap: () {
          //ウィジェットがタップ時、focusNodeがフォーカスを取得する
          FocusScope.of(context).requestFocus(focusNode);
        },
        //ユーザーがタップできる領域を示すContainerウィジェット
        child: Container(
          //パディング設定
          padding: const EdgeInsets.all(16.0),
          color: Colors.blue,//背景色設定: 青
          child: const Text( //説明テキスト
            'タップしてフォーカスを取得し、Enterキーを押してください'
          ),
        ),
      ),
    );
  }
}
  1. 特定のウィジェットにフォーカスをリクエスト(requestFocus() メソッド)
     requestFocus() メソッドを使って、特定のフォーカスノード(FocusNode)に対してフォーカスを要求する際に、特にそのフォーカスノードが子孫ウィジェットに渡された場合に使うべきです。
     FocusNode は、Flutterのウィジェットツリー内で特定のウィジェットがキーボード入力やその他のユーザー操作を受け取るためにフォーカスを持つためのノードです。requestFocus() メソッドを使うことで、特定のウィジェットにフォーカスをリクエストすることができます。
     この場合、説明文では「ancestor」や「descendant」という言葉が使われているので、親ウィジェット(ancestor)が子孫ウィジェット(descendant)にFocusNodeを渡している構造を指しています。このような場合、親が保持しているフォーカスノードに対して、requestFocus() を呼び出すことで、そのノードにフォーカスを要求できる、という意味です。

     例えば、親ウィジェットが複数の子ウィジェットを持っており、その中の特定の子ウィジェットがフォーカスを取得する必要がある場合、親ウィジェットが子ウィジェットにFocusNodeを渡します。子ウィジェットで何かしらの操作が行われた時、そのFocusNodeに対してrequestFocus()を呼び出すと、子ウィジェットがそのノードを使ってフォーカスを取得できるという流れです。
  1. focusNode.requestFocus() の推奨!:
     focusNode.requestFocus() を使用してください
    FocusScope.of(context).requestFocus(focusNode) を呼び出す必要はありません
    focusNode.requestFocus() メソッドは同等であり、より高性能です。
requestFocus() メソッド利用により、フォーカスを特定ノードに要求する(例):
FocusNode focusNode = FocusNode();

@override
Widget build(BuildContext context) {
  return GestureDetector(
    onTap: () {
      focusNode.requestFocus();  //フォーカスをこのノードに要求
    },
    child: TextField(
      focusNode: focusNode,  //このTextFieldがフォーカスノードを使用
    ),
  );
}

(2)Focus解除

 ここでは、FocusNode.unfocus() メソッドを使って、あるノードからフォーカスを取り除く(”unfocusing”)方法と、その際のフォーカスの処理について解説しています。しかし、フォーカスを「完全に取り除く」ことはできず、常にどこかにフォーカスが移動するという重要な点が強調されています。

(2-1)FocusNode.unfocus() の概要:

 FocusNode.unfocus() は、特定のノードからフォーカスを外しますが、フォーカスを完全に「無効」には出来ず、必ずどこかにフォーカスが移動します。アプリ内には常に「プライマリフォーカス」というものが存在し、どれかのノードがその役割を担います。

(2-2)フォーカスの移動先:

 フォーカスが外れた後、どのノードにフォーカスが移動するかは、unfocus() メソッドdisposition 引数によって制御されます。
この引数には、次の2つの選択肢があります。

  • UnfocusDisposition.scope:(デフォルト設定)
     フォーカスは最も近い FocusScopeNode に移動します。
     (フォーカスを次の対象に移す際、そのスコープ内の最初のフォーカス可能な要素に移動します。)
  • UnfocusDisposition.previouslyFocusedChild
     スコープ内で以前フォーカスされていた子ノードにフォーカスを戻します。
     (以前フォーカスされていたノードがあれば、それが選ばれますが、なければ scope と同じ動作をします。)
unfocus() メソッドの disposition 引数によって制御する(例):
FocusNode focusNode = FocusNode();
FocusNode otherFocusNode = FocusNode();

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      TextField(focusNode: focusNode),
      ElevatedButton(
        onPressed: () {
          //現在のノードのフォーカスを外し、
          //以前フォーカスされていたノードにフォーカスを戻す:
          focusNode.unfocus(
            disposition: UnfocusDisposition.previouslyFocusedChild);
        },
        child: Text('Unfocus and revert'),
      ),
      TextField(focusNode: otherFocusNode),
    ],
  );
}

(2-3)より詳細な制御:

 unfocus() だけではフォーカスの移動先を厳密に制御できない場合もあります。
そのような場合、nextFocus() previousFocus() といったフォーカス移動メソッドを利用して、意図的にフォーカスの移動先を制御することが推奨されます。
また、特定のノードに明示的にフォーカスを設定したい場合には、requestFocus() を使ってそのノードにフォーカスを移すことができます。

unfocus() は、特定のノードからフォーカスを外す際に、どのノードにフォーカスが移るかを決定するために disposition 引数を使うことで、アプリケーション内でのフォーカス移動を制御します。また、場合によっては明示的に requestFocus() を使ってフォーカスの移動先を指定することも推奨されます。

(2-4)注意点:

 FocusNode.unfocus() を使う際の注意点について述べています。特に、フォーカスがルートスコープに移動することで、予期しない動作が発生する可能性があることに警告しています。

① フォーカスがルートスコープに移動する可能性

 FocusNode.unfocus() を呼び出すと、フォーカスは「次のフォーカス可能なノード」に移動します。
しかし、もし現在のフォーカスのスコープ( FocusScopeNode )が存在しない場合、フォーカスは最上位のスコープである FocusManager.rootScope に移動します。このルートスコープには具体的な「次にどのノードにフォーカスを移すか」というコンテキストが無い為、期待通りの動作が行われなくなります

② フォーカス移動がうまくいかない場合

 アプリ内で突然フォーカスの移動が機能しなくなった場合、この FocusManager.rootScopeフォーカスが移動している可能性があります。これは、ルートスコープがフォーカスの順序や移動に関する情報を持たないため、アプリが次にどの要素にフォーカスすべきかを判断できなくなるためです。

③ 解決策

 そのような問題が発生した場合、FocusScope フォーカスノードの祖先として追加することで解決できます。FocusScope は、フォーカスを管理し、フォーカスの移動先を適切に制御するためのスコープです。
通常、WidgetsApp やその派生クラス(MaterialApp や CupertinoApp)はデフォルトFocusScope を持っているため、これらのウィジェットを使用している限りは、問題が発生することは少ないです。

4.Focus ウィジェット

 Flutterにおける Focus widget の役割と使用方法について解説されています。具体的には、Focus ウィジェットどのようにしてフォーカスを管理し、カスタムコントロールをフォーカス可能にするかに焦点を当てています。

4-1.Focus ウィジェットの役割

(1)FocusNode の管理

 Focus widget は、Focus node所有し管理する、フォーカスシステムの主力です。
所有するFocus node を Focus tree に接続・切断する役割持ちFocus node の属性とコールバックを管理し、Focus tree接続されたFocus node 検出可能にする静的関数を持ちます

(2)Focus の管理方法

 Focus widget は、自身の onFocusChange コールバックを持ち、フォーカスが当たったり外れたりした時に、その状態変化に応じ何かしらの処理を実行することができます。

(3)FocusNode を直接渡すか、自動生成させるか

 通常は、Focus ウィジェットが独自に FocusNode を生成しますが、必要に応じて開発者が自身で FocusNode を作成して管理することもできます。FocusNode を手動で作成する場合、親ウィジェットから requestFocus() メソッドを呼び出すことで、そのフォーカスを管理することができます

(4)Flutter の標準ウィジェットでの使用例

 多くの Flutter 標準の UI コンポーネント(ボタンやテキストフィールドなど)も Focus ウィジェットを使用してフォーカス機能を実装しています。したがって、カスタムウィジェットにフォーカス機能を追加する際も、この同じ手法を使うことが推奨されます。

4-2.サンプルコード

 以下のサンプルコードは、2つのカスタムウィジェットフォーカスを受け取ったり失ったりする際に、その状態を UI 上で視覚的に表示する方法を示しています。

  • カスタムウィジェット(MyCustomWidget)コンテンツは 、Focus ウィジェットでラップされています。
  • onFocusChange コールバックで、フォーカスの変化時に色 (_color)、ラベル (_label) を更新しています。
  • タップ、クリックでフォーカスを取得し、背景色が変わり、ラベルも「Focused」に変化させています。
Focus widget を使ってカスタムコントロールをフォーカス可能にする(例):
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  static const String _title = 'Focus Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            MyCustomWidget(),  //カスタムウィジェット( その1 )
            MyCustomWidget(),  //カスタムウィジェット( その2 )
          ],
        ),
      ),
    );
  }
}

//MyCustomWidget は StatefulWidget で、フォーカスとラベル変更を管理
class MyCustomWidget extends StatefulWidget {
  const MyCustomWidget({super.key});

  @override
  State<MyCustomWidget> createState() => _MyCustomWidgetState();
}

//_MyCustomWidgetState はウィジェットの状態を管理
class _MyCustomWidgetState extends State<MyCustomWidget> {
  Color _color = Colors.white;  //初期背景色は白
  String _label = 'Unfocused';  //初期テキストは "Unfocused"
  
  //FocusNode を生成。これでフォーカスを管理
  final FocusNode _focusNode = FocusNode();

  @override
  void dispose() {
    _focusNode.dispose();  //FocusNode を破棄。リソースリーク防止
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    //「Focus」ウィジェットはタップやクリックだけでは自動的にフォーカスを取得しません。
    //フォーカスを明示的に取得するように指示する必要があります。
    //その為、GestureDetector ウィジェットを追加し、ユーザーがタップしたときにフォーカスを取得します。
    return GestureDetector(
      //ウィジェットがタップされたときにフォーカスをリクエスト
      onTap: () {
        //FocusScope.of(context).requestFocus(focusNode) と focusNode.requestFocus() は
        //基本的に同じことを行いますが、focusNode.requestFocus() の方が直接的で、より効率的です。
        //これは、FocusScope.of(context) を使うと、一旦ウィジェットツリーを遡って FocusScope を探し、
        //そこから requestFocus() を呼び出すため、少し冗長になります。
        //対して、focusNode.requestFocus() では、すでに対象の FocusNode にアクセスできているため、
        //処理がシンプルで高速です。
        _focusNode.requestFocus();  //推奨される方法でフォーカスを要求
      },
      child: Focus(
        focusNode: _focusNode,  //この Focus ウィジェットが管理する FocusNode
        onFocusChange: (focused) {
          //フォーカスの状態が変わったときに呼び出される
          setState(() {
            _color = focused ? Colors.black26 : Colors.white;  //フォーカス時は背景色を変更
            _label = focused ? 'Focused' : 'Unfocused';  //フォーカス時のラベルを変更
          });
        },
        child: Center(
          child: Container(
            width: 300,  //コンテナの幅
            height: 50,  //コンテナの高さ
            alignment: Alignment.center,  //テキストを中央揃え
            color: _color,  //フォーカスに応じた背景色を設定
            child: Text(_label),  //フォーカスに応じたラベルを表示
          ),
        ),
      ),
    );
  }
}

(1)Key イベント

(1-1)Key イベントの伝播について

 サブツリーのキーイベント監視したい場合は、Focus ウィジェットonKeyEvent 属性に、次のどちらかを設定します。

  • KeyEventResult.ignoredリッスンするだけのハンドラ)
     キーイベントを監視するが、KeyEventResult.ignored を返すことで他のウィジェットに伝播させます。
     つまり、イベントを無視し、KeyEventResult.ignored を返すことで、他のウィジェットがそのキーイベントを受け取ることができます。これは、他のウィジェットでも同じイベントを扱いたい場合に使用されます。
  • KeyEventResult.handled伝播を停止するハンドラ)
     キーイベントを処理し、KeyEventResult.handled を返すことで、他のウィジェットへの伝播を止めます
     「キーを処理して伝播を停止する」とは、キーイベントを処理し、その後他のウィジェットがそのキーイベントを受け取らないようにすることです。KeyEventResult.handled を返すことで、そのイベントは完全に処理されたと見なされ、他のウィジェットには伝播しません。

 上記により、必要なコンポーネントだけキーイベントを処理できるように制御し、他の部分への影響を防ぐことができます。

(1-2)サブツリーのキーイベントを吸収する Focus ウィジェット

 これは、サブツリーが扱わない全てのキーを吸収するフォーカスウィジェット例です。

サブツリーのキーイベントを吸収する Focus ウィジェット(例):
@override
Widget build(BuildContext context) {
  return Focus(
    // ( onKeyEvent で ) 全てのキーイベントを 
    // ( KeyEventResult.handled により ) 処理します。
    //onKeyEvent コールバックでキーイベントをリッスンしていますが、
    //KeyEventResult.ignored を返すことで、
    //そのイベントが他のウィジェットに伝わるようにしています。
    //この場合、キーイベントはログに表示されるだけで、
    //他のウィジェットも同じイベントを処理することが可能です。
    onKeyEvent: (node, event) => KeyEventResult.handled,
    
    //この Focus ウィジェットが、フォーカスを要求できないようにする
    //( canRequestFocus: false は、
    //  このウィジェットがフォーカスされることを防ぎますが、
    //  キーイベントはそのサブツリー内で処理されます )
    canRequestFocus: false,

    child: child,  //サブツリーをここに配置する
  );
}

 フォーカス・キー・イベントはテキスト入力イベントの前に処理されるため、フォーカス・ウィジェットがテキスト・フィールドを囲んでいるときにキー・イベントを処理すると、そのキーがテキスト・フィールドに入力されなくなります。

(1-3)”a” の入力を防ぐ TextField

 以下は、テキスト・フィールドに “a “を入力することを許可しないウィジェットの例です。

Dart
@override
Widget build(BuildContext context) {
  return Focus(
    //"a" キーが押されたときはイベントを処理し、それ以外は無視する
    onKeyEvent: (node, event) {
      //LogicalKeyboardKey.keyA が押された場合は 
      //KeyEventResult.handled を返すことで、
      //"a" が入力されないようにしています。
      return (event.logicalKey == LogicalKeyboardKey.keyA)
          ? KeyEventResult.handled  //"a" を防ぐ
          : KeyEventResult.ignored;  //他のキーは無視して他に伝播
          //注意点: 
          //キーイベントはテキスト入力イベントの前に処理されるため、
          //ここでキーを処理すると、テキストフィールドに入力されなくなります。
    },
    child: const TextField(),  //フォーカスの対象は TextField
  );
}

 入力検証を意図しているのであれば、この例の機能はTextInputFormatterを使って実装した方が良いだろうが、それでもこのテクニックは役に立つ。例えば、ショートカットウィジェットは、テキスト入力になる前のショートカットを処理するためにこのメソッドを使います。

 このように、Focus ウィジェットを使うことで、特定のキー入力をハンドリングし、必要に応じてそのイベントがサブツリー内でどう扱われるかを制御することができます。また、フォーカスの設定をカスタマイズすることで、UI上の他のコンポーネントに影響を与えずにキーイベントの管理が可能です。

(2)Focus 対象の制御

 ここでは、Focusシステムにおける「何がフォーカスを受け取るか」を制御するための属性について触れています。
具体的には、skipTraversalcanRequestFocusdescendantsAreFocusable という3つの属性を使用して、フォーカスの受け取り方の制御ができます。

(2-1)skipTraversal 属性

 skipTraversal が true の場合、そのノードはフォーカスの「トラバーサル」(順にフォーカスが移動する処理)に参加しません。つまり、フォーカスを手動requestFocus 設定しない限り、通常のキーボードナビゲーションやフォーカス移動時には無視されます
 (例えば、Tab キーなどでフォーカス移動が行われる時、このノードはその移動経路から外されますが、明示的にフォーカスをリクエストすれば、フォーカスを受け取れます。)

skipTraversal属性: true(例):
Focus(
  skipTraversal: true,  //トラバーサルで無視される
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
),

(2-2)canRequestFocus 属性

 この属性はその名の通り、フォーカスノードがフォーカスをリクエストできるかを制御します。
canRequestFocus false の場合、requestFocus()呼び出してもフォーカスを取得できず他のノードがフォーカスされます
さらに、このノードはフォーカスのトラバーサルからも除外されます

canRequestFocus属性: false(例):
Focus(
  canRequestFocus: false,  //requestFocus でフォーカスを取得できない
  child: Container(
    width: 100,
    height: 100,
    color: Colors.green,
  ),
),

(2-3)descendantsAreFocusable 属性

 descendantsAreFocusableは、そのフォーカスノードの子孫(descendants)がフォーカスを受け取れるかを制御します。
この属性 が false の場合、子孫ノードはフォーカスを受け取れなくなりますが、そのノード自体はフォーカスを取得できます
この属性は、ウィジェットのサブツリー全体のフォーカス受け取りを無効にする際に便利です
(例えば、ExcludeFocus ウィジェットはこの属性を false に設定しているFocusウィジェットです。)

descendantsAreFocusable属性: false(例):
Focus(
  descendantsAreFocusable: false,  //子孫ノードはフォーカス不可
  child: Column(
    children: [
      TextField(),  //これにはフォーカスできない
      ElevatedButton(onPressed: () {}, child: Text('Button')),
    ],
  ),
),

(2-4)まとめ

 これらの属性を使うことで、アプリ内でフォーカスの動作を細かく制御できます
 (例えば、特定のウィジェットがトラバーサルされない様にしたり、フォーカスを受け取るかどうかを条件付きでコントロールしたりすることができます。)

(3)autofocus属性

(3-1)autofocusの基本的な役割

 autofocus属性を設定すると、そのウィジェットがフォーカススコープ内で最初にフォーカスをリクエストします
ウィジェットのツリーが表示され、フォーカススコープが初めてフォーカスされるときに、自動的にそのウィジェットがフォーカスを取得します

(3-2)複数のautofocusが設定された場合

 もし同じフォーカススコープ内で複数のウィジェットautofocusが設定されていると、その中でどのウィジェットが実際にフォーカスされるかは不定です。故に、1つのフォーカススコープに対して、1つのウィジェットのみに autofocus を設定することが推奨されます。

(3-3)他のフォーカススコープに属するノードにautofocusを設定する場合

 異なるフォーカススコープ属する2つのフォーカスノードautofocus設定することは問題ありません
それぞれのフォーカススコープがフォーカスされた時、その中の autofocus を持つウィジェットがフォーカスされます。
つまり、スコープごとに独立して autofocus が機能します。

(3-4)注意点

 autofocus 属性が機能するのは、そのフォーカススコープ内に既にフォーカスされているノードがない場合に限られます。
既に他のウィジェットがフォーカスされている場合、そのウィジェットが優先され、 autofocus の効果は無視されます

(3-5)サンプルコード

 以下はautofocus基本的な例です。
このコードでは、アプリ起動時にTextFieldに自動でフォーカスが当たります

(autofocusの基本的な例)アプリ起動時にTextFieldに自動でフォーカスが当たります:
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Autofocus Example')),
        body: const Center(
          child: TextField(
            autofocus: true,  //ここで自動フォーカスを設定
          ),
        ),
      ),
    );
  }
}

(3-6)まとめ

  • 1つのフォーカススコープにつき1つのウィジェットに autofocus を設定することが重要です。
  • フォーカススコープ間では複数の autofocus 設定が可能ですが、それぞれのスコープがフォーカスされるタイミングでのみ有効になります。

(4)変更通知

 Focus.onFocusChanged コールバックは、特定のFocus nodeフォーカスの状態を変更した時通知受け取るために使用されます。このコールバックを使用することで、フォーカスがそのノードに割り当てられたり解除されたりした際に反応できます

(4-1)Focus.onFocusChangedの役割

  • このコールバックは、フォーカスノードがフォーカスチェインに追加された場合や、フォーカスが解除された場合に通知を受け取ります。
    フォーカスチェインとは、現在フォーカスされているすべてのフォーカスノードの順序を意味します。

(4-2)onFocusChangedは主フォーカスに限らない

  • onFocusChangedコールバックは、そのフォーカスノードが主フォーカス(Primary Focus)でなくても通知を受け取ります。主フォーカスは、フォーカスチェイン内で一番外側にあるフォーカスノードを指します。
  • もし主フォーカスを受け取ったかどうかだけに興味がある場合は、そのフォーカスノードのhasPrimaryFocusプロパティを使って確認することができます。このプロパティがtrueの場合、そのノードが主フォーカスを受け取っていることを意味します。

(4-3)使いどころ

  • ウィジェットがフォーカスされたときや、フォーカスが外れたときに、何かの状態を更新したい場合に役立ちます。例えば、テキストフィールドがフォーカスを得たら色を変える、などの操作をすることができます。

(4-4)サンプルコード

 以下は、onFocusChanged を使って、フォーカス状態が変わる度に背景色を変更し、さらにそのウィジェットが Primary focus かどうかをチェックする例です。

onFocusChanged を使って、フォーカス状態が変わる度に背景色を変更し、さらにそのウィジェットが Primary focus かどうかをチェックする(例):
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text(
          'Focus Change Example')),
        body: const Center(
          child: MyFocusableWidget(),
        ),
      ),
    );
  }
}

class MyFocusableWidget extends StatefulWidget {
  const MyFocusableWidget({super.key});

  @override
  State<MyFocusableWidget> createState() => _MyFocusableWidgetState();
}

class _MyFocusableWidgetState extends State<MyFocusableWidget> {
  Color _color = Colors.white;
  FocusNode _focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    _focusNode.addListener(() {
      //フォーカスが変更時にコールバックされる
      setState(() {
        _color = _focusNode.hasPrimaryFocus ? Colors.lightBlue : Colors.white;
      });
    });
  }

  @override
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        //ウィジェットのタップ時にフォーカスをリクエスト
        _focusNode.requestFocus();
      },
      child: Focus(
        focusNode: _focusNode,
        onFocusChange: (bool focused) {
          //フォーカス状態変更時に通知を受け取る
          print('フォーカス状態が変わりました: $focused');
        },
        child: Container(
          width: 300,
          height: 100,
          alignment: Alignment.center,
          color: _color,  //フォーカスに応じた色
          child: Text(_focusNode.hasPrimaryFocus ? '主フォーカス' : 'フォーカスなし'),
        ),
      ),
    );
  }
}

(4-5)解説

  • このコードでは、Focusウィジェットの onFocusChange を使って、フォーカスが変わったことを通知しています。
    また、FocusNode hasPrimaryFocus プロパティを使って、そのフォーカスノードが現在 Primary focus であるかどうか確認しています。
  • タップ時、そのウィジェットがフォーカスを取得し、背景色が変わり、テキストが「Primary focus」に変わります。

(4-6)まとめ

  • onFocusChangedフォーカスの変更時通知受け取るための便利なコールバックです。
  • Primary focus かどうか確認するには、hasPrimaryFocusプロパティを使います。
  • 複数の Focus node の管理や、フォーカスの変更時UIの状態を更新したい場合有用です。

(5)FocusNode の取得

 FocusNode は、フォーカス管理の中心的なオブジェクトです。
時には、Focusウィジェット内の FocusNode 接続して、その属性確認したり、制御したりする必要があります。
FocusNode 取得するには、次の様な方法があります。

(5-1)FocusNodeを取得する方法

(A)FocusNode を直接に 渡す

 Focusウィジェットの focusNode 属性に、直接に FocusNode を作成して渡すことができます。
この FocusNode は状態を保持する必要があるため、StatefulWidget の中で作成し、使い終わったら dispose する必要があります。

(B)Focus.of(context) で取得する

 ウィジェットツリー内で、Focusウィジェットの下位の子ウィジェットが FocusNode に接続する場合、Focus.of(context)メソッドを使って、最も近いFocusウィジェットFocus node を取得できます。
但し、buildメソッド内で直接に接続する場合は、Builderウィジェットを使って正しい context を提供する必要があります。

(5-2)サンプルコード

 以下は、Focus.of(context) を使用して FocusNode に接続し、そのノードが Primary focus を持っているか確認する例です。
Builderウィジェットを使用することで、正しいcontextを確保しています。

Focus.of(context) を使用して FocusNode に接続し、そのノードが Primary focus を持っているかを確認する(例):
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('FocusNode Example'),
        ),
        body: const Center(
          child: MyFocusableWidget(),
        ),
      ),
    );
  }
}

class MyFocusableWidget extends StatefulWidget {
  const MyFocusableWidget({super.key});

  @override
  State<MyFocusableWidget> createState() => _MyFocusableWidgetState();
}

class _MyFocusableWidgetState extends State<MyFocusableWidget> {
  //FocusNodeを状態管理の一部として保持
  final FocusNode _focusNode = FocusNode();

  @override
  void dispose() {
    //ウィジェットが破棄される際に、FocusNodeをdispose
    _focusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      focusNode: _focusNode,
      child: Builder(
        //Builderを使って正しいcontextを取得
        builder: (context) {
          //Focus.of(context)を使ってFocusNodeを取得: 
          //このメソッドを使うと、
          //最も近いFocusウィジェットのFocusNodeにアクセスできます。
          //特に、Builderウィジェットを使うことで、
          //buildメソッド内の適切なcontextを取得し、
          //Focus.of(context)を使用できるようにしています。
          final bool hasPrimary = Focus.of(context).hasPrimaryFocus;
          if (kDebugMode) {
            print('現在のフォーカス状態: $hasPrimary');
          }

          return GestureDetector(
            onTap: () {
              //ウィジェットがタップされたらフォーカスを取得
              _focusNode.requestFocus();
            },
            child: Container(
              width: 200,
              height: 100,
              color: hasPrimary ? Colors.lightGreen : Colors.grey,
              alignment: Alignment.center,
              child: Text(hasPrimary ? '主フォーカス' : 'フォーカスなし'),
            ),
          );
        },
      ),
    );
  }
}
  • FocusNodeを渡す場合:
     StatefulWidgetで作成したFocusNodeをFocusウィジェットに渡すことで、特定のノードにフォーカス制御を行えます。
  • Focus.of(context)の使用:
     子ウィジェットからFocusNodeに接続する際に便利で、正しいcontextを確保する為にBuilderウィジェットを使用します。

(6)タイミング

 フォーカスシステムのタイミングについては、フォーカスが要求された際、その効果は現在のビルドフェーズが完了した後反映されます。この仕組みは、フォーカス変更がウィジェットツリーの任意の部分に再ビルドを引き起こす可能性がある為です。特に、現在フォーカスをリクエストしているウィジェットの祖先ウィジェットを再ビルドする場合があります。

(6-1)フレーム遅延の理由

 フォーカスの変更は、ビルドフェーズ中ではなく、次のフレームまで待つ必要があります。
その理由は、以下の通りです。

  • 祖先ウィジェットの再ビルド
     フォーカスの変更は、ウィジェットツリーの他の部分(特に祖先ウィジェットにも影響を及ぼすことがあります。
    祖先ウィジェットは通常、子ウィジェットよりも先に再ビルドされるべきである為、現在のフレーム内では変更できません
  • 子ウィジェットが祖先ウィジェットを汚さない
     子ウィジェットから祖先ウィジェットに直接影響を与えることはFlutterではできません。
    そのため、フォーカスの変更は、次のフレームで反映されます

(6-2)実際の影響

 例えば、ウィジェットが requestFocus() 呼び出したとしても、フォーカスの変更はその次のフレームで適用されます
この動作は、ビルドフェーズとフレームの更新との整合性を保つために設計されています。
結果として、ウィジェットがフォーカスを取得したり失ったりする際の状態の変更が、一度に正しく処理されることになります。

(6-3)実用的な例

 フォーカスをリクエストした直後に、何らかのUI変更を期待している場合、その変更は即座に反映されないことに注意が必要です。(例えば、フォーカスを得たウィジェットが特定の色に変わる場合でも、それは次のフレームでの変更となります。)
したがって、プログラムがそのビルドフェーズを終了し、新しいフレームが描画されるまで待つ必要があります。

(6-4)まとめ

 このタイミングの仕組みは、ウィジェットツリーの整合性を保ちながら、必要な再ビルドや変更が正しく行われるための重要なメカニズムです。フォーカス変更が遅延する理由を理解しておくことで、ウィジェットの状態管理フォーカスイベントの処理における予測可能性高まります

5.FocusScope ウィジェット

(1)FocusScopeウィジェットの役割

 FocusScope ウィジェットは、 FocusScopeNode を管理する特別なバージョンの Focus ウィジェットです。
このウィジェットは、フォーカスツリーにおける特定のサブツリー内のノードをグループ化するために使用されます。
FocusScope は、ある範囲内でのフォーカストラバーサル(フォーカスの移動)がそのスコープ内で行われるよう制御します。
スコープ外のノードが明示的にフォーカスされない限り、フォーカスはそのスコープ内で循環します。

(2)フォーカスの履歴管理

 FocusScope は、そのサブツリー内で現在のフォーカス状態や、以前にフォーカスされていたノードの履歴を追跡します。
これにより、フォーカスされているノードがフォーカスを失ったり削除された場合、そのスコープ内で直前にフォーカスされていたノードにフォーカスが戻るようになります
この仕組みにより、ユーザーがフォーカスの移動をより自然に感じることができます。

(3)フォーカスがなくなった場合の挙動

 もしスコープ内のすべてのノードがフォーカスを失った場合、FocusScopeフォーカスの「戻る先」としても機能します。
つまり、フォーカスが失われた時にフォーカスを再び移動する為の「起点」として機能し、最初に移動すべきフォーカス可能なコントロールを見つける為の手掛かりを提供します。

(4)FocusScopeNode の動作

 FocusScopeNodeをフォーカスした場合、スコープ内で現在フォーカスされているノードや、直前にフォーカスされていたノードをまずフォーカスしようとします。また、もしそのサブツリー内で autofocus が設定されているノードがあれば、それを優先的にフォーカスします。もしそのようなノードが存在しない場合、FocusScope自体がフォーカスを受け取ります。

(5)まとめ

 FocusScope ウィジェットは、フォーカストラバーサルを制御し、サブツリー内でフォーカスの移動や管理を円滑に行うための強力なツールです。また、フォーカスの履歴を保持することで、フォーカスが失われた場合に自然に再度フォーカスを移動させることができます。

6.FocusableActionDetector ウィジェット

6-1.概要

 FocusableActionDetector ウィジェットは、 以下の機能1つのウィジェット統合したものです。

  • アクション(Actions)
  • ショートカット(Shortcuts)
  • マウス(MouseRegion)
  • フォーカス(Focus)

 上記のウィジェットを使うことで、次の様な機能を実現できます。

(1)アクション(Actions)→ アクションの定義

 ユーザーが実行する操作(たとえば「コピー」や「全選択」など)を設定

(2)ショートカット(Shortcuts)→ ショートカットキーの設定

 特定のキーキーの組み合わせアクションを呼び出すキー入力処理

(3)マウス(MouseRegion)→ ホバー処理

 マウスがウィジェット上にあるかどうかの状態(ホバー)に応じて、見た目を変えたりする。

(4)フォーカス(Focus)→ フォーカス処理

 ウィジェットがフォーカスされた時の振る舞いを管理。

6-2.実装の構成

 FocusableActionDetectorウィジェットは、上述の機能をまとめて提供する非常に便利なツールですが、全ての機能を必要としない場合個々のウィジェット(Actions、Shortcuts、Focusなど)を個別に使っても構いません。
ただし、このウィジェットを使うと、カスタムウィジェットやコントロールに一括してこれらの振る舞いを組み込むのに便利です。

6-3.カスタムウィジェットでの利用例

 例えば、カスタムボタンを作る際に、クリック時にアクションを実行し、フォーカスが当たったりホバーしたりした時に見た目を変えるといった一連の機能FocusableActionDetector で簡単に実装できます

カスタムボタンを作る際に、クリック時にアクションを実行し、フォーカスが当たったり、ホバーしたりした時に見た目を変えるといった一連の機能を FocusableActionDetector で簡単に実装できます:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FocusableActionDetector Example',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('FocusableActionDetector Example'),
        ),
        body: const Center(
          child: MyCustomButton(),
        ),
      ),
    );
  }
}

class MyCustomButton extends StatefulWidget {
  const MyCustomButton({super.key});

  @override
  State<MyCustomButton> createState() => _MyCustomButtonState();
}

class _MyCustomButtonState extends State<MyCustomButton> {
  bool _isHovered = false;
  bool _isFocused = false;

  @override
  Widget build(BuildContext context) {
    //FocusableActionDetectorの定義
    //(1) キーボードショートカットやフォーカス、ホバー
    //     などの操作を統合的に処理します。
    //(2) actionsとshortcutsで、エンターキーを押すと
    //     アクションが実行されるように設定しています。
    return FocusableActionDetector(
      //(1) アクション(エンターキー押下などの動作を設定)
      actions: <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<Intent>(
          onInvoke: (Intent intent) {
            if (kDebugMode) {
              print('ボタンが押されました!');
            }
            return null;
          },
        ),
      },
      //(2) ショートカットキーの設定(エンターキーをショートカットに設定)
      shortcuts: <LogicalKeySet, Intent>{
        LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
      },
      //フォーカス時のハイライト表示:
      // フォーカスが当たったときに、ボタンの周りにオレンジ色の
      // ボーダーが表示されるように設定しています。
      onShowFocusHighlight: (bool focused) {
        setState(() {
          _isFocused = focused;
        });
      },
      //ホバー時のハイライト表示:
      // ホバー時に背景色が変わるように設定しています。
      //  (青→薄い青に変わります)
      onShowHoverHighlight: (bool hovering) {
        setState(() {
          _isHovered = hovering;
        });
      },
      //マウスカーソルの設定:
      // cursorプロパティを使って、ボタン上にマウスをホバーすると
      // カーソルが「クリック可能」を示す手の形に変わります。
      child: MouseRegion(
        cursor: SystemMouseCursors.click,
        child: Container(
          padding: const EdgeInsets.all(16.0),
          decoration: BoxDecoration(
            color: _isHovered ? Colors.blue.shade200 : Colors.blue,
            border: _isFocused
                ? Border.all(color: Colors.orange, width: 2.0)
                : null,
          ),
          child: const Text('アクションボタン'),
        ),
      ),
    );
  }
}

7.Focus 移動の制御

7-1.概要

 ここでは、ユーザがキーボードや他の入力デバイスを使ってフォーカスを移動する機能について触れています。
特に、「タブ移動」は最も一般的で、ユーザが Tabキー を押すと「次」のコントロールにフォーカスが移動します。
この「次」が何を指すのか定義することが、今回のフォーカス移動制御となります。

(1)簡単な例(グリッドレイアウト)

 例えば、グリッドレイアウトの場合、「次」が比較的容易に定義されます
行の終わりでなければ、次のコントロールは右側にあります(右から左へのロケールでは左)。
もし行の最後にいる場合は、次の行の最初のコントロールが「次」になります
しかし、アプリが常にグリッドレイアウトとは限らないため、より詳細なガイダンスが必要になることが多いです。

(2)デフォルトのフォーカス移動( ReadingOrderTraversalPolicy

 ReadingOrderTraversalPolicy は、適切な順序を提供するフォーカス移動制御デフォルトアルゴリズムです。
しかし、複雑なレイアウト特定のデザイン要件がある場合には、ReadingOrderTraversalPolicy では問題が発生します
故に(上記以外にも)以降の様な、必要に応じフォーカス順序をカスタマイズできるメカニズムが必要です。

7-2.FocusTraversalGroup ウィジェット

 FocusTraversalGroup ウィジェットは、フォーカスの順序を制御するために使用されます。
特定のウィジェットサブツリーを(FocusTraversalGroup で)ラップすることで、そのサブツリー完全にフォーカスされてから次のウィジェットやグループに移動するようにします
このようにウィジェットをグループ化するだけで、タブ移動の順序の問題を多く解決できます。

補足:以降似た様な名称が多く混乱し易いのですが、各々の関係は次の様になります。

FocusTraversalPolicy抽象クラス

 以下は上記のサブクラス(実装時継承しなくとも暗黙のうちにこの抽象クラスを継承している)です。

 ① OrderedTraversalPolicy

   数値やアルファベット順など)特定の順序を定義してフォーカス移動を提供します。
   このポリシーを利用する際に、④ FocusTraversalOrder を使って順序を指定します。

 ReadingOrderTraversalPolicy(デフォルト)

   読み順(左から右、上から下)に基づくフォーカス移動を提供します。

 WidgetOrderTraversalPolicy

   ウィジェットが配置されている順序に従ってフォーカス移動を行うポリシーです。
   (ウィジェット順序UI外観ではなく、ウィジェットがコード内で記述された順

 次の ④ FocusTraversalOrder による順序指定方法:
 (① OrderedTraversalPolicy をpolicy に設定した場合)FocusTraversalOrderウィジェットをフォーカス可能なコンポーネントの周りにラップすることで、移動順序を指定できます。
 移動の順序を指定するために、NumericFocusOrder LexicalFocusOrder といった FocusOrder クラスのサブクラスを使用します。

Dart
  Widget build(BuildContext context) {
    //FocusTraversalGroup:フォーカスグループを形成し、TraversalPolicyを指定
    //OrderedTraversalPolicy を使ってボタンのフォーカス順序を制御
    return FocusTraversalGroup(
      policy: OrderedTraversalPolicy(),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,//ボタンを中央揃え
        children: <Widget>[
          const Spacer(), //スペーサーでボタンの間にスペースを追加
          FocusTraversalOrder(
            //NumericFocusOrder(2):
            // フォーカス順序を「2」に設定(2番目にフォーカスされる)
            order: const NumericFocusOrder(2),
            child: TextButton(
              onPressed: () {
                //ボタンが押されたときの処理(コンソールに出力)
                if (kDebugMode) {
                  print('Button ONE pressed');
                }
              },
              child: const Text('ONE'),
            ),
          ),

(4)カスタムポリシーの作成

 提供されているフォーカス移動ポリシーが要件に合わない場合は、独自のポリシーを作成し、希望する移動順序をカスタマイズすることもできます。

(5)サンプルコード:ボタンの移動順序を指定

 次の例では、NumericFocusOrder を使用して、ボタンを TWO, ONE, THREE の順に移動させています。

次の例では、NumericFocusOrder を使用して、ボタンを TWO, ONE, THREE の順に移動させる(例):
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FocusTraversalGroup( フォーカス移動 )の例:',
      home: Scaffold(
        appBar: AppBar(
          title: const Text(
            'FocusTraversalGroup( フォーカス移動 )の例:'),
        ),
        //中央に OrderedButtonRow を配置
        body: const Center(
          child: OrderedButtonRow(),
        ),
      ),
    );
  }
}

//ボタンのフォーカス順序を定義するクラス
class OrderedButtonRow extends StatelessWidget {
  const OrderedButtonRow({super.key});

  @override
  Widget build(BuildContext context) {
    //FocusTraversalGroup:
    //(フォーカスグループを形成し、TraversalPolicy を指定)
    return FocusTraversalGroup(
      //OrderedTraversalPolicy によりボタンのフォーカス順序を制御
      policy: OrderedTraversalPolicy(),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center, //ボタンを中央に揃える
        children: <Widget>[
          const Spacer(), //スペーサーでボタンの間にスペースを追加
          FocusTraversalOrder(
            //NumericFocusOrder(2):
            //(フォーカス順序を「2」に設定(2番目にフォーカスされる))
            order: const NumericFocusOrder(2),
            child: TextButton(
              onPressed: () {
                //ボタンが押されたときの処理(コンソールに出力)
                if (kDebugMode) {
                  print('Button ONE が押されました');
                }
              },
              child: const Text('ONE'),
            ),
          ),
          const Spacer(),
          FocusTraversalOrder(
            //NumericFocusOrder(1): 最初にフォーカスされる
            order: const NumericFocusOrder(1),
            child: TextButton(
              onPressed: () {
                if (kDebugMode) {
                  print('Button TWO が押されました');
                }
              },
              child: const Text('TWO'),
            ),
          ),
          const Spacer(),
          FocusTraversalOrder(
            //NumericFocusOrder(3): 3番目にフォーカスされる
            order: const NumericFocusOrder(3),
            child: TextButton(
              onPressed: () {
                if (kDebugMode) {
                  print('Button THREE が押されました');
                }
              },
              child: const Text('THREE'),
            ),
          ),
          const Spacer(),
        ],
      ),
    );
  }
}

7-3.FocusTraversalPolicy

 (上記と重複してしまいますが)FocusTraversalPolicyは、ウィジェット間のフォーカス移動を制御するための抽象クラスです。
このクラスは、ユーザの要求(リクエスト)に基づいて、現在フォーカスされているノードから次にどのウィジェットにフォーカスを移すかを決定する役割を担います。

(1)フォーカス移動のリクエスト

 FocusTraversalPolicyには、いくつかのメンバ関数があります。
これらは、フォーカス移動の具体的なリクエストに基づいて使用されます。
代表的なリクエストには次のようなものがあります。

  • findFirstFocus
     最初のフォーカス可能なウィジェット見つけます
  • findLastFocus
     最後のフォーカス可能なウィジェット見つけます
  • next
     現在のフォーカスから次のウィジェットにフォーカスを移します
  • previous
     現在のフォーカスから前のウィジェットにフォーカスを戻します
  • inDirection
     方向(上下左右)に基づいて、フォーカス移動を行います。

 これらのメソッドは、ユーザーの操作やアプリケーションのフォーカス移動の要求に応じて呼び出され、次にどのウィジェットにフォーカスを移すかを決定します。

(2)抽象クラスと具象クラス

 FocusTraversalPolicyは抽象クラスであり、実際のフォーカス移動のロジックを定義するために、いくつかの具象クラス具体的な実装を持つクラス)が存在します。これには、次のようなクラスがあります。

  • ReadingOrderTraversalPolicy
     左から右、上から下など、読み順に基づいてフォーカスを移動させます。標準的なレイアウトに適しています。
  • OrderedTraversalPolicy
     (数値やアルファベットの順序等)開発者がフォーカス移動順序を手動で設定できます。
  • DirectionalFocusTraversalPolicyMixin
     特定の方向に基づいたフォーカス移動を実装するためのミックスインクラスです。

(3)FocusTraversalGroupとの連携

 FocusTraversalPolicyは直接使われることは少なく、主にFocusTraversalGroupウィジェットに渡されます。
このウィジェットは、フォーカス移動を管理するためのポリシーが適用されるウィジェットツリーの一部を定義します。
例えば、特定のウィジェットグループ内でのフォーカス移動の順序をカスタマイズしたい場合、FocusTraversalGroupを使用してそのグループにOrderedTraversalPolicyを適用することができます。

(4)主にフォーカスシステムによって使用される

 このクラスのメンバ関数は、通常、直接呼び出されることはありません。
これらの関数は、フォーカスシステムの内部で使用され、アプリケーション全体のフォーカスの流れを制御します。
開発者がカスタムのフォーカス移動を実装したい場合には、これらの関数をオーバーライドすることができます。

(5)まとめ

 FocusTraversalPolicyは、ウィジェット間でのフォーカス移動を制御するための抽象的なポリシーであり、アプリケーション全体のフォーカスフローを管理するのに役立ちます。標準的なフォーカス移動にはデフォルトのポリシーが適しており、特殊なシナリオでは、開発者が独自のポリシーを定義して使用することができます。

8.フォーカスマネージャ

 FocusManager は、「 現在の primaryFocus 」(つまり、ユーザが操作している主要な Focus node )を管理します。

8-1.primaryFocus

 FocusManager.instance.primaryFocus プロパティは、現在フォーカスされている Focus node を返します
これはアプリ全体の中で「どのウィジェットが現在フォーカスされているか」を知るために役立ちます。
また、primaryFocus グローバルなフィールドとしてもアクセス可能で、現在のフォーカス状態をプログラム的に取得したい場合に利用されます。

8-2.highlightMode と highlightStrategy

 highlightMode highlightStrategy は、フォーカスハイライトの表示方法に関係します。
これは、ユーザがタッチデバイスや伝統的な入力デバイス(マウスやキーボード)を使用しているかに応じて、UI上でフォーカスハイライトを切り替えるために使用されます。

  • highlightMode
     現在のデバイス入力モードを示します。
    (例えば、タッチスクリーンの操作では通常フォーカスハイライトを隠し、マウスやキーボードの操作時にはハイライトを表示します。)
  • highlightStrategy
     デバイスの入力モード応じたフォーカスハイライト切り替えを指定します。
    (デフォルトでは、最新の入力イベント(タッチ、マウス、キーボード)に基づき、自動的にタッチモード従来モードに切り替えます。但し、この動作を固定(ロック)して、常にタッチモード従来モードを維持することも可能です。)

 これらの属性は、アプリのフォーカスハイライトが自動的に適切なモードで表示されるように制御します。
デフォルトのウィジェットはこの動作を既にサポートしている為、通常はカスタムコントロール作成時にのみ使用されます。

8-3.addHighlightModeListener

 フォーカスのハイライトモードが変更された際に、その変化をリッスンすることも可能です。
addHighlightModeListener を使用して、デバイスがタッチモードからマウス/キーボードモードに切り替わる際の通知を受け取り、それに応じた処理を行うことができます。

8-4.まとめ

 FocusManagerは、アプリ内でのフォーカスの流れを管理し、特にフォーカスハイライトの表示方法をデバイスの入力モードに応じて制御します。ほとんどの場合、標準ウィジェットがこの仕組みを自動的に処理してくれるため、カスタムウィジェットを作成する場合にのみこのAPIを使用する必要があります。

コメントを残す