🫠

pointer_interceptorを使ってHtmlElementView上のFlutterオブジェクトを押せるようにする

2024/05/12に公開

何に困っているのか

Flutter Webを使ってOSM(Open Street Map)の地図を表示したいと思ってflutter_osm_pluginを使用しました。

その後、Stackウィジェットを使って地図の上にElevatedButtonを重ねて、ElevatedButtonをクリックしてみたらonPressedプロパティ内の処理が実行されません。なんならタップされているかどうかすら怪しく見える...。

解答

flutter_osm_pluginのページに答えが書いてました。

Deeplで翻訳すると...。

ユーザーが地図上をクリックするのを管理するボタンやUIを表示するには、次のライブラリを使用する必要がある: pointer_interceptor

どうやらpointer_interceptorというパッケージが必要らしいです。

何が原因なのか

pointer_interceptorのページに親切にもイラスト付きで説明されています。

青線枠で囲まれた部分をDeeplで翻訳してみます。

マウスジェスチャーに反応する(例えばクリックを扱う)HtmlElementView/PlatformViewウィジェットの上にFlutterウィジェットを重ねると、クリックはHtmlElementView/PlatformViewによって消費され、Flutterには中継されません。

その結果、FlutterウィジェットのonTap(やその他の)ハンドラは期待通りに発火しませんが、基盤となるネイティブプラットフォームビューに影響を与えます。

どうやらクリックがHtmlElementViewやPlatformViewに奪われて、Flutter Widgetに届かないようです。

実際にやってみた

pointer_interceptorパッケージを使って地図上にダイアログを表示したいと思います。

開発環境、SDK・使用するパッケージのバージョン

開発環境

PC : MacBook Air
OS : macOS Sonoma 14.2.1
チップ : Apple M1
メモリ : 16GB
エディター : Android Studio Jellyfish | 2023.3.1

SDK・使用するパッケージのバージョン

Flutter SDK : 3.19.6
Dart SDK : 3.3.0
flutter_osm_plugin: ^1.0.3
pointer_interceptor: ^0.10.1+1

地図を表示する

まずマーカーが表示される地図を表示します。マーカーを立てる場所は東京都庁と新宿駅としましょう。

class _MyHomePageState extends State<MyHomePage> {
  late MapController controller;
  
  void initState() {
    super.initState();
    controller = MapController.withPosition(
        initPosition: GeoPoint(
            latitude: 35.6895014,
            longitude: 139.6917337)); //コントローラーのポジションを東京都庁にして初期化。
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: OSMFlutter(
        controller: controller,
        osmOption: OSMOption(
          zoomOption: const ZoomOption(initZoom: 14), //地図の初期ズームレベルを設定する
          staticPoints: [
            StaticPositionGeoPoint(
              '0', //マーカーのID
              const MarkerIcon(
                icon: Icon(
                  Icons.location_on,
                  color: Colors.red,
                ), //マーカーのアイコン
              ),
              [
                GeoPoint(latitude: 35.6895, longitude: 139.6917), //東京都庁の緯度経度
              ], //マーカーを表示するGeoPointのリスト
            ),
            StaticPositionGeoPoint(
              '1', //マーカーのID
              const MarkerIcon(
                icon: Icon(
                  Icons.location_on,
                  color: Colors.blue,
                ), //マーカーのアイコン
              ),
              [
                GeoPoint(latitude: 35.6905, longitude: 139.6995), //新宿駅の緯度経度
              ], //マーカーを表示するGeoPointのリスト
            ),
          ],
        ),
      ),
    );
  }
}

コントローラーの初期のポジションを東京都庁の位置に設定して、初期のズームレベルとマーカーのアイコン・表示する場所を設定しました。
シミュレーターをGoogle Chromeにして実行すると以下の画像のようになると思います。

東京都庁の位置に赤いマーカー、新宿駅の位置に青いマーカーが表示されていることが確認できます。
これで土台となる地図は完成しました。

マーカーを押したらダイアログが表示されるようにする

次にマーカーを押したらダイアログが表示されるようにしましょう。表示する内容は簡単に緯度経度のみにします。

class _MyHomePageState extends State<MyHomePage> {
  late MapController controller;
  
  void initState() {
    super.initState();
    controller = MapController.withPosition(
        initPosition: GeoPoint(
            latitude: 35.6895014,
            longitude: 139.6917337)); //コントローラーのポジションを東京都庁にして初期化。
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: OSMFlutter(
        controller: controller,
        osmOption: OSMOption(
          zoomOption: const ZoomOption(initZoom: 14), //地図の初期ズームレベルを設定する
          staticPoints: [
            StaticPositionGeoPoint(
              '0', //マーカーのID
              const MarkerIcon(
                icon: Icon(
                  Icons.location_on,
                  color: Colors.red,
                ), //マーカーのアイコン
              ),
              [
                GeoPoint(latitude: 35.6895, longitude: 139.6917), //東京都庁の緯度経度
              ], //マーカーを表示するGeoPointのリスト
            ),
            StaticPositionGeoPoint(
              '1', //マーカーのID
              const MarkerIcon(
                icon: Icon(
                  Icons.location_on,
                  color: Colors.blue,
                ), //マーカーのアイコン
              ),
              [
                GeoPoint(latitude: 35.6905, longitude: 139.6995), //新宿駅の緯度経度
              ], //マーカーを表示するGeoPointのリスト
            ),
          ],
        ),
+        onGeoPointClicked: (GeoPoint geoPoint) {
+          showDialog(
+            context: context,
+            builder: (_) {
+              return AlertDialog(
+                content:
+                    Text("緯度:${geoPoint.latitude} 経度:${geoPoint.longitude}"),
+                actions: [
+                  ElevatedButton(
+                    onPressed: () {
+                      Navigator.of(context).pop();
+                    },
+                    child: const Text("閉じる"),
+                  )
+                ],
+              );
+            },
+          );
+        },
      ),
    );
  }
}

onGeoPointClickedプロパティはマーカーがクリックされた時に処理が実行されるコールバック関数です。
Google Chromeで実行して、赤いマーカーを押すと東京都庁の緯度と経度、閉じるボタンが表示されるダイアログが出ました。

しかし、ダイアログを閉じようと思い、閉じるボタンを押してもダイアログが閉じません。ということで次はpointer_interceptorを使ってクリックが閉じるボタンに届くようにしましょう。

ボタンを押したらダイアログが閉じるようにする

それでは閉じるボタンを押したらダイアログが閉じるようにしてみましょう。

class _MyHomePageState extends State<MyHomePage> {
  late MapController controller;
  
  void initState() {
    super.initState();
    controller = MapController.withPosition(
        initPosition: GeoPoint(
            latitude: 35.6895014,
            longitude: 139.6917337)); //コントローラーのポジションを東京都庁にして初期化。
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: OSMFlutter(
        controller: controller,
        osmOption: OSMOption(
          zoomOption: const ZoomOption(initZoom: 14), //地図の初期ズームレベルを設定する
          staticPoints: [
            StaticPositionGeoPoint(
              '0', //マーカーのID
              const MarkerIcon(
                icon: Icon(
                  Icons.location_on,
                  color: Colors.red,
                ), //マーカーのアイコン
              ),
              [
                GeoPoint(latitude: 35.6895, longitude: 139.6917), //東京都庁の緯度経度
              ], //マーカーを表示するGeoPointのリスト
            ),
            StaticPositionGeoPoint(
              '1', //マーカーのID
              const MarkerIcon(
                icon: Icon(
                  Icons.location_on,
                  color: Colors.blue,
                ), //マーカーのアイコン
              ),
              [
                GeoPoint(latitude: 35.6905, longitude: 139.6995), //新宿駅の緯度経度
              ], //マーカーを表示するGeoPointのリスト
            ),
          ],
        ),
        onGeoPointClicked: (GeoPoint geoPoint) {
          showDialog(
            context: context,
            builder: (_) {
+              return PointerInterceptor(
+                child: AlertDialog(
+                  content:
+                      Text("緯度:${geoPoint.latitude} 経度:${geoPoint.longitude}"),
+                  actions: [
+                    ElevatedButton(
+                      onPressed: () {
+                        Navigator.of(context).pop();
+                      },
+                      child: const Text("閉じる"),
+                    )
+                  ],
+                ),
+              );
            },
          );
        },
      ),
    );
  }
}

閉じるボタンを押せるようにするだけなら簡単で、AlertDialogPointerInterceptorでWrapします。
こうすることで、ダイアログ内の閉じるボタンを押すとダイアログが閉じることが確認できました。やったね!

と思ったのですが、ダイアログの領域外を押してもダイアログが閉じなくなってしまいました...。
これの原因はまだ調査できてないので、今後の課題とします...。

最後に

ここまで読んでいただきありがとうございました!
今回載せたコードの全文はGithubで公開しています。
https://github.com/Yoshikawa-0918/pointer_interceptor_sample

まだ「クリックがHtmlElementViewやPlatformViewに奪われて、Flutter Widgetに届かない理由」や、「ダイアログの領域外を押してもダイアログが閉じなくなってしまう理由」を完璧に調査することができていませんがひとまずHtmlElementView上のFlutterオブジェクトを押せないな〜っていう時はpointer_interceptorというパッケージで解決できるということを知っていただけたら嬉しいです。

また、Zennに他の技術系記事を上げたり、Qiitaにもイベント参加レポや技術系記事を上げているのでよろしければチェックしてくださいー♪
https://qiita.com/Kyomu777

Discussion