まえおき
GoogleMapをFlutterで使うときは、 https://pub.dev/packages/google_maps_flutter を使うことになると思う。
一つ前の記事 で、
こんなUIを作っている話をしていたが、これをFlutterに移植しようとしたときにハマったのでメモ。
やりたかったこと
ページをスワイプして変更したときには、地図側で
- 選択されたお店の緯度経度に向かってanimateCameraする
- 選択されたお店のマーカーアイコンを切り替える
ということをやりたい。普通に考えれば、「選択されたお店」をStateにもたせてしまって、それをGoogleMapとPageViewに反映させる作りがよさそう。
でもぐぐって出てくるのは・・・
サンプルコードでも、ググって出てくる大抵の例も、
こんな感じで、イベントハンドラから直接 googleMapController.animateCamera している(状態は書き換えていない)のが多い。
状態が変化して、ウィジェットを再レイアウトする際にanimateCameraしている例が全然見つからなかった。
うまくいったやり方
結論から書こう。2019/10月現在は以下のやりかたでうまく行った。
class PlaceListPageState extends State {
static CameraPosition _initCameraPosition = ....
Completer<GoogleMapController> _googleMapController = Completer();
List<Place> places = List();
Place selectedPlace = null; // 地図で中心表示にしたい&PageViewで選択したい場所
@override
Widget build(BuildContext context) {
// マップがロード済みであれば、
_googleMapController.future.then((googleMap) {
// 中心に表示したい場所があれば、そこに向かってanimateCameraする。
if (selectedPlace != null) {
googleMap.animateCamera(CameraUpdate.newLatLng(selectedPlace.location));
}
});
return Column(
children: <Widget>[
(中略)
child: GoogleMap(
initialCameraPosition: _initCameraPosition,
markers: places.map((place) {
return Marker(
markerId: MarkerId(place.uuid),
position: place.location,
icon: place.uuid == selectedPlace?.uuid ? 選択時のアイコン : デフォルト時のアイコン,
onTap: () {
// マーカーをタップしたら中心の場所を更新する
setState(() { selectedPlace = place; });
});
}).toSet(),
onMapCreated: (googleMap) {
_googleMapController.complete(googleMap);
},
),
),
(中略)
child: PageView(
children: ....,
onPageChanged: (page) {
// PaveViewのページ変更契機でも中心の場所を更新する
setState(() { selectedPlace = places.elementAt(page); })
},
),
build()
で愚直に、Completerにつつんだ GoogleMapController の animateCamera を呼ぶだけ、であった。
うまくいかなかった方法
initialCameraPositionをselectedPlaceの有無によって変える
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
(中略)
child: GoogleMap(
initialCameraPosition: selectedPlace?.locationAsCameraPosition() ?? _initCameraPosition,
markers: places.map((place) {
こういうやり方。
Stack Overflowで、 これ とか これ とか見ると、initialCameraPositionをリアクティブにマップで表示してくれそうに見えるが、実際にはそんなことはない。
理由はGoogleMapのライブラリのソースを見れば一目瞭然で、
Future<void> onPlatformViewCreated(int id) async {
final GoogleMapController controller = await GoogleMapController.init(
id,
widget.initialCameraPosition,
this,
);
_controller.complete(controller);
if (widget.onMapCreated != null) {
widget.onMapCreated(controller);
}
}
GoogleMapできあがったよイベント(ネイティブからのコールバック)で最初の1回だけしかinitialCameraPositionは評価されない。State更新時にはinitialCameraPositionを見ていないのだ。
ちなみに蛇足だが、マーカーとか図形とかを描画してるのは、以下のように更新されるらしい。
@override
void didUpdateWidget(GoogleMap oldWidget) {
super.didUpdateWidget(oldWidget);
_updateOptions();
_updateMarkers();
_updatePolygons();
_updatePolylines();
_updateCircles();
}
void _updateOptions() async {
final _GoogleMapOptions newOptions = _GoogleMapOptions.fromWidget(widget);
final Map<String, dynamic> updates =
_googleMapOptions.updatesMap(newOptions);
if (updates.isEmpty) {
return;
}
final GoogleMapController controller = await _controller.future;
controller._updateMapOptions(updates);
_googleMapOptions = newOptions;
}
void _updateMarkers() async {
final GoogleMapController controller = await _controller.future;
controller._updateMarkers(
_MarkerUpdates.from(_markers.values.toSet(), widget.markers));
_markers = _keyByMarkerId(widget.markers);
}
void _updatePolygons() async {
final GoogleMapController controller = await _controller.future;
controller._updatePolygons(
_PolygonUpdates.from(_polygons.values.toSet(), widget.polygons));
_polygons = _keyByPolygonId(widget.polygons);
}
void _updatePolylines() async {
final GoogleMapController controller = await _controller.future;
controller._updatePolylines(
_PolylineUpdates.from(_polylines.values.toSet(), widget.polylines));
_polylines = _keyByPolylineId(widget.polylines);
}
void _updateCircles() async {
final GoogleMapController controller = await _controller.future;
controller._updateCircles(
_CircleUpdates.from(_circles.values.toSet(), widget.circles));
_circles = _keyByCircleId(widget.circles);
}
まとめ
- GoogleMapの onMapCreated はネイティブのGoogleMapが作られたときに1回だけ呼ばれる。
- GoogleMapのinitialCameraPositionはonMapCreatedの直前で1回だけ評価される。
- よってState管理している緯度経度にanimateCameraしたければ、
build()
で愚直にそういう処理を書く必要がある。