Google Flutter コラム- 第4回 Mapbox地図とUIの重ね合わせ(2/2)

Google Flutterのコラム第3回・4回では、

mapboxの地図にFlutterUIを重ね合わせてカーナビのような画面を作成していきます。

Flutterでmapboxを使用するために、有志が開発を行っている「mapbox_gl」パッケージを使用します。

Android,iOS,Webに対応しており、ターゲットのプラットフォームを意識せずに地図表示を簡単に行うことができます。

mapbox_glの導入方法などについては、

前回のコラム「GoogleMapとmapboxの地図表示を比較してみた」の
[mapboxを使用したアプリケーション開発]の章で解説しています。是非参考にしてみてください。

mapbox_glとFlutter UI

mapbox_glとFlutter UIを組み合わせて、カーナビのような画面を作成しました。
今回はカーナビで普段使うような機能を作成してみました。

現在地アイコンユーザーの現在地を示します
検索ボタン検索画面へ遷移します
時計現在時刻を表示します
ノースアップ・ヘディングアップ地図の表示方向を変更します
ノースアップは常に北向き、
ヘディングアップは現在地アイコンの正面が上向きになります
現在地ボタン現在地アイコンの位置へ画面を戻します
拡大・縮小ボタン地図の拡大率を変更します
メニューボタンドロワーメニューを表示します
地図mapbox_glを使って画面に描画した地図

以降では、それぞれの機能について、
Widget(UI)とmapbox_glの組み合わせについて紹介していこうと思います。

ボタン

01. ボタンの表示

Google Flutterは様々なButtonクラスを提供しています。
Buttonクラスはボタン押下を検知するonPressedプロパティと、長押しを検知するonLongPressプロパティを持っています。
どちらもイベントを検知するとプロパティに設定したコールバックが実行されます。

【ボタン サンプル】 ※Runボタンを押すことでサンプルを実行できます

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: <Widget>[
              TextButton(
                child: const Text('TextButton'),
                style: TextButton.styleFrom(
                  primary: Colors.black,
                ),
                onPressed: () => print("onPressed TextButton"),
                onLongPress: () => print("onLongPress TextButton"),
              ),
              ElevatedButton(
                child: const Text('ElevatedButton'),
                style: ElevatedButton.styleFrom(
                  primary: Colors.blue,
                  onPrimary: Colors.white,
                ),
                onPressed: () => print("onPressed ElevatedButton"),
                onLongPress: () => print("onLongPress ElevatedButton"),
              ),
              OutlinedButton(
                child: const Text('OutlinedButton'),
                style: OutlinedButton.styleFrom(
                  primary: Colors.black,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                  side: const BorderSide(),
                ),
                onPressed: () => print("onPressed OutlinedButton"),
                onLongPress: () => print("onLongPress OutlinedButton"),
              ),
            ],
          ),
        ),
      ),
    ),
  );
}
          

02. 拡大・縮小ボタン

画面に作成した拡大・縮小ボタンのonPressedプロパティに、
_zoomIn()/_zoomOut()を設定して、地図拡縮機能を作りました。

それぞれのボタンが押されると、
onPressedからMapboxMapController※1のmoveCamera()を使って地図のカメラ位置を操作しています。

この時、拡縮倍率(ズームレベル)を設定するために、CameraUpdateのzoomIn()/zoomOut()を使用しています。
  // mapboxコントローラ
  MapboxMapController _mapController;
 
  // 地図拡大
  void _zoomIn() {
    setState(() {
      _mapController.moveCamera(CameraUpdate.zoomIn());
    });
  }
 
  // 地図縮小
  void _zoomOut() {
    setState(() {
      _mapController.moveCamera(CameraUpdate.zoomOut());
    });
  }
                
このように、拡大・縮小ボタンの押下に合わせて、地図の拡縮を変更することができました。

03. 現在地ボタン

ボタンを押すと、カメラを現在地アイコンの位置へ戻すことができます。
mapbox_glのカメラの更新/移動機能を使って作成しました。

  // mapboxコントローラ
  MapboxMapController _mapController;
 
  // 現在地へカメラを戻す
  void _returnToCurrentPosition() {
    // 現在地取得
    var currentLatLng = _locationManager.getLastCurrentLatLng();
 
    setState(() {
      _mapController.moveCamera(CameraUpdate.newLatLng(currentLatLng));
    });
  }
                

04. ノースアップ・ヘディングアップ

ボタン押すことで、
画面のノースアップとヘディングアップを切り替えることができます。
mapbox_glのカメラ/シンボル更新機能を使って作成しています。

カメラの方向は、北を基準の0°として、360°を1/4分割した各方位の角度を目安に設定を行います。
(北:0.0°、東:90.0°、南:180.0°、西:270.0°)

カメラと現在地アイコンの方向※2を ノースアップ・ヘディングアップそれぞれの設定にすることで画面の見た目を切り替えています。

・カメラ方位:北向き(0.0°)

・現在地アイコンの角度:ユーザーの向き

・カメラ方位:ユーザーの向き

・現在地アイコンの角度:北向き(0.0°)

  // mapboxコントローラ
  MapboxMapController _mapController;
 
  // 現在地アイコン(シンボル)
  Symbol _vehiclePositionSymbol;
 
  // ユーザー地図表示変更
  void _onChangeMapLookUp() {
    setState(() {
      // 表示アイコンの更新
      (Icons.explore == _lookUpIcon.icon)
          ? _lookUpIcon = Icon(
              Icons.explore_off,
              color: Colors.black,
              size: 30.0,
            )
          : _lookUpIcon = Icon(
              Icons.explore,
              color: Colors.black,
              size: 30.0,
            );
 
      // 地図方位(初期値:北)
      var mapRotateDirection = 0.0;
 
      // シンボル角度(初期値:ユーザーの方向)
      var symbolRotateDirection = _locationManager.getLastCurrentDirection();
 
      // ヘディングアップの場合
      if (Icons.explore_off == _lookUpIcon.icon) {
        mapRotateDirection = _locationManager.getLastCurrentDirection();
        symbolRotateDirection = 0.0;
      }
 
      // カメラの方位を変更する
      _mapController.moveCamera(CameraUpdate.bearingTo(mapRotateDirection));
 
      // シンボルの方向を更新する
      _mapController.updateSymbol(
        _vehiclePositionSymbol,
        SymbolOptions(
          // シンボル角度更新
          iconRotate: symbolRotateDirection,
        ),
      );
    });
  }
                

アイコン

01. アイコンの表示

Google Flutterは様々なアイコンを提供しています。
アイコンを表示するにはIconクラスを使用します。
Iconクラスは任意のIconDataを設定することで、ざまざまなアイコン画像を表示することができます。
IconDataの詳細はIconクラス API仕様を参照してください。

【アイコン サンプル】 ※Runボタンを押すことでサンプルを実行できます

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: const <Widget>[
              Icon(
                Icons.access_alarm,
                color: Colors.pink,
                size: 40.0,
              ),
              Icon(
                Icons.add_call,
                color: Colors.green,
                size: 40.0,
              ),
              Icon(
                Icons.garage_outlined,
                color: Colors.blue,
                size: 40.0,
              ),
            ],
          ),
        ),
      ),
    ),
  );
}
            

しかし、Flutter標準のアイコンでは、

地図の特定地点を指定したアイコンを表示することが難しいです。

そのため、今回はmapbox_glの機能を使って現在地アイコンを作成しています。

02. 現在地アイコンの表示

現在地アイコンは、地図上のユーザー位置にアイコンを表示します。
地図の特定地点にアイコンを表示するためには、mapbox_glのシンボル追加機能を使います。

今回の現在地アイコンは画像データを使用しており、
最初にMapboxMapControllerのaddImage()を使って画像データの読み込みを行います。

読み込んだ画像データとユーザーの位置情報をSymbolOptionsクラスに設定して、
addSymbol()することで、地図上の現在地点にアイコンを表示することができます。

  // mapboxコントローラ
  MapboxMapController _mapController;
  
  // 現在地アイコン(シンボル)
  Symbol _vehiclePositionSymbol;
 
  // mapbox地図生成後コールバック
  void _onMapCreated(MapboxMapController controller) async {
    _mapController = controller;
  
    // 現在地変化コールバック登録
    _locationManager.addOnUserLocationChanged(_onUserLocationChanged);
  
    // 現在地取得
    var currentLatLng = _locationManager.getLastCurrentLatLng();
  
    // 現在地アイコンの画像を読み込む
    final ByteData bytes = await rootBundle.load('images/UserIcon.png');
    final Uint8List list = bytes.buffer.asUint8List();
    await _mapController.addImage("UserIcon", list);
  
    // 現在地アイコン(シンボル)の描画
    // 生成したシンボルは地図画面クラスで保持します
    _vehiclePositionSymbol = await _mapController.addSymbol(
      SymbolOptions(
        iconSize: 0.3,
        iconImage: "UserIcon",
        iconRotate: _locationManager.getLastCurrentDirection(),
        geometry: currentLatLng,
        draggable: false,
      ),
    );
  }
                
このように、ユーザーの現在地にアイコンを表示することができました。

次は、ユーザーの移動に合わせて、

現在地アイコンの位置情報を更新しようと思います。

そのために、MapboxMapControllerのupdateSymbol()を使用します。

現在地変化のコールバックを受けて、生成時に保持したシンボルの位置と方向を更新しています。

  // 現在地変化コールバック
  void _onUserLocationChanged() async {
    // 位置取得
    var currentLatLng = _locationManager.getLastCurrentLatLng();
 
    // 回転方向
    var rotateDirection = _locationManager.getLastCurrentDirection();
 
    // 現在地アイコン(シンボル)の更新
    _mapController.updateSymbol(
      _vehiclePositionSymbol,
      SymbolOptions(
        iconRotate: rotateDirection,
        geometry: currentLatLng,
      ),
    );
  }
                

このように、移動に合わせて現在地アイコンを追従させることができました。

位置情報を取得する方法は様々ありますが、
今回は、「location」パッケージを使用して、位置情報を取得しています。

ドロワーメニュー

01. ドロワーメニューの表示

地図画面のメニューを今回は、ドロワーメニューを使って作成することにしました。

Google Flutterは標準でドロワーメニューを提供しています。

ドロワーメニューは、土台となるScaffoldクラスと一緒に説明します。

Scaffoldクラスはアプリ画面のベースとなるクラスです。

各種プロパティに画面部品のWidgetを設定することで、
アプリバーやドロワーメニューを持つ標準的なアプリを作ることができます。

ドロワーメニューを表示するには、

ScaffoldクラスのdrawerプロパティにDrawerクラスを設定します。

設定するDrawerクラスのchildプロパティに任意のWidgetを設定することで、

オリジナルのドロワーメニューが表示可能になります。

【ドロワー サンプル】 ※Runボタンを押すことでサンプルを実行できます

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        drawer: Drawer(
          child: ListView(
            children: <Widget>[
              DrawerHeader(
                child: const Text("Drawer sample"),
                decoration: BoxDecoration(color: Colors.grey[100]),
              ),
            ],
          ),
        ),
        body: const MyAppScreen(),
      ),
    ),
  );
}

@immutable
class MyAppScreen extends StatelessWidget {
  const MyAppScreen({Key? key}) : super(key: key);
  
  @protected
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      padding: const EdgeInsets.all(20),
      child: Stack(
        children: [
          const Center(child: Text("Press the menu button")),
          Align(
            alignment: Alignment.bottomRight,
            child: ElevatedButton(
              child: const Icon(Icons.menu),
              style: ElevatedButton.styleFrom(
                elevation: 6.0,
                shape: const CircleBorder(),
                primary: Colors.grey,
                onPrimary: Colors.white,
              ),
              onPressed: () {
                Scaffold.of(context).openDrawer();
              },
            ),
          ),
        ],
      ),
    );
  }
}
            

サンプルのようにボタン押下をトリガーにドロワーメニューを表示する場合は、

ScaffoldStateクラスのopenDrawer()を使用します。

ScaffoldStateクラスの取得など少し複雑ですが、下図の流れでドロワーメニューを表示しています。
Scaffold+ScaffoldState of(BuildContext context)ScaffoldState+void openDrawer()BuildContextDrawer+void open()MyAppScreen+void onPressedCallback()#Widget build(BuildContext context)ElevatedButton+VoidCallback? onPressedユーザーdrawerbody③BuildContextから   Scaffoldの状態取得②onPressedコールバックchild④ドロワー表示要求⑤ドロワー表示要求①[≡]ボタン押下⑥ドロワーメニュー表示

02. ドロワーメニューのカスタム

ドロワーメニューをカスタマイズして、地図のスタイルを変更してみました。
まずは、MapboxMapクラスのstyleStringプロパティに設定する地図スタイルの文字列を保持します。

Drawerに作成したスイッチ(SwitchListTileクラス)のON/OFFで、
保持したスタイルの文字列が変更され、地図の見た目が変わります。

  Scaffold(
    // 右から左にスライドするendDrawerを使用
    endDrawer: SizedBox(
      width: deviceWidth / 2.5,
      child: Drawer(
        child: ListView(children: [
          DrawerHeader(
            child: Text(
              '地図画面表示設定',
              style: TextStyle(fontSize: 50),
            ),
            decoration: BoxDecoration(color: Colors.grey[100]),
          ),
          SwitchListTile(
            value: _bTrafficInfo,
            activeColor: Colors.orange,
            activeTrackColor: Colors.red,
            inactiveThumbColor: Colors.grey,
            inactiveTrackColor: Colors.grey,
            secondary: Icon(
              Icons.directions_car,
              size: 50.0,
            ),
            title: Text(
              '交通情報表示',
              style: TextStyle(fontSize: 20),
            ),
            onChanged: _onChangeTrafficInfoSwitch,
          ),
        ),
      ),
    ),
  ),
                

_onChangeTrafficInfoSwitch()はスイッチのON/OFFでコールバックされ、
今回は、渋滞フローの表示/非表示のスタイルを切り替えています。

  // 交通情報表示変更
  void _onChangeTrafficInfoSwitch(bool switchON) {
    setState(() {
      _bTrafficInfo = switchON;
      //スタイルを切り替える
      _bTrafficInfo
          ? _mapStyle = MapboxStyles.TRAFFIC_DAY      // 渋滞フローあり
          : _mapStyle = MapboxStyles.MAPBOX_STREETS;  // 渋滞フローなし
    });
  }
このように、スイッチのON/OFFに合わせて地図のスタイルを変更することができました。

注釈

※1 MapboxMapControllerは地図生成時のコールバック(MapboxMapクラスのonMapCreatedプロパティー)によって渡されます

渡されたMapboxMapControllerインスタンスを地図画面クラスで保持しています

※2 現在地アイコンはmapbox_glのシンボル(アイコン)表示機能を使用しています

シンボルはカメラ方位に合わせて、自動的に表示されている向きが変化するため、
カメラの表示角度に合わせて、シンボル角度の再設定を行っています

まとめ

mapbox_glとFlutter UIを組み合わせることで、
デザイン性の高いカーナビのような画面が作成できたと思います。

Google Flutterを使うことで、少ないコード量で使いやすいUIが利用でき、

マルチプラットフォームに即時対応できるというのがとても魅力的だと感じました。

一方でmapbox_glなど情報が少ないところも多く開発に手間取ることもありました。

情報が少ない点については、今後のコミュニティの発展に期待したいと思います。

現在はアプリを発展させて、検索や案内機能を想定した画面を作成中です。
次回のコラムでは、その作成内容についてお伝えしようと思います。

○GoogleMap
○mapbox(mapbox_gl:0.11.0)
○GoogleMap
○mapbox(mapbox_gl:0.12.0)
○GoogleMap
○mapbox(mapbox_gl:0.12.0)
○GoogleMap
○mapbox(mapbox_gl:0.12.0)
○GoogleMap
○mapbox(mapbox_gl:0.12.0)
○GoogleMap(Night Theme)
○mapbox(mapbox_gl:0.12.0、DARK)

お問い合わせ

名古屋本社

東京本社

【受付時間】9:00~18:00

pagetop

お客様のお悩み承ります。