ふらっとFlutter

Flutterでのアプリ開発についてメモしていきます

Google Mapsで画面に見えている領域の範囲内だけマーカーを表示する(Flutter)

Google Mapsでは対象の場所や施設をわかりやすく表示するためにマーカーが使われます。  
単純なマーカーの表示方法は以前の記事で紹介しました。

flutterdev.hatenablog.com

  今回は少し応用で、「画面に表示されている領域のマーカーだけ」を表示する方法です。

なぜ必要なのか?

イメージしやすいように、実際のアプリを例に説明します。
わたしが開発している「ダムめぐりガイド ビーバー」では、全国にあるダムがマップにマーカーとして表示されます。

大量のマーカーを表示したい場合の話

単純に「ダムのデータ」を「マーカー」に変換して追加すれば良いようにも思えますが、ダムは全国に1,000以上あり、実際にはマーカーが多すぎてレンダリングエラーになってしまいます。

仮にデータ数が少ない場合でもユーザーが見えている範囲外にマーカーを表示することに意味はないため、パフォーマンス的にも損になるでしょう。

そこで、「画面に見えている範囲」を取得して、そこで表示すべき情報のみをマーカーにしたいというのがモチベーションです。

 

画面に表示されている範囲を取得する

それでは実装していきましょう。領域を取得するには getVisibleRegion() を呼びます。

Completer<GoogleMapController> _controller = Completer();

// ...

var googleMap = await _controller.future;
var region = await googleMap.getVisibleRegion();

ここで得られる regionLatLngBounds 型の値で、簡潔にいうと次のようなクラスです。

class LatLngBounds {
  // 南西の座標
  final LatLng southwest;

  // 北東の座標
  final LatLng northeast;
}

つまり、日本地図でいうところの「左下」「右上」の座標が得られます。
この値を使って表示すべき情報をフィルタしていきましょう。

見えている範囲だけマーカーを表示する

最初に出したダムの例で考えてみます。

まず、ダムは次のクラスで定義されているとします。

class Dam {
  String name;
  double latitude;
  double longitude;
}

全国のすべてのダムは dams という変数に入っているとします。

List<Dam> dams = [ ... ];   // 全国のダム情報が入っている

この時、画面内のダムだけを抽出するには次の実装になります。

List<CardDam> visibleDams = dams.where((Dam dam) {
  if (dam.latitude < region.southwest.latitude ||
      dam.latitude > region.northeast.latitude) {
    return false;
  } else if (dam.longitude < region.southwest.longitude ||
      dam.longitude > region.northeast.longitude) {
    return false;
  } else {
    return true;
  }
}).toList();

// いま表示すべき情報だけに絞られた
print(visibleDams);
}

これで「いま表示すべき情報」だけに絞られました。

画面の移動に応じて再描画する

多くの場合、マップはドラッグ可能で別のエリアに移動することができると思います。
上記で得たのはあくまで「その時点で表示すべき情報」なので、中心の座標やズーム率が変われば再計算しなければいけません。

Google Mapsでは、カメラが移動した時のコールバックも用意されています。

GoogleMap(
  // ...
  onCameraMove: () => { print("表示領域を再計算する") },
);

onCameraMove を使ってこのように記述できます。

画面の最大/最小ズーム率を指定する

デフォルトではGoogle Mapsは日本全土を表示できてしまい、そうなると結局すべてのマーカーを表示せざるを得なくなります。
(他のSDKではクラスタ化といってマーカーをまとめる機能がありますが、Flutterでは利用できません)

そこで、ズーム率の最大/最小倍率を指定します。

GoogleMap(
  // ....
  minMaxZoomPreference: MinMaxZoomPreference(8, 15),
);

最大でもここまでしかズームアウトできない

最大ズーム率を仕様の許す範囲で設定し、大量のマーカーが同時に描画されないようにしましょう。

まとめ

大量のデータをマップにマーカー表示する方法について紹介しました。 UIの描画はコストも高いので、良いユーザー体験のためにもパフォーマンスは大事にしたいですね。

FlutterでGoogle Mapsのマーカー(ピン)をタップして吹き出しを表示する

f:id:himaratsu:20210301235040p:plain

前回はGoogle MapsをFlutterに表示しました。

flutterdev.hatenablog.com     

マーカーをタップして吹き出しを表示する

  Google Mapsを扱うパッケージ google_maps_flutter では、マーカーは Marker クラスを使って表現します。

マーカーをタップして吹き出しを表示するには、以下のようなマーカーを定義します。

Marker tokyoTower = Marker(
  markerId: MarkerId('tokyo_tower'),
  position: LatLng(
    35.658581,
    139.745433,
  ),
);

これをGoogleMapsのWidgetに渡すとマーカーが表示されます。

GoogleMap(
        initialCameraPosition: _initialLocation,
        mapType: MapType.normal,
        onMapCreated: (GoogleMapController controller) {
          mapController = controller;
        },
        markers: [tokyoTower].toSet(),   // マーカーをセット
      ),

f:id:himaratsu:20210301235040p:plain
マーカーをタップして吹き出し(InfoWindow)を表示

他にも色々とセットすることができて、例えばアイコンの色を変えたり、吹き出しタップ時にアクションを行えます。

return new Marker(
  markerId: MarkerId(dam.id),
  position: LatLng(
    35.658581,
    139.745433,
  ),
  icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
  onTap: () => { print("マーカーがタップされた") },
  infoWindow: InfoWindow(
    title: "ここは東京タワーです",
    onTap: () => { print("InfoWindowがタップされた") },
  ),
);

別のアクションから吹き出しを表示する

例えば別のUIをタップした時など、マーカータップ以外のアクションから吹き出しを表示したい時があると思います。

その場合は showMarkerInfoWindow を使って、以下のようなコードで実現できます。

final Completer<GoogleMapController> _controller = Completer();

// ...

var googleMap = await _controller.future;

final MarkerId marker = MarkerId("tokyo_tower");
await googleMap.showMarkerInfoWindow(marker);

吹き出しを開きたい MarkerId を指定しましょう。

同じように、「吹き出しを隠す」「吹き出しが表示されているかをチェックする」こともできます。

吹き出しを隠す

googleMap.hideMarkerInfoWindow(marker);

吹き出しが表示中かをチェックする

googleMap.isMarkerInfoWindowShown(marker)    // -> Bool

まとめ

今回はGoogle Mapsのマーカー周りの処理について紹介しました。 マーカーは Google Maps Widget に Set として渡され、重複する MarkerId は一つしか表示されないので注意してください。

FlutterでChoiceChipを使って選択肢を表示する

Materialパッケージに含まれるChoiceChipでは選択肢の表示とその選択を実現できます。
実装する機会があったのでコードをメモしておきます。

ChoiceChipの実装

このような選択肢からひとつを選択できるものを作ります。

// 選択肢の準備
var _visitOptions = ["すべて", "訪問済", "未訪問"];
var _selectedVisitIndex = 0;

_visitOptions.indexedMap((index, title) {
      choices.add(Container(
        padding: const EdgeInsets.all(8.0),
        child: ChoiceChip(
          selectedColor: Colors.green[600],
          backgroundColor: Colors.grey[300],
          label: Text(title),
          selected: _selectedVisitIndex == index,
          onSelected: (_) {
            // 選択された時の処理
            print("title is " + title + ", index is " + index.toString());
          },
        ),
      ));
    });

...

// indexと項目の両方を使いたいのでExtensionを定義
extension IndexedMap<T, E> on List<T> {
  List<E> indexedMap<E>(E Function(int index, T item) function) {
    final list = <E>[];
    asMap().forEach((index, element) {
      list.add(function(index, element));
    });
    return list;
  }
}

デザインを整えるための項目も多いですが、コアとなるのは以下の部分です。

ChoiceChip(
  selectedColor: Colors.green[600],
  backgroundColor: Colors.grey[300],
  label: Text(title),
  selected: _selectedVisitIndex == index,
  onSelected: (_) {
    // 選択された時の処理
    print("title is " + title + ", index is " + index.toString());
    _selectedVisitIndex = index;
  },
)

onSelected で選択された時の処理を実装し、 selected でその項目の選択状態を返します。 そのほかにも設定できる項目が色々あってデザインのカスタマイズの幅も広いです。

また、IndexedMapの実装はこちらの記事を参考にさせていただきました。

qiita.com

まとめ

ChoiceChipの実装方法を書きました。 FlutterのMaterialのコンポーネントで選択肢を選ばせるというと SimpleDialog もありますが、今回のように選択肢の数が少ない場合は並べて表示する方が親切ではないかと思っています。

FlutterでGoogle Mapsを表示するシンプルな実装

f:id:himaratsu:20210209231100p:plain

Flutterで地図のあるアプリを作るとき、最初に候補にあがるのがGoogle Mapsだと思います。 今回はFlutterでGoogleMapsを表示してみます。

google_maps_flutterパッケージを利用して、API Keyの取得〜マップの表示の実装までをまとめます。

FlutterでGoogle Mapsを表示する流れ

パッケージの入手

pub.dev

まずはパッケージを手に入れましょう。pubspec.yamlに以下を追記します。

google_maps_flutter: 0.5.33

API Keyの取得

Google Mapsの利用にはAPI Keyが必要です。 入手する方法は上記のサイトの通りですが、

console.cloud.google.com

このサイトにアクセスして、

  • Google Mapsを使いたいプロジェクトを選択
  • ナビゲーションメニューから「Google Maps」を選択
  • 「APIs」を選択
  • Android用に有効にしたいなら、Maps SDK for Android を ENABLE にする
  • iOS用に有効にしたいなら、Maps SDK for iOS を ENABLE にする
  • Enabled APIs セクションで、有効にしたAPIを確認する

といった手順で発行できます。

iOS/Androidアプリへの設定

API Keyの設定をOSごとに行います。

iOS

ios/Runner/AppDelegate.swift を開いて、以下を追記:

// ここを追記
import GoogleMaps 

...

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // ここを追記
    GMSServices.provideAPIKey("YOUR KEY HERE")
    GeneratedPluginRegistrant.register(with: self)

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

Android

android/app/src/main/AndroidManifest.xml を開いて、以下を追記:

<manifest ...
  <application ...
    <meta-data android:name="com.google.android.geo.API_KEY"
               android:value="YOUR KEY HERE"/>

ここまでで準備は整いました。

Google Mapsを表示する

パッケージを利用したい画面で以下を宣言します。

import 'package:google_maps_flutter/google_maps_flutter.dart';

Google MapsはStatefulなWidgetで使います。

class MyMapPage extends StatefulWidget {
  @override
  State<MyMapPage> createState() => _MyMapPageState();
}

class _MyMapPageState extends State<MyMapPage> {
  ...
}

まずはcontrollerを用意します。

Completer<GoogleMapController> _controller = Completer();

最初に表示する中心地を決めて、

final CameraPosition _initPosition =
      CameraPosition(target: LatLng(36.566449, 137.6621042), zoom: 11);

Widget を buildします。

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: GoogleMap(
        initialCameraPosition: _initPosition,
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller);
        },
      ),
    );
  }

これだけで地図が表示できました。シンプルですね。

FlutterでGoogle Mapsを表示

マーカーを表示する

マーカーも表示してみましょう。 GoogleMapを作る際に markers パラメータに値を渡すことで表示します。

まず、マーカーを作る処理はこのようになります。

// マーカーを追加
var marker = new Marker(
    markerId: MarkerId("marker-1"), position: _initPosition.target);
Set<Marker> markers = [marker].toSet();

position には _initPosition.target を渡して、画面の中心にマーカーが立つようにしています。 また、markers は Set である必要があるので、今回は toSet() で変換しました。

これを以下のようにGoogleMapに渡します。

@override
  Widget build(BuildContext context) {
    // マーカーを追加
    var marker = new Marker(
        markerId: MarkerId("marker-1"), position: _initPosition.target);
    Set<Marker> markers = [marker].toSet();

    return new Scaffold(
      body: GoogleMap(
        initialCameraPosition: _initPosition,
        markers: markers,
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller);
        },
      ),
    );
  }

これでマーカーを表示できました。

FlutterでGoogle Mapsとマーカーを表示

まとめ

FlutterでGoogle Mapsを表示する流れを書きました。シンプルなコードでリッチなマップ機能を利用できます。 この記事がどなたかのお役に立てるとうれしいです。