Flutter: TextFieldのToolbarに追加した項目をタップしてもToolbarを消せない

※当サイトは、アフィリエイト広告を利用しています

結論:contextMenuBuilderプロパティの引数経由でhideToolbarメソッドを実行する

2023/9/23 Flutter エラー・バグ日記

 

TextFieldのToolbarをカスタマイズして、独自の項目を追加したが、その追加した項目をタップしても、Toolbarが消えなかった

 

デフォルトで用意されている「Copy」や「Paste」をタップした場合は、自動で消えるのだが、、。

 

 

本記事はライトな日記思考で書いているので、詳細説明はしておらず、基本、テキストのみで画像とかはあまり載せておりません。。m(_ _)m

解説記事ではないため、解決していない内容や、その時々の間違った解釈を述べてしまっている可能性が大いにありますので、何卒、ご了承ください。

 

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


Dart入門 - Dartの要点をつかむためのクイックツアー

タイトルとURLをコピーしました