🚗

【Flutter×Google Maps】Cardメニューで位置情報をGoogle Mapsに連携しナビを開く

2024/11/17に公開

概要

Flutterアプリで地図を表示し、特定の地点に応じたCardメニューを表示することがあるかと思います。今回は、Cardメニュー内のボタンを押すと、その地点や現在地からのルート案内をGoogle Mapsアプリで直接開ける仕組みを実装してみようと思います。

動作環境

  • Mac Book Air M3 24GB / macOS: 14.6.1
  • iOSシュミレータ: iPhone 15 Pro / iOS: 17.4
  • Androidシュミレータ: Android API VanillalceCream arm64-v8a

セットアップ

fvmを使ってプロジェクト作成していきます。

mkdir google_map_flutter_sample
cd google_map_flutter_sample
fvm use 3.24.3 --force
fvm flutter create .

次に必要なパッケージを追加します。 pubspec.yaml に以下を追加します。

dependencies:
  google_maps_flutter: ^2.9.0
  url_launcher: ^6.3.1

Google Maps APIのキーの取得や設定を行います。↓の記事参照

https://zenn.dev/slowhand/articles/f4e4e092f9b72b

次に単にMapを表示するだけの画面を実装し表示できればOKです。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class CardNaviMap extends StatefulWidget {
  const CardNaviMap({super.key});

  
  State<CardNaviMap> createState() => _CardNaviMapState();
}

class _CardNaviMapState extends State<CardNaviMap> {
  final Completer<GoogleMapController> _controller =
      Completer<GoogleMapController>();

  static const CameraPosition _initialCameraPosition = CameraPosition(
    target: LatLng(35.68123428932672, 139.76714355230686),
    zoom: 12.0,
  );

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Card Navigation Map')),
      body: GoogleMap(
        mapType: MapType.normal,
        initialCameraPosition: _initialCameraPosition,
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller);
        },
      ),
    );
  }
}

サンプルデータ表示

次に実際のアプリで使われるようなEntityクラスを作成します。今回は以下のようなクラスにしました。

class Point {
  int id;
  double latitude;
  double longitude;
  String name;

  Point(this.id, this.latitude, this.longitude, this.name);
}

このクラスを使ったサンプルデータを定義します。

final List<Point> samplePoints = [
  Point(1, 35.6586, 139.7454, '東京タワー'),
  Point(2, 35.7100, 139.8107, '東京スカイツリー'),
  Point(3, 35.6764, 139.6993, '新宿御苑'),
  Point(4, 35.6605, 139.7297, '六本木ヒルズ'),
  Point(5, 35.6986, 139.7731, '秋葉原'),
  Point(6, 35.6553, 139.7630, '浜離宮恩賜庭園'),
  Point(7, 35.6595, 139.7004, '渋谷スクランブル交差点'),
  Point(8, 35.7170, 139.7745, '上野動物園'),
  Point(9, 35.6718, 139.6946, '代々木公園'),
  Point(10, 35.6852, 139.7528, '皇居'),
];

これをMarkerとしてMapに表示させます。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Card Navigation Map')),
      body: GoogleMap(
        mapType: MapType.normal,
        initialCameraPosition: _initialCameraPosition,
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller);
        },
        markers: samplePoints // ←追加
            .map((point) => Marker(
                  markerId: MarkerId(point.id.toString()),
                  position: LatLng(point.latitude, point.longitude),
                ))
            .toSet(),
      ),
    );
  }

↓の様にMakerが表示されていればOKです。

image1.png

Cardメニュー表示

続いて現在選択されているMakerのCardメニューを表示させるサンプルを作成したいと思います。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class Point {
  int id;
  double latitude;
  double longitude;
  String name;

  Point(this.id, this.latitude, this.longitude, this.name);
}

final List<Point> samplePoints = [
  Point(1, 35.6586, 139.7454, '東京タワー'),
  Point(2, 35.7100, 139.8107, '東京スカイツリー'),
  Point(3, 35.6764, 139.6993, '新宿御苑'),
  Point(4, 35.6605, 139.7297, '六本木ヒルズ'),
  Point(5, 35.6986, 139.7731, '秋葉原'),
  Point(6, 35.6553, 139.7630, '浜離宮恩賜庭園'),
  Point(7, 35.6595, 139.7004, '渋谷スクランブル交差点'),
  Point(8, 35.7170, 139.7745, '上野動物園'),
  Point(9, 35.6718, 139.6946, '代々木公園'),
  Point(10, 35.6852, 139.7528, '皇居'),
];

class CardNaviMap extends StatefulWidget {
  const CardNaviMap({super.key});

  
  State<CardNaviMap> createState() => _CardNaviMapState();
}

class _CardNaviMapState extends State<CardNaviMap> {
  final Completer<GoogleMapController> _controller =
      Completer<GoogleMapController>();

  static const CameraPosition _initialCameraPosition = CameraPosition(
    target: LatLng(35.68123428932672, 139.76714355230686),
    zoom: 12.0,
  );

  final _pageController = PageController(
    viewportFraction: 0.85,
  );

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Card Navigation Map')),
      body: Stack(
        alignment: Alignment.bottomCenter,
        children: [
          _map(),
          _cards(),
        ],
      ),
    );
  }

  Widget _map() {
    return GoogleMap(
      mapType: MapType.normal,
      initialCameraPosition: _initialCameraPosition,
      onMapCreated: (GoogleMapController controller) {
        _controller.complete(controller);
      },
      markers: samplePoints
          .map((point) => Marker(
                markerId: MarkerId(point.id.toString()),
                position: LatLng(point.latitude, point.longitude),
                onTap: () async {
                  final index =
                      samplePoints.indexWhere((p) => p.id == point.id);
                  _pageController.jumpToPage(index);
                },
              ))
          .toSet(),
    );
  }

  Widget _cards() {
    return Container(
      height: 148,
      padding: const EdgeInsets.fromLTRB(0, 0, 0, 20),
      child: PageView(
        onPageChanged: (int index) async {
          final selectedPoint = samplePoints.elementAt(index);
          final GoogleMapController controller = await _controller.future;
          final zoomLevel = await controller.getZoomLevel();
          controller.animateCamera(
            CameraUpdate.newCameraPosition(
              CameraPosition(
                target: LatLng(selectedPoint.latitude, selectedPoint.longitude),
                zoom: zoomLevel,
              ),
            ),
          );
        },
        controller: _pageController,
        children: _tiles(),
      ),
    );
  }

  List<Widget> _tiles() {
    return samplePoints.map(
      (point) {
        return Card(
          child: SizedBox(
            height: 100,
            child: Center(
              child: Text(point.name),
            ),
          ),
        );
      },
    ).toList();
  }
}

こちらを実行すると以下の様な挙動になります。

image2.gif

Google Mapアプリを開く

今度はCardメニュー内のボタンを押すと、その地点をGoogle Mapアプリで開ける様にしたいと思います。

_launch を追加し _tiles を以下に修正します。

  List<Widget> _tiles() {
    return samplePoints.map(
      (point) {
        return Card(
          child: SizedBox(
            height: 100,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(point.name),
                const SizedBox(height: 10),
                ElevatedButton(
                  onPressed: () async {
                    final uri = Uri.parse(
                        "https://www.google.com/maps/search/?api=1&query=${point.latitude},${point.longitude}");
                    await _launch(context, uri);
                  },
                  child: const Text('GoogleMapで表示'),
                ),
              ],
            ),
          ),
        );
      },
    ).toList();
  }

  Future<void> _launch(BuildContext context, Uri uri) async {
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    }
  }
  • iOSのシュミレータの場合
    image3.gif

  • Androidシュミレータの場合 (Google Mapインストール)

    image4.gif

※ Google Maps URLs を開く場合ケース毎に挙動が異なります。詳細はこちら👇

https://zenn.dev/slowhand/articles/5234dc81379f99

現在地から対象の地点までのナビをGoogle Mapアプリで開く

今度はCardメニュー内のボタンを押すと、現在地からその地点までのナビがGoogle Mapアプリで開くようにしてみたいと思います。

_tiles を以下に修正します。

  List<Widget> _tiles() {
    return samplePoints.map(
      (point) {
        return Card(
          child: SizedBox(
            height: 100,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Text(point.name),
                const SizedBox(height: 10),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton(
                      onPressed: () async {
                        final uri = Uri.parse(
                            "https://www.google.com/maps/search/?api=1&query=${point.latitude},${point.longitude}");
                        await _launch(context, uri);
                      },
                      child: const Text('GoogleMapで表示'),
                    ),
                    const SizedBox(width: 10),
                    ElevatedButton(
                      onPressed: () async {
                        final uri = Uri.parse(
                            "https://www.google.com/maps/dir/?api=1&destination=${point.latitude},${point.longitude}");
                        await _launch(context, uri);
                      },
                      child: const Text('ナビ表示'),
                    ),
                  ],
                )
              ],
            ),
          ),
        );
      },
    ).toList();
  }

※ 現在地は東京駅に設定しています。

  • iOSシュミレータの場合
    image5.gif

  • Androidシュミレータの場合 (Google Mapインストール)
    image6.gif

参考URL

https://zenn.dev/flutteruniv_dev/articles/bc50ca942eb450

Discussion