Help us understand the problem. What is going on with this article?

FlutterでState管理されている緯度経度に向かってGoogleMapController#animateCamera する

More than 1 year has passed since last update.

まえおき

GoogleMapをFlutterで使うときは、 https://pub.dev/packages/google_maps_flutter を使うことになると思う。

一つ前の記事 で、

image

こんなUIを作っている話をしていたが、これをFlutterに移植しようとしたときにハマったのでメモ。

やりたかったこと

ページをスワイプして変更したときには、地図側で

  • 選択されたお店の緯度経度に向かってanimateCameraする
  • 選択されたお店のマーカーアイコンを切り替える

ということをやりたい。普通に考えれば、「選択されたお店」をStateにもたせてしまって、それをGoogleMapとPageViewに反映させる作りがよさそう。

Slice.png

でもぐぐって出てくるのは・・・

サンプルコードでも、ググって出てくる大抵の例も、

Slice.png

こんな感じで、イベントハンドラから直接 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のライブラリのソースを見れば一目瞭然で、

google_map.dart
  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を見ていないのだ。

ちなみに蛇足だが、マーカーとか図形とかを描画してるのは、以下のように更新されるらしい。

google_map.dart
  @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() で愚直にそういう処理を書く必要がある。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away