4
0

【Flutter】テキストフィールドのフォーカス解除と条件付きの場合のコード理解

Last updated at Posted at 2023-12-14

やりたいこと

テキストフィールドでのフォーカス解除をやりたい

フォーカスの解除

フォーカス解除の一般的な方法は以下になります。

primaryFocus?.unfocus();

このコードは、現在のプライマリフォーカスを持つウィジェットからフォーカスを外す処理を行います。

プライマリフォーカスを持つウィジェットとは?
その時点でユーザーの入力(キーボード入力など)を直接受け取るウィジェットのことです。

primaryFocus?.unfocus();を呼び出すことで、テキストフィールドからフォーカスを外すことが出来ます。

条件付きフォーカス解除

primaryFocus?.unfocus();でもフォーカスを解除できますが、ググっていると以下のような条件でフォーカス解除している例を見ます。

これが具体的に何をしているのかを私自身が理解するために、使用例をもとに解説していきます。

final currentScope = FocusScope.of(context);
if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
  primaryFocus?.unfocus();
}

使用例

ユーザーがメイン画面のテキストフィールドにフォーカスを当てている状態から、ダイアログを表示し、ダイアログ内の「閉じるボタン」をタップすることで、メイン画面のテキストフィールドからフォーカスを外す動作で検証します。

例①:条件なしの場合

Closeボタンをタップした時に以下のメソッドを呼び出しています。

void _unFocusFirstVersion() {
    primaryFocus?.unfocus();
}

挙動
focus_first.gif

Closeボタンをタップすると、ダイアログを閉じた後テキストフィールドのフォーカスが解除されていることがわかります。

では次の例を見てみましょう

例②:条件ありの場合

Closeボタンをタップした時に以下のメソッドを呼び出しています。

  void _unFocusSecondVersion(BuildContext context) {
    final currentScope = FocusScope.of(context);
    if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
      primaryFocus?.unfocus();
    }
  }

挙動
focus_second.gif

「Closeボタン」をタップすると、ダイアログを閉じた後テキストフィールドにフォーカスが当たっています。

条件付きフォーカス解除の解説

それでは以下のコードが何をしているのかを解説していきます。

final currentScope = FocusScope.of(context);
if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
  primaryFocus?.unfocus();
}

①現在のフォーカススコープの取得

final currentScope = FocusScope.of(context); 

この行は、現在のウィジェットツリーのコンテキストにおけるフォーカススコープを取得します。

現在のウィジェットツリーのコンテキストとは?
contextというパラメータは、現在のウィジェットの位置や状態に関する情報を持っています。FocusScope.of(context) を呼び出すことで、そのウィジェットが属するフォーカススコープを取得します。

フォーカススコープとは?
フォーカススコープは、フォーカスを受け取ることができるウィジェットのグループを管理する仕組みです。

Flutterでは、ウィジェットツリー内の各部分が独自のフォーカススコープを持つことができ、これによりアプリケーション内の異なるUI部分(例えば、画面全体、ダイアログ、フォームなど)でフォーカスを個別に管理できます。

②フォーカスの条件チェック

if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) { ... }

現在のフォーカススコープがプライマリフォーカスを持っていない場合にのみフォーカスを解除します。

!currentScope.hasPrimaryFocus:現在のフォーカススコープにプライマリフォーカス(ユーザーの入力を受け取るアクティブなウィジェット)が存在しない。

currentScope.hasFocus:現在のフォーカススコープ内のいずれかのウィジェットがフォーカスを持っている。

③フォーカスの解除

primaryFocus?.unfocus();

現在のプライマリフォーカス(アクティブなフォーカス)を持つウィジェットからフォーカスを外します。

全体の流れ

①ダイアログのフォーカススコープ

ダイアログが表示されると、新しいフォーカススコープが作成されます。
このスコープ内には、ダイアログ内の要素(例えば、テキストフィールドやボタン)が含まれます。これにより、ダイアログ内のフォーカス管理が可能になります。

②プライマリフォーカスの位置

ダイアログが開かれると、通常はプライマリフォーカスがダイアログの最初のフォーカス可能な要素に移動します。
したがって、プライマリフォーカスはメイン画面の要素からダイアログ内の要素に移行します。

③フォーカス解除の条件

_unFocusSecondVersionメソッドの条件は、ダイアログ(現在のフォーカススコープ)がプライマリフォーカスを持っていない場合にのみtrueとなります。
これは、ダイアログ内の要素がフォーカスを持っているが、それがプライマリフォーカスではない場合にフォーカスを解除することができることを意味します。

しかし、メイン画面のテキストフィールドは、ダイアログのフォーカススコープとは独立しているため、このメソッドでは影響を受けません。そのためフォーカスは当たったままとなります。

条件付きフォーカス解除の使用例について

あくまで条件の比較のため上記の使用例で説明していましたが、
_unFocusSecondVersion の本来の使い方としては、ダイアログ内の要素がフォーカスを受けた後(例えばユーザーがダイアログ内のテキストフィールドをタップした後)にそのフォーカスを解除するために適しています。
このメソッドは、特にダイアログやモーダルウィンドウ内でのフォーカス管理に有効かと思います。

全コード

今回使用したコードになります。

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(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  void _showDialog(BuildContext context, bool useFirstVersion) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Dialog'),
          content: const TextField(),
          actions: [
            ElevatedButton(
              child: const Text('Close'),
              onPressed: () {
                if (useFirstVersion) {
                  _unFocusFirstVersion();
                } else {
                  _unFocusSecondVersion(context);
                }
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _unFocusFirstVersion() {
    primaryFocus?.unfocus();
  }

  void _unFocusSecondVersion(BuildContext context) {
    final currentScope = FocusScope.of(context);
    if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
      primaryFocus?.unfocus();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Focus Demo'),
      ),
      body: Center(
        child: Column(
          children: [
            const TextField(
              autofocus: true,
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: () => _showDialog(context, true),
              child: const Text('Open Dialog (条件なし)'),
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: () => _showDialog(context, false),
              child: const Text('Open Dialog (条件あり)'),
            ),
          ],
        ),
      ),
    );
  }
}

参考記事

4
0
0

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
4
0