LoginSignup
9

More than 3 years have passed since last update.

posted at

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

まえおき

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() で愚直にそういう処理を書く必要がある。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
9