Flutter + Mapbox で Symbol をタップして id を取得・表示する
以前書いた、
の続きです。
前回、画面にピン(マーク)を立てる機能を追加しましたが、このピン(Symbol
として表示)をタップしたときに(Symbol
のid
を取得し、AlertDialog
で表示してみます。
※Symbol
のid
を表示するだけでは特に何の意味もありませんが、このid
をMap
やデータベースに入れた情報と突き合わせて表示する場合などに使えます。ただし、アプリケーションを終了するなどして一旦地図上から消えたピン(Symbol
)をデータベースから読み込んだ(緯度・経度・ラベルなどの)情報を使って再配置するケースでは再配置の都度id
が変わってしまうため、データベースにid
を保存するのではなく、表示中のピン(Symbol
)のid
をデータベース上の主キー列に変換するMap
が必要になりそうです(後日トライして記事化する予定)。
ポイント
-
MapboxMapController.onSymbolTapped
のadd
でピン(Symbol
)をタップしたときに呼び出す処理を一度だけセットする
です。
加えて、
- 親ウィジェットから
BuildContext
を引き継げない状況でAlertDialog
を表示する
方法についても考慮が必要になります。
Symbol
)をタップしたときに呼び出す処理をセットする
ピン((Symbol
)をタップしたときに呼び出す処理は、MapboxMapController.onSymbolTapped
のadd
でセットしますが、この処理のセットは一度だけ行います。
最初、ピンを立てるごとに毎回セットするコードを書いたところ、うまく動いた…と思ったらピンをタップしたときに同じアラートダイアログがピンの数だけ重なって表示されてしまいました。
そのため、最初にピンを立てたときだけセットするように修正しました。
【↓ここから不要↓】
// onSymbolTapped 設定済み?
bool _symbolSet = false;
// マーク(ピン)を立てて時刻(hh:mm)のラベルを付ける
void _addMark(LatLng tapPoint) {
// 時刻を取得
DateTime _now = DateTime.now();
String _hhmm = _fillZero(_now.hour) + ':' + _fillZero(_now.minute);
// マーク(ピン)を立てる
_controller.future.then((mapboxMap) {
Future<Symbol> _symbol = mapboxMap.addSymbol(SymbolOptions(
geometry: tapPoint,
textField: _hhmm,
textAnchor: "top",
textColor: "#000",
textHaloColor: "#FFF",
textHaloWidth: 3,
iconImage: "mapbox-marker-icon-blue",
iconSize: 1,
));
if (!_symbolSet) {
_symbol.then((symbol) {
mapboxMap.onSymbolTapped.add(_onSymbolTap);
setState(() {
_symbolSet = true;
});
});
}
});
}
【↑ここまで不要↑】
mapboxMap.onSymbolTapped.add(_onSymbolTap);
が処理をセットしている部分です。
こちらが呼び出される側です。
// マークをタップしたときに Symbol の情報を表示する
void _onSymbolTap(Symbol symbol) {
_dispSymbolInfo(symbol);
}
// Symbol の情報を表示する
void _dispSymbolInfo(Symbol symbol) {
showDialog(
context: navigatorKey.currentContext!,
builder: (BuildContext context) => AlertDialog(
title: Text('Symbol ID : ${symbol.id}'),
actions: [
IconButton(
icon: const Icon(Icons.close),
color: Colors.blue,
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
}
AlertDialog
を表示する
この形でアラートダイアログを表示しようとすると、親ウィジェットからBuildContext
を引き継げません。
この場合は、以下のように準備したNavigatorState
の、context
プロパティからBuildContext
を取得します。
final navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Mapbox',
home: const MapPage(),
navigatorKey: navigatorKey,
);
}
}
※アラートダイアログを表示する側では、shawDialog
でcontext: navigatorKey.currentContext!
を指定(前掲のコードのとおり)。
画面例
ソースコード
今回 2 つに分割しました。
main.dart
import 'package:flutter/material.dart';
import 'map_page.dart';
final navigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Mapbox',
home: const MapPage(),
navigatorKey: navigatorKey,
);
}
}
map_page.dart
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:location/location.dart';
import 'package:mapbox_gl/mapbox_gl.dart';
import 'package:maptool/main.dart';
class MapPage extends StatefulWidget {
const MapPage({Key? key}) : super(key: key);
_MapPageState createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
final Completer<MapboxMapController> _controller = Completer();
final Location _locationService = Location();
// 地図スタイル用 Mapbox URL
final String _style = '【地図スタイルのURL】';
// Location で緯度経度が取れなかったときのデフォルト値
final double _initialLat = 35.6895014;
final double _initialLong = 139.6917337;
// ズームのデフォルト値
final double _initialZoom = 13.5;
// 方位のデフォルト値(北)
final double _initialBearing = 0.0;
// 現在位置
LocationData? _yourLocation;
// GPS 追従?
bool _gpsTracking = false;
// 現在位置の監視状況
StreamSubscription? _locationChangedListen;
void initState() {
super.initState();
// 現在位置の取得
_getLocation();
// 現在位置の変化を監視
_locationChangedListen =
_locationService.onLocationChanged.listen((LocationData result) async {
setState(() {
_yourLocation = result;
});
});
setState(() {
_gpsTracking = true;
});
}
void dispose() {
super.dispose();
// 監視を終了
_locationChangedListen?.cancel();
}
Widget build(BuildContext context) {
return Scaffold(
body: _makeMapboxMap(),
floatingActionButton: _makeFloatingIcons(),
);
}
// 地図ウィジェット
Widget _makeMapboxMap() {
if (_yourLocation == null) {
// 現在位置が取れるまではロード中画面を表示
return const Center(
child: CircularProgressIndicator(),
);
}
// GPS 追従が ON かつ地図がロードされている→地図の中心を移動
_moveCameraToGpsPoint();
// Mapbox ウィジェットを返す
return MapboxMap(
// 地図(スタイル)を指定
styleString: _style,
// 初期表示される位置情報を現在位置から設定
initialCameraPosition: CameraPosition(
target: LatLng(_yourLocation!.latitude ?? _initialLat,
_yourLocation!.longitude ?? _initialLong),
zoom: _initialZoom,
),
onMapCreated: (MapboxMapController controller) {
_controller.complete(controller);
_controller.future.then((mapboxMap) {
mapboxMap.onSymbolTapped.add(_onSymbolTap);
});
},
compassEnabled: true,
// 現在位置を表示する
myLocationEnabled: true,
// 地図をタップしたとき
onMapClick: (Point<double> point, LatLng tapPoint) {
_onTap(point, tapPoint);
},
// 地図を長押ししたとき
onMapLongClick: (Point<double> point, LatLng tapPoint) {
_addMark(tapPoint);
},
);
}
// フローティングアイコンウィジェット
Widget _makeFloatingIcons() {
return Column(mainAxisSize: MainAxisSize.min, children: [
FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {
// ズームを戻す
_resetZoom();
},
child: const Text('±', style: TextStyle(fontSize: 28.0, height: 1.0)),
),
const Gap(16),
FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {
// 北向きに戻す
_resetBearing();
},
child: const Text('N',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
),
const Gap(16),
FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {
_gpsToggle();
},
child: Icon(
// GPS 追従の ON / OFF に合わせてアイコン表示する
_gpsTracking ? Icons.gps_fixed : Icons.gps_not_fixed,
),
),
]);
}
// 現在位置を取得
void _getLocation() async {
_yourLocation = await _locationService.getLocation();
}
// GPS 追従を ON / OFF
void _gpsToggle() {
setState(() {
_gpsTracking = !_gpsTracking;
});
// ここは本来 iOS では不要
_moveCameraToGpsPoint();
}
// GPS 追従が ON なら地図の中心を現在位置へ
void _moveCameraToGpsPoint() {
if (_gpsTracking) {
_controller.future.then((mapboxMap) {
if (Platform.isAndroid) {
mapboxMap.moveCamera(CameraUpdate.newLatLng(LatLng(
_yourLocation!.latitude ?? _initialLat,
_yourLocation!.longitude ?? _initialLong)));
} else if (Platform.isIOS) {
mapboxMap.animateCamera(CameraUpdate.newLatLng(LatLng(
_yourLocation!.latitude ?? _initialLat,
_yourLocation!.longitude ?? _initialLong)));
}
});
}
}
// 地図をタップしたときの処理
void _onTap(Point<double> point, LatLng tapPoint) {
_moveCameraToTapPoint(tapPoint);
setState(() {
_gpsTracking = false;
});
}
// 地図の中心をタップした場所へ
void _moveCameraToTapPoint(LatLng tapPoint) {
_controller.future.then((mapboxMap) {
if (Platform.isAndroid) {
mapboxMap.moveCamera(CameraUpdate.newLatLng(tapPoint));
} else if (Platform.isIOS) {
mapboxMap.animateCamera(CameraUpdate.newLatLng(tapPoint));
}
});
}
// マーク(ピン)を立てて時刻(hh:mm)のラベルを付ける
void _addMark(LatLng tapPoint) {
// 時刻を取得
DateTime _now = DateTime.now();
String _hhmm = _fillZero(_now.hour) + ':' + _fillZero(_now.minute);
// マーク(ピン)を立てる
_controller.future.then((mapboxMap) {
Future<Symbol> _symbol = mapboxMap.addSymbol(SymbolOptions(
geometry: tapPoint,
textField: _hhmm,
textAnchor: "top",
textColor: "#000",
textHaloColor: "#FFF",
textHaloWidth: 3,
iconImage: "mapbox-marker-icon-blue",
iconSize: 1,
));
});
}
// マークをタップしたときに Symbol の情報を表示する
void _onSymbolTap(Symbol symbol) {
_dispSymbolInfo(symbol);
}
// Symbol の情報を表示する
void _dispSymbolInfo(Symbol symbol) {
showDialog(
context: navigatorKey.currentContext!,
builder: (BuildContext context) => AlertDialog(
title: Text('Symbol ID : ${symbol.id}'),
actions: [
IconButton(
icon: const Icon(Icons.close),
color: Colors.blue,
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
}
// 2 桁 0 埋め(Intl が正しく動かなかったため仕方なく)
String _fillZero(int number) {
String _tmpNumber = ('0' + number.toString());
return _tmpNumber.substring(_tmpNumber.length - 2);
}
// 地図の上を北に
void _resetBearing() {
_controller.future.then((mapboxMap) {
mapboxMap.animateCamera(CameraUpdate.bearingTo(_initialBearing));
});
}
// 地図のズームを初期状態に
void _resetZoom() {
_controller.future.then((mapboxMap) {
mapboxMap.moveCamera(CameraUpdate.zoomTo(_initialZoom));
});
}
}
Discussion