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の基礎文法を素早くインプットできる/





