ボタンに、ツールチップ(tooltip)と長押し(onLongPress)処理の両方を設定したいけど、片方しか発動できない。解決方法はないの?
という方向けの記事です。
1つのボタンに、①タップ処理、②長押し処理、③ツールチップの3つを設定したかったのですが、単純にやろうとすると、長押し処理かツールチップの片方しか発動できませんでした。
※ツールチップ(tooltip)は、ボタンを長押しすると、簡単な説明ポップアップを表示できる機能です。
ネット上にもズバリの情報は見つからず、解決法を見つけるのに苦労したので、共有したいと思います。
結論は、以下になります。
- IconButtonをInkWell(またはGestureDetector)でラップし、さらにTooltipクラスでラップする
- TooltipクラスにGlobalKeyを設置する
- タップ処理はIconButton、長押し処理はInkWell(またはGestureDetector)に設定する
- 長押し処理の中で、GlobalKeyの「currentState」プロパティを通じて、ツールチップを表示させるための「ensureTooltipVisible」メソッドを呼び出す
ツールチップを表示させるクラスを自作する手もあると思いますが、大変なので、、、できるだけ簡単にできる方法を模索した結果、上記方法になりました。
こちらの情報が参考になりました。ありがとうございます!
なお、IconButtonに、通常タップと長押しを同時に設定する方法については、下記記事に整理しましたので、よろしければご参考にして下さい。
以降に、自作したサンプルコードを使ってご説明します。
40代からプログラミング(Flutter)を始めて、GooglePlayとAppStoreにアプリを公開しているhalzo appdevです。
作成したアプリはこちら↓ 全てFlutterで開発したアプリです。
作成したサンプルコードの全体像
先に、作成したサンプルコードを掲載します。そのまま「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: "説明を表示", ), ),
ただ、ダブルタップ時にタッチフィードバックのマークが消えない状況が生じるため、採用できませんでした。
今のところ、この解決法が分かっていないため、、、判明したら更新したいと思います。
以上、ご参考になれば幸いです。
最後までお読みいただき、ありがとうございました。
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/
コメント