Flutter: TextFieldのToolbarに追加した項目をタップしてもToolbarを消せない
結論:contextMenuBuilderプロパティの引数経由でhideToolbarメソッドを実行する
2023/9/23 Flutter エラー・バグ日記
TextFieldのToolbarをカスタマイズして、独自の項目を追加したが、その追加した項目をタップしても、Toolbarが消えなかった。
デフォルトで用意されている「Copy」や「Paste」をタップした場合は、自動で消えるのだが、、。
ちなみに、Toolbarに独自の項目を追加するには、TextFieldの「contextMenuBuilder」プロパティを使用する。
// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの) // の名前の末尾には、大文字のオー「O」をつけています // ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、 // 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため // TextField部分の例 TextField( // ・・・(略)・・・ contextMenuBuilder: (contextO, editableTextStateO) { return AdaptiveTextSelectionToolbar.buttonItems( anchors: editableTextState.contextMenuAnchors, buttonItems: editableTextState.contextMenuButtonItems // 標準のToolbarの先頭に以下の項目を追加 ..insertAll( 0, [ ContextMenuButtonItem( label: "追加ボタン", onPressed: () { // ここに追加ボタンを押したときの処理を書く debugPrint("ボタンを追加した"); }, ), ], ), ); }, ),
以前は、「toolbarOption」プロパティで設定できたが、Flutter 3.3.0以降は非推奨となっており、「contextMenuBuilder」プロパティに変わっていた。
選択を解除しても駄目
Toolbarは、テキストが選択された時に表示されるので、下記のように選択状態を強制的に解除すれば、Toolbarも消えるだろうと考えた。
// ・・・(略)・・・ TextEditingController textEditingControllerO = TextEditingController(); // コントローラーを作成 // ・・・(略)・・・ TextField( controller: textEditingControllerO, // コントローラーを設置 contextMenuBuilder: (contextO, editableTextStateO) { // ・・・(略)・・・ onPressed: () async{ debugPrint("ボタンを追加した"); int cursorPositionO = textEditingControllerO.selection.end; // 選択範囲の末尾の位置を保存 textEditingControllerO.selection = TextSelection.collapsed(offset: cursorPositionO); // 選択状態を解除し、末尾の位置にカーソルを設定 // ・・・(略)・・・ ),
しかし、選択は解除されるが、Toolbarの表示は残ったままで、解決にならなかった。
popメソッドで消すと、アプリ全体が落ちる
ネット上の情報を探すも、ズバリの記事は見つからず。。
そこで、Chat-GPTに尋ねたところ、Toolbarは、ダイアログと同じように、popメソッド「Navigator.of(context).pop()」で消すことができるとのこと。
早速試してみたが、Toolbarが消えるのではなく、アプリ全体が落ちてしまった。
フォーカスを外すことで、Toolbarを消す(一応成功)
TextFieldのフォーカスが外れれば、Toolbarも消えるはず。
そこで、Toolbarの追加項目をタップした後、下記コードにより、TextFieldのフォーカスを外すことで、Toolbarを消すことにした。
FocusManager.instance.primaryFocus?.unfocus()
但し、追加項目をタップした後、TextFieldのフォーカス状態を維持したかったので、フォーカスを外した後、再度TextFieldをフォーカスするようにした。
// ・・・(略)・・・ TextEditingController textEditingControllerO = TextEditingController(); FocusNode focusNodeO = FocusNode(); // FocusNodeを定義 // ・・・(略)・・・ TextField( // ・・・(略)・・・ focusNode: focusNodeO, // FocusNodeを設定 controller: textEditingControllerO, contextMenuBuilder: (contextO, editableTextStateO) { // ・・・(略)・・・ onPressed: () async{ debugPrint("ボタンを追加した"); int cursorPositionO = textEditingControllerO.selection.end; textEditingControllerO.selection = TextSelection.collapsed(offset: cursorPositionO); FocusManager.instance.primaryFocus?.unfocus(); // TextFieldのフォーカスを外す await Future.delayed(Duration(milliseconds: 0)); // ダミーの待ち時間を入れる focusNodeO.requestFocus(); // 再度TextFieldにフォーカスを当てる // ・・・(略)・・・ ),
フォーカスを外す処理と、フォーカスする処理が連続すると、機能しないので、Future.delayedでダミーの待ち時間を入れた。
また、フォーカスを戻したときに、カーソルが元の位置に戻るよう、フォーカスを外す前のカーソル位置を記憶させた。
一応、この方法で解決はしたが、キーボードがいったん引っ込んでまた出る、という挙動になるので、違和感が残る。。
hideToolbarメソッドにアクセスする(解決!)
引き続き調べてみると、「EditableTextState」クラスのソースコード内に、「hideToolbar」というメソッドがあることが分かった。これにアクセスできれば、Toolbarを消せそうだ。
// EditableTextState クラス内のソースコード抜粋 @override void hideToolbar([bool hideHandles = true]) { if (hideHandles) { // Hide the handles and the toolbar. _selectionOverlay?.hide(); } else if (_selectionOverlay?.toolbarIsVisible ?? false) { // Hide only the toolbar but not the handles. _selectionOverlay?.hideToolbar(); } }
TextFieldから「hideToolbar」メソッドにアクセスする方法が分からなかったので、Chat-GPTに聞いてみると、「hideToolbar」は「EditableTextState」のプライベートメソッドのため、アクセスできない、との回答。。
諦めかけたが、よく見ると、「contextMenuBuilder」プロパティに設定する関数には、2つ目の引数として、「EditableTextState」型の変数があることに気づいた。
この「EditableTextState」型の引数経由で、「hideToolbar」メソッドにアクセスできるのではと思い、下記コードのように試したところ、無事Toolbarを消すことができた。
// ・・・(略)・・・ TextField( // ・・・(略)・・・ contextMenuBuilder: (contextO, editableTextStateO) { // ・・・(略)・・・ onPressed: () async{ debugPrint("ボタンを追加した"); int cursorPositionO = textEditingControllerO.selection.end; textEditingControllerO.selection = TextSelection.collapsed(offset: cursorPositionO); editableTextStateO.hideToolbar(); // 2つめの引数(editableTextStateO)経由で、hideToolbarを呼び出す // ・・・(略)・・・ ),
単純にToolbarを消すだけなので、TextFieldのフォーカスも維持されたまま。挙動に違和感はない。
サンプルコードの全体像(ご参考)
ご参考として、実際の挙動を確認できる自作のサンプルコードを掲載。
// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの) // の名前の末尾には、大文字のオー「O」をつけています // ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、 // 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Test", theme: ThemeData.light(), home: SampleScreenO(), ); } } class SampleScreenO extends StatefulWidget { @override _SampleScreenOState createState() => _SampleScreenOState(); } class _SampleScreenOState extends State<SampleScreenO> { TextEditingController textEditingControllerO = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("TestApp"), ), body: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Text("↓TextField"), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), height: 300, child: Scrollbar( child: TextField( controller: textEditingControllerO, decoration: InputDecoration( border: InputBorder.none, ), keyboardType: TextInputType.multiline, maxLines: null, // Toolbarに独自の項目を追加 contextMenuBuilder: (contextO, editableTextStateO) { return AdaptiveTextSelectionToolbar.buttonItems( anchors: editableTextStateO.contextMenuAnchors, buttonItems: editableTextStateO.contextMenuButtonItems ..insertAll( 0, [ ContextMenuButtonItem( label: "追加ボタン", onPressed: () async{ // ここに追加ボタンを押したときの処理を書く debugPrint("ボタンを追加した"); int cursorPositionO = textEditingControllerO.selection.end; // テキスト選択範囲の末尾の位置を記憶 textEditingControllerO.selection = TextSelection.collapsed(offset: cursorPositionO); // テキスト選択を解除し、上で記憶した位置にカーソルを設定 editableTextStateO.hideToolbar(); // Toolbarを消す }, ), ], ), ); }, ), ), ), ], ), ), ); } }
右往左往してしまったが、意外に簡単な解決方法だった。。
リリースしたアプリ(全てFlutterで開発)
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/