Flutter:ボタンにtooltipと長押し(onLongPress)処理を同時に設定する方法

Flutter

ボタンに、ツールチップ(tooltip)長押し(onLongPress)処理の両方を設定したいけど、片方しか発動できない。解決方法はないの?

という方向けの記事です。

 

1つのボタンに、①タップ処理②長押し処理③ツールチップの3つを設定したかったのですが、単純にやろうとすると、長押し処理かツールチップの片方しか発動できませんでした。

 

※ツールチップ(tooltip)は、ボタンを長押しすると、簡単な説明ポップアップを表示できる機能です。

 

ネット上にもズバリの情報は見つからず、解決法を見つけるのに苦労したので、共有したいと思います。

 

結論は、以下になります。

 

  • IconButtonをInkWell(またはGestureDetector)でラップし、さらにTooltipクラスでラップする
  • TooltipクラスにGlobalKeyを設置する
  • タップ処理はIconButton、長押し処理はInkWell(またはGestureDetector)に設定する
  • 長押し処理の中で、GlobalKeyの「currentState」プロパティを通じて、ツールチップを表示させるための「ensureTooltipVisible」メソッドを呼び出す

 

ツールチップを表示させるクラスを自作する手もあると思いますが、大変なので、、、できるだけ簡単にできる方法を模索した結果、上記方法になりました。

 

こちらの情報が参考になりました。ありがとうございます!

 

 

 

なお、IconButtonに、通常タップと長押しを同時に設定する方法については、下記記事に整理しましたので、よろしければご参考にして下さい。

 

 

以降に、自作したサンプルコードを使ってご説明します。

 


 

40代からプログラミング(Flutter)を始めて、GooglePlayAppStoreにアプリを公開しているhalzo appdevです。

 

作成したアプリはこちら↓ 全てFlutterで開発したアプリです。

 

超即ToDo –最短2タップで通知登録できるタスク管理アプリ

Google Play で手に入れよう
Download on the App Store

 

かんたんプリント管理:アラート・OCR文字認識・検索機能を搭載

Google Play で手に入れよう
Download on the App Store

 

シンプルメモ帳「BasicMemo」 – 文字カウント、ワンタッチ入力、タグ管理等の機能を搭載

Google Play で手に入れよう
Download on the App Store

 

作成したサンプルコードの全体像

先に、作成したサンプルコードを掲載します。そのまま「main.dart」に貼り付ければ、挙動を確認いただけます。

 

どのボタンも、タップすると数字が1増え、長押しすると1減る仕様になっています。

 

左側のボタン、真ん中のボタンは、うまく動作しない場合の例です。

 

右側のボタンが、期待どおりに動作する場合の例です。

 

// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの)
// の名前の末尾には、大文字のオー「O」をつけています
// ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、
//  自分で作成したもの(名前の変更可のもの)の区別をしやすくするため

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Test",
      home: SampleScreenO(),
    );
  }
}

class SampleScreenO extends StatefulWidget {
  @override
  _SampleScreenOState createState() => _SampleScreenOState();
}

// ToolTipに設置するグローバルキーを定義
GlobalKey _toolTipKeyO = GlobalKey();

class _SampleScreenOState extends State<SampleScreenO> {

  // ここまではお決まりのコード

  // カウンター変数を定義
  int counterO = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            // 方法① うまく動作しない例 ※長押しが動作しない
            InkWell(
              onLongPress: () => setState(() {
                counterO--;
              }),
              child: IconButton(
                icon: Icon(Icons.add),
                onPressed: () => setState(() {
                  counterO++;
                }),
                tooltip: "説明を表示",
              ),
            ),

            SizedBox(
              width: 8,
            ),

            // 方法② うまく動作しない例 ※ツールチップが表示されない
            // 任意のウィジェットにツールチップを表示できるTooltipクラスでラップ
            Tooltip(
              message: "説明を表示",
              child: InkWell(
                onLongPress: () => setState(() {
                  counterO--;
                }),
                child: IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () => setState(() {
                    counterO++;
                  }),
                ),
              ),
            ),

            // 方法③ 動作する例(解決策)
            Tooltip(

              // グローバルキーを設置
              key: _toolTipKeyO,

              message: "説明を表示",

              // ↓InkWellは「GestureDetector」でも可能
              child: InkWell(

                // 長押し処理はInkWellまたはGestureDetectorに設置
                onLongPress: () {

                  // グローバルキーのcurrentStateにアクセスすることで、
                  // 上記messageの値を保持したTooltipのインスタンスを作成
                  // 但し、ツールチップを表示させるメソッドは、外からはアクセスできない
                  // 「_TooltipState」クラス内にあるため、dynamic型で定義
                  final dynamic _toolTipO = _toolTipKeyO.currentState;

                  // インスタンスを通じて、ツールチップを表示させるメソッドを実行
                  _toolTipO.ensureTooltipVisible();

                  // カウントを1減らして、画面を再描画
                  setState(() {
                    counterO--;
                  });
                },

                // タップ処理はIconButtonの方に設定
                child: IconButton(
                  icon: Icon(Icons.add),

                  // カウントを1増やして、画面を再描画
                  onPressed: () {
                    setState(() {
                      counterO++;
                    });
                  },
                ),
              ),
            ),            
          ],
        ),
      ),
      body: Container(
        alignment: Alignment.topCenter,
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: [

              // カウンター変数を表示
              Text(
                "$counterO",
                style: TextStyle(fontSize: 20),
              ),

              // 画面内に収まるようにFittedBoxでラップ
              FittedBox(
                fit: BoxFit.scaleDown,
                child: Text(
                  "タップで +1、長押しで -1\n【左ボタン】IconButtonにツールチップを設定\n【中ボタン】ToolTipにツールチップを設定\n【右ボタン】GlobalKey経由でツールチップを起動",
                  textAlign: TextAlign.center,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

 

失敗例(方法①、方法②)

方法①(左側のボタン)は、最も単純な例で、「InkWell」に長押し処理、「IconButton」にタップ処理を設定した上で、「IconButton」に「tooltip」属性も設定したパターンです。

 

しかし、この場合はツールチップの表示が優先されてしまい、長押し処理(カウンター変数を-1する処理)が実行されませんでした。

 

長押ししても数字が減らない

 

ツールチップを表示するには、「IconButton」の「tooltip」属性を使う方法のほかに、任意のウィジェットを「Tooltip」クラスでラップする方法があります。

 

そこで、方法②(中央のボタン)では、「InkWell」に長押し処理、「IconButton」にタップ処理を設定した上で、全体を「Tooltip」クラスでラップしました。

 

しかし、今度は長押し処理は実行されますが、ツールチップが表示されませんでした。

 

長押ししてもツールチップが表示されない

 

どうやら、ツールチップと長押し処理が同時に設定された場合、子ウィジェットに設定した方が優先されるようです。

 

この他に、Stackを使って、同じボタンを上下に重ね、「IgnorePointer」を用いて、タップ処理を下層のボタンに伝達する方法(上層にツールチップ、下層に長押し処理を設定)も試みましたが、上層のアクションが発動せず、駄目でした。。

 

解決策(方法③)

ツールチップにも、ダイアログ表示の「showDialog」のようなメソッドがあれば、「onLongPress」の処理中で実行できると思い、Flutter公式サイトやパッケージ集(pub.dev)を探しましたが、見つかりませんでした。

 

その他、日本語・英語の情報をかなり調べましたが、ツールチップに関する多くの情報は、「通常のタップ(長押しではなく)でツールチップを起動させる方法」に関するものでした。

 

ただ、その中で、下記情報は応用できそうでした。

 

 

 

Flutter標準のライブラリである「tooltip.dart」内のメソッドに、GlobalKeyを用いて直接アクセスする方法が紹介されています。

 

この方法によると、「ToolTip」クラスにGlobalKeyを設定し、その「currentState」のインスタンスを作れば、「ToolTip」クラスの内部コードのメソッドにアクセスできるようになります。

 

※「ToolTip」クラスのソースコードは、Flutter標準のライブラリである「tooltip.dart」にあります。

 

ensureTooltipVisible」は、「ToolTip」クラス(Stateful Widgetなので、厳密には「_TooltipState」クラス内)で定義されているメソッドです。

 

ソースコードの説明を見ると、「ツールチップが未表示の場合に、表示させるメソッド」と書かれています。

 

※「tooltip.dart」は、サンプルコード中の「ensureTooltipVisible」をF4しても辿れないので、Android Studioの場合は、「command+shift+F」(Find in Path)で「ensureTooltipVisible」を検索すると、見つけられるかと思います。

 

上記StackOverflowのコードは、通常のタップ時にツールチップを起動させる例になっているので、これを方法③のコードのように書き換え、無事、動作させることができました。

 

長押しで数字を減らし、かつツールチップも表示できている

 

親Widgetを「ToolTip」クラスとし、そこに設置したGlobalKeyを、子Widgetの「InkWell」内で使用する、というところがポイントです。

 

また、上記Twitter記事の方が解説くださっていますが、「_TooltipState」は外部からアクセスできないため、通常は「_TooltipState」型のインスタンスを作れないのですが、「dynamic」型で定義することによって、この問題を回避している、という点は大変勉強になりました。

 

留意点と補足

  • 「ensureTooltipVisible」は、Flutter標準のライブラリ内で使用されている内部コードのメソッドで、開発者が外部からアクセスすることは想定されていないため、バージョンアップ等で変更される恐れがある点は注意が必要です。

 

  • 「Tooltip」クラスを下記のように変更することで、通常タップ時にツールチップを表示させることもできます(ただ、表示後にツールチップを消すには、どこかをタップする必要があります)。この場合、タップ処理は、競合が発生しないように、アイコン側には設定せず、InkWellの方にツールチップ表示とまとめて設定する必要があります。

 

            Tooltip(
              key: _toolTipKeyO,
              message: "説明を表示",
              child: InkWell(

                // onTap属性をInkWellの方に設定
                // ツールチップ表示とカウント加算処理をまとめて設定 
                onTap: () {
                  final dynamic _toolTipO = _toolTipKeyO.currentState;
                  _toolTipO.ensureTooltipVisible();

                  setState(() {
                    counterO++;
                  });
                },

                onLongPress: () {
                  setState(() {
                    counterO--;
                  });
                },

                // 処理を設定しないため、単なるIconウィジェットにする
                child: Icon(Icons.add),
              ),
            ),

 

  • そもそも競合が発生しないように、タップ処理→加算、ダブルタップ処理→減算、長押し処理→ツールチップ表示、のように、タップアクションを3つに分ける方法も考えました(以下がコード例です)。

 

            InkWell(

              // 減算処理はダブルタップにする
              onDoubleTap: () => setState(() {
                counterO--;
              }),

              child: IconButton(
                icon: Icon(Icons.add),
                onPressed: () => setState(() {
                  counterO++;
                }),
                tooltip: "説明を表示",
              ),
            ),

 

 ただ、ダブルタップ時にタッチフィードバックのマークが消えない状況が生じるため、採用できませんでした。

 

右側のボタンを、ダブルタップで減算に変更した場合の例

 

 今のところ、この解決法が分かっていないため、、、判明したら更新したいと思います。

 


以上、ご参考になれば幸いです。

 

最後までお読みいただき、ありがとうございました。

 

\ Flutterの学習で役立ったコンテンツ・書籍 /

The Complete 2021 Flutter Development Bootcamp with Dart

 

 


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

コメント

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