やりたいこと
テキストフィールドでのフォーカス解除をやりたい
フォーカスの解除
フォーカス解除の一般的な方法は以下になります。
primaryFocus?.unfocus();
このコードは、現在のプライマリフォーカスを持つウィジェットからフォーカスを外す処理を行います。
プライマリフォーカスを持つウィジェットとは?
その時点でユーザーの入力(キーボード入力など)を直接受け取るウィジェットのことです。
primaryFocus?.unfocus();
を呼び出すことで、テキストフィールドからフォーカスを外すことが出来ます。
条件付きフォーカス解除
primaryFocus?.unfocus();
でもフォーカスを解除できますが、ググっていると以下のような条件でフォーカス解除している例を見ます。
これが具体的に何をしているのかを私自身が理解するために、使用例をもとに解説していきます。
final currentScope = FocusScope.of(context);
if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
primaryFocus?.unfocus();
}
使用例
ユーザーがメイン画面のテキストフィールドにフォーカスを当てている状態から、ダイアログを表示し、ダイアログ内の「閉じるボタン」をタップすることで、メイン画面のテキストフィールドからフォーカスを外す動作で検証します。
例①:条件なしの場合
Closeボタン
をタップした時に以下のメソッドを呼び出しています。
void _unFocusFirstVersion() {
primaryFocus?.unfocus();
}
Closeボタン
をタップすると、ダイアログを閉じた後テキストフィールドのフォーカスが解除されていることがわかります。
では次の例を見てみましょう
例②:条件ありの場合
Closeボタン
をタップした時に以下のメソッドを呼び出しています。
void _unFocusSecondVersion(BuildContext context) {
final currentScope = FocusScope.of(context);
if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
primaryFocus?.unfocus();
}
}
「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 (条件あり)'),
),
],
),
),
);
}
}
参考記事