Add GoogleMap callbacks (#507)
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java index 6fd0284..5e24cff 100644 --- a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/Convert.java
@@ -7,18 +7,13 @@ import android.graphics.Point; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; -import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.GoogleMapOptions; -import com.google.android.gms.maps.UiSettings; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; -import com.google.android.gms.maps.model.MarkerOptions; import io.flutter.view.FlutterMain; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -101,62 +96,15 @@ return ((Number) o).floatValue(); } - static GoogleMapOptions toGoogleMapOptions(Object o) { - final Map<?, ?> data = toMap(o); - final GoogleMapOptions options = new GoogleMapOptions(); - final Object cameraPosition = data.get("cameraPosition"); - if (cameraPosition != null) { - options.camera(toCameraPosition(cameraPosition)); - } - final Object cameraTargetBounds = data.get("cameraTargetBounds"); - if (cameraTargetBounds != null) { - final List<?> targetData = toList(cameraTargetBounds); - if (targetData.get(0) != null) { - options.latLngBoundsForCameraTarget(toLatLngBounds(targetData.get(0))); - } - } - final Object compassEnabled = data.get("compassEnabled"); - if (compassEnabled != null) { - options.compassEnabled(toBoolean(compassEnabled)); - } - final Object mapType = data.get("mapType"); - if (mapType != null) { - options.mapType(toInt(mapType)); - } - final Object rotateGesturesEnabled = data.get("rotateGesturesEnabled"); - if (rotateGesturesEnabled != null) { - options.rotateGesturesEnabled(toBoolean(rotateGesturesEnabled)); - } - final Object scrollGesturesEnabled = data.get("scrollGesturesEnabled"); - if (scrollGesturesEnabled != null) { - options.scrollGesturesEnabled(toBoolean(scrollGesturesEnabled)); - } - final Object tiltGesturesEnabled = data.get("tiltGesturesEnabled"); - if (tiltGesturesEnabled != null) { - options.tiltGesturesEnabled(toBoolean(tiltGesturesEnabled)); - } - final Object zoomBounds = data.get("zoomBounds"); - if (zoomBounds != null) { - final List<?> zoomData = toList(zoomBounds); - if (zoomData.get(0) != null) { - options.minZoomPreference(toFloat(zoomData.get(0))); - } - if (zoomData.get(1) != null) { - options.maxZoomPreference(toFloat(zoomData.get(1))); - } - } - final Object zoomGesturesEnabled = data.get("zoomGesturesEnabled"); - if (zoomGesturesEnabled != null) { - options.zoomGesturesEnabled(toBoolean(zoomGesturesEnabled)); - } - return options; + private static Float toFloatWrapper(Object o) { + return (o == null) ? null : toFloat(o); } static int toInt(Object o) { return ((Number) o).intValue(); } - private static Object toJson(CameraPosition position) { + static Object toJson(CameraPosition position) { final Map<String, Object> data = new HashMap<>(); data.put("bearing", position.bearing); data.put("target", toJson(position.target)); @@ -175,6 +123,9 @@ } private static LatLngBounds toLatLngBounds(Object o) { + if (o == null) { + return null; + } final List<?> data = toList(o); return new LatLngBounds(toLatLng(data.get(0)), toLatLng(data.get(1))); } @@ -191,25 +142,6 @@ return (Map<?, ?>) o; } - static MarkerOptions toMarkerOptions(Object o) { - final Map<?, ?> data = toMap(o); - final List<?> anchor = toList(data.get("anchor")); - final List<?> infoWindowAnchor = toList(data.get("infoWindowAnchor")); - return new MarkerOptions() - .position(toLatLng(data.get("position"))) - .alpha(toFloat(data.get("alpha"))) - .anchor(toFloat(anchor.get(0)), toFloat(anchor.get(1))) - .draggable(toBoolean(data.get("draggable"))) - .flat(toBoolean(data.get("flat"))) - .icon(toBitmapDescriptor(data.get("icon"))) - .infoWindowAnchor(toFloat(infoWindowAnchor.get(0)), toFloat(infoWindowAnchor.get(1))) - .rotation(toFloat(data.get("rotation"))) - .snippet(toString(data.get("snippet"))) - .title(toString(data.get("title"))) - .visible(toBoolean(data.get("visible"))) - .zIndex(toFloat(data.get("zIndex"))); - } - private static Point toPoint(Object o) { final List<?> data = toList(o); return new Point(toInt(data.get(0)), toInt(data.get(1))); @@ -219,109 +151,110 @@ return (String) o; } - /** - * Sets GoogleMaps user interface options extracted from the specified JSON-like value on the - * given GoogleMap instance. - * - * @param o the JSON-like value - * @param googleMap the GoogleMap instance - */ - static void setMapOptions(Object o, GoogleMap googleMap) { - final Map<?, ?> options = toMap(o); - final Object cameraTargetBounds = options.get("cameraTargetBounds"); - final UiSettings uiSettings = googleMap.getUiSettings(); - if (cameraTargetBounds != null) { - final List<?> targetData = toList(cameraTargetBounds); - if (targetData.get(0) == null) { - googleMap.setLatLngBoundsForCameraTarget(null); - } else { - googleMap.setLatLngBoundsForCameraTarget(toLatLngBounds(targetData.get(0))); - } - } - final Object compassEnabled = options.get("compassEnabled"); - if (compassEnabled != null) { - uiSettings.setCompassEnabled(toBoolean(compassEnabled)); - } - final Object mapType = options.get("mapType"); - if (mapType != null) { - googleMap.setMapType(toInt(mapType)); - } - final Object rotateGesturesEnabled = options.get("rotateGesturesEnabled"); - if (rotateGesturesEnabled != null) { - uiSettings.setRotateGesturesEnabled(toBoolean(rotateGesturesEnabled)); - } - final Object scrollGesturesEnabled = options.get("scrollGesturesEnabled"); - if (scrollGesturesEnabled != null) { - uiSettings.setScrollGesturesEnabled(toBoolean(scrollGesturesEnabled)); - } - final Object tiltGesturesEnabled = options.get("tiltGesturesEnabled"); - if (tiltGesturesEnabled != null) { - uiSettings.setTiltGesturesEnabled(toBoolean(tiltGesturesEnabled)); - } - final Object zoomBounds = options.get("zoomBounds"); - if (zoomBounds != null) { - final List<?> zoomData = toList(zoomBounds); - googleMap.resetMinMaxZoomPreference(); - if (zoomData.get(0) != null) { - googleMap.setMinZoomPreference(toFloat(zoomData.get(0))); - } - if (zoomData.get(1) != null) { - googleMap.setMaxZoomPreference(toFloat(zoomData.get(1))); - } - } - final Object zoomGesturesEnabled = options.get("zoomGesturesEnabled"); - if (zoomGesturesEnabled != null) { - uiSettings.setZoomGesturesEnabled(toBoolean(zoomGesturesEnabled)); - } - final Object cameraPosition = options.get("cameraPosition"); + static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { + final Map<?, ?> data = toMap(o); + final Object cameraPosition = data.get("cameraPosition"); if (cameraPosition != null) { - googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(toCameraPosition(cameraPosition))); + sink.setCameraPosition(toCameraPosition(cameraPosition)); + } + final Object compassEnabled = data.get("compassEnabled"); + if (compassEnabled != null) { + sink.setCompassEnabled(toBoolean(compassEnabled)); + } + final Object latLngCameraTargetBounds = data.get("latLngCameraTargetBounds"); + if (latLngCameraTargetBounds != null) { + final List<?> targetData = toList(latLngCameraTargetBounds); + sink.setLatLngBoundsForCameraTarget(toLatLngBounds(targetData.get(0))); + } + final Object mapType = data.get("mapType"); + if (mapType != null) { + sink.setMapType(toInt(mapType)); + } + final Object minMaxZoomPreference = data.get("minMaxZoomPreference"); + if (minMaxZoomPreference != null) { + final List<?> zoomPreferenceData = toList(minMaxZoomPreference); + sink.setMinMaxZoomPreference( // + toFloatWrapper(zoomPreferenceData.get(0)), // + toFloatWrapper(zoomPreferenceData.get(1))); + } + final Object rotateGesturesEnabled = data.get("rotateGesturesEnabled"); + if (rotateGesturesEnabled != null) { + sink.setRotateGesturesEnabled(toBoolean(rotateGesturesEnabled)); + } + final Object scrollGesturesEnabled = data.get("scrollGesturesEnabled"); + if (scrollGesturesEnabled != null) { + sink.setScrollGesturesEnabled(toBoolean(scrollGesturesEnabled)); + } + final Object tiltGesturesEnabled = data.get("tiltGesturesEnabled"); + if (tiltGesturesEnabled != null) { + sink.setTiltGesturesEnabled(toBoolean(tiltGesturesEnabled)); + } + final Object trackCameraPosition = data.get("trackCameraPosition"); + if (trackCameraPosition != null) { + sink.setTrackCameraPosition(toBoolean(trackCameraPosition)); + } + final Object zoomGesturesEnabled = data.get("zoomGesturesEnabled"); + if (zoomGesturesEnabled != null) { + sink.setZoomGesturesEnabled(toBoolean(zoomGesturesEnabled)); } } - /** - * Stores GoogleMaps user interface configuration items extracted from the specified JSON-like - * value in the provided storage Map for cases where no getters exist in the GoogleMaps APIs. - * - * @param o the JSON-like value - * @param storageMap the storage Map - */ - static void setMapOptionsWithNoGetters(Object o, Map<String, Object> storageMap) { - final Map<?, ?> options = toMap(o); - final Object cameraTargetBounds = options.get("cameraTargetBounds"); - if (cameraTargetBounds != null) { - storageMap.put("cameraTargetBounds", cameraTargetBounds); + static void interpretMarkerOptions(Object o, MarkerOptionsSink sink) { + final Map<?, ?> data = toMap(o); + final Object alpha = data.get("alpha"); + if (alpha != null) { + sink.setAlpha(toFloat(alpha)); } - final Object zoomBounds = options.get("zoomBounds"); - if (zoomBounds != null) { - storageMap.put("zoomBounds", zoomBounds); + final Object anchor = data.get("anchor"); + if (anchor != null) { + final List<?> anchorData = toList(anchor); + sink.setAnchor(toFloat(anchorData.get(0)), toFloat(anchorData.get(1))); } - } - - /** - * Extract current GoogleMaps user interface configuration items in a JSON-like value, using the - * specified storage Map for cases where no getters exist in the GoogleMaps APIs. - * - * @param googleMap a GoogleMap instance - * @param storageMap the storage Map - * @return a JSON-like value - */ - static Object getMapOptions(GoogleMap googleMap, Map<String, Object> storageMap) { - final Map<String, Object> json = new HashMap<>(storageMap); - final UiSettings uiSettings = googleMap.getUiSettings(); - json.put("cameraPosition", toJson(googleMap.getCameraPosition())); - if (!json.containsKey("cameraTargetBounds")) { - json.put("cameraTargetBounds", Collections.singletonList(null)); // unbounded + final Object consumesTapEvents = data.get("consumesTapEvents"); + if (consumesTapEvents != null) { + sink.setConsumesTapEvents(toBoolean(consumesTapEvents)); } - json.put("compassEnabled", uiSettings.isCompassEnabled()); - json.put("mapType", googleMap.getMapType()); - json.put("rotateGesturesEnabled", uiSettings.isRotateGesturesEnabled()); - json.put("scrollGesturesEnabled", uiSettings.isScrollGesturesEnabled()); - json.put("tiltGesturesEnabled", uiSettings.isTiltGesturesEnabled()); - if (!json.containsKey("zoomBounds")) { - json.put("zoomBounds", Arrays.asList(null, null)); // unbounded + final Object draggable = data.get("draggable"); + if (draggable != null) { + sink.setDraggable(toBoolean(draggable)); } - json.put("zoomGesturesEnabled", uiSettings.isZoomGesturesEnabled()); - return json; + final Object flat = data.get("flat"); + if (flat != null) { + sink.setFlat(toBoolean(flat)); + } + final Object icon = data.get("icon"); + if (icon != null) { + sink.setIcon(toBitmapDescriptor(icon)); + } + final Object infoWindowAnchor = data.get("infoWindowAnchor"); + if (infoWindowAnchor != null) { + final List<?> anchorData = toList(infoWindowAnchor); + sink.setInfoWindowAnchor(toFloat(anchorData.get(0)), toFloat(anchorData.get(1))); + } + final Object infoWindowShown = data.get("infoWindowShown"); + if (infoWindowShown != null) { + sink.setInfoWindowShown(toBoolean(infoWindowShown)); + } + final Object infoWindowText = data.get("infoWindowText"); + if (infoWindowText != null) { + final List<?> textData = toList(infoWindowText); + sink.setInfoWindowText(toString(textData.get(0)), toString(textData.get(1))); + } + final Object position = data.get("position"); + if (position != null) { + sink.setPosition(toLatLng(position)); + } + final Object rotation = data.get("rotation"); + if (rotation != null) { + sink.setRotation(toFloat(rotation)); + } + final Object visible = data.get("visible"); + if (visible != null) { + sink.setVisible(toBoolean(visible)); + } + final Object zIndex = data.get("zIndex"); + if (zIndex != null) { + sink.setZIndex(toFloat(zIndex)); + } } }
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapBuilder.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapBuilder.java new file mode 100644 index 0000000..87ac319 --- /dev/null +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapBuilder.java
@@ -0,0 +1,85 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemobilemaps; + +import com.google.android.gms.maps.GoogleMapOptions; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLngBounds; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +import java.util.concurrent.atomic.AtomicInteger; + +class GoogleMapBuilder implements GoogleMapOptionsSink { + private final GoogleMapOptions options = new GoogleMapOptions(); + private boolean trackCameraPosition = false; + + GoogleMapController build( + AtomicInteger state, + PluginRegistry.Registrar registrar, + int width, + int height, + MethodChannel.Result result) { + final GoogleMapController controller = + new GoogleMapController(state, registrar, width, height, options, result); + controller.init(); + controller.setTrackCameraPosition(trackCameraPosition); + return controller; + } + + @Override + public void setCameraPosition(CameraPosition position) { + options.camera(position); + } + + @Override + public void setCompassEnabled(boolean compassEnabled) { + options.compassEnabled(compassEnabled); + } + + @Override + public void setLatLngBoundsForCameraTarget(LatLngBounds bounds) { + options.latLngBoundsForCameraTarget(bounds); + } + + @Override + public void setMapType(int mapType) { + options.mapType(mapType); + } + + @Override + public void setMinMaxZoomPreference(Float min, Float max) { + if (min != null) { + options.minZoomPreference(min); + } + if (max != null) { + options.maxZoomPreference(max); + } + } + + @Override + public void setTrackCameraPosition(boolean trackCameraPosition) { + this.trackCameraPosition = trackCameraPosition; + } + + @Override + public void setRotateGesturesEnabled(boolean rotateGesturesEnabled) { + options.rotateGesturesEnabled(rotateGesturesEnabled); + } + + @Override + public void setScrollGesturesEnabled(boolean scrollGesturesEnabled) { + options.scrollGesturesEnabled(scrollGesturesEnabled); + } + + @Override + public void setTiltGesturesEnabled(boolean tiltGesturesEnabled) { + options.tiltGesturesEnabled(tiltGesturesEnabled); + } + + @Override + public void setZoomGesturesEnabled(boolean zoomGesturesEnabled) { + options.zoomGesturesEnabled(zoomGesturesEnabled); + } +}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java index 4ae0a90..3f6114d 100644 --- a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapController.java
@@ -19,9 +19,13 @@ import android.view.Surface; import android.widget.FrameLayout; import com.google.android.gms.maps.CameraUpdate; +import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.GoogleMapOptions; import com.google.android.gms.maps.MapView; import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import io.flutter.plugin.common.MethodChannel; @@ -38,9 +42,12 @@ /** Controller of a single GoogleMaps MapView instance. */ final class GoogleMapController implements Application.ActivityLifecycleCallbacks, + GoogleMapOptionsSink, OnMapReadyCallback, GoogleMap.SnapshotReadyCallback, + GoogleMap.OnMarkerClickListener, GoogleMap.OnCameraMoveStartedListener, + GoogleMap.OnCameraMoveListener, GoogleMap.OnCameraIdleListener { private final AtomicInteger activityState; private final FrameLayout parent; @@ -52,10 +59,12 @@ private final int height; private final MethodChannel.Result result; private final Timer timer; - private final Map<String, Marker> markers; - private final Map<String, Object> optionsStorage; + private final Map<String, MarkerController> markers; + private OnMarkerTappedListener onMarkerTappedListener; + private OnCameraMoveListener onCameraMoveListener; private GoogleMap googleMap; private Surface surface; + private boolean trackCameraPosition = false; private boolean disposed = false; GoogleMapController( @@ -63,7 +72,7 @@ PluginRegistry.Registrar registrar, int width, int height, - Object options, + GoogleMapOptions options, MethodChannel.Result result) { this.activityState = activityState; this.registrar = registrar; @@ -75,11 +84,17 @@ this.textureEntry = registrar.textures().createSurfaceTexture(); this.surface = new Surface(textureEntry.surfaceTexture()); textureEntry.surfaceTexture().setDefaultBufferSize(width, height); - this.mapView = new MapView(registrar.activity(), Convert.toGoogleMapOptions(options)); + this.mapView = new MapView(registrar.activity(), options); this.timer = new Timer(); this.markers = new HashMap<>(); - this.optionsStorage = new HashMap<>(); - Convert.setMapOptionsWithNoGetters(options, optionsStorage); + } + + void setOnCameraMoveListener(OnCameraMoveListener listener) { + this.onCameraMoveListener = listener; + } + + void setOnMarkerTappedListener(OnMarkerTappedListener listener) { + this.onMarkerTappedListener = listener; } void init() { @@ -140,15 +155,6 @@ parent.addView(mapView, 0); } - void setMapOptions(Object json) { - Convert.setMapOptions(json, googleMap); - Convert.setMapOptionsWithNoGetters(json, optionsStorage); - } - - Object getMapOptions() { - return Convert.getMapOptions(googleMap, optionsStorage); - } - void moveCamera(CameraUpdate cameraUpdate) { googleMap.moveCamera(cameraUpdate); } @@ -157,47 +163,26 @@ googleMap.animateCamera(cameraUpdate); } - String addMarker(MarkerOptions options) { - final Marker marker = googleMap.addMarker(options); - markers.put(marker.getId(), marker); - return marker.getId(); + MarkerBuilder newMarkerBuilder() { + return new MarkerBuilder(this); + } + + Marker addMarker(MarkerOptions markerOptions, boolean consumesTapEvents) { + final Marker marker = googleMap.addMarker(markerOptions); + markers.put( + marker.getId(), new MarkerController(marker, consumesTapEvents, onMarkerTappedListener)); + return marker; } void removeMarker(String markerId) { - final Marker marker = markers.remove(markerId); - if (marker != null) { - marker.remove(); + final MarkerController markerController = markers.remove(markerId); + if (markerController != null) { + markerController.remove(); } } - void showMarkerInfoWindow(String markerId) { - final Marker marker = marker(markerId); - marker.showInfoWindow(); - } - - void hideMarkerInfoWindow(String markerId) { - final Marker marker = marker(markerId); - marker.hideInfoWindow(); - } - - void updateMarker(String markerId, MarkerOptions options) { - final Marker marker = marker(markerId); - marker.setPosition(options.getPosition()); - marker.setAlpha(options.getAlpha()); - marker.setAnchor(options.getAnchorU(), options.getAnchorV()); - marker.setDraggable(options.isDraggable()); - marker.setFlat(options.isFlat()); - marker.setIcon(options.getIcon()); - marker.setInfoWindowAnchor(options.getInfoWindowAnchorU(), options.getInfoWindowAnchorV()); - marker.setRotation(options.getRotation()); - marker.setSnippet(options.getSnippet()); - marker.setTitle(options.getTitle()); - marker.setVisible(options.isVisible()); - marker.setZIndex(options.getZIndex()); - } - - private Marker marker(String markerId) { - final Marker marker = markers.get(markerId); + MarkerController marker(String markerId) { + final MarkerController marker = markers.get(markerId); if (marker == null) { throw new IllegalArgumentException("Unknown marker: " + markerId); } @@ -218,7 +203,9 @@ this.googleMap = googleMap; result.success(id()); googleMap.setOnCameraMoveStartedListener(this); + googleMap.setOnCameraMoveListener(this); googleMap.setOnCameraIdleListener(this); + googleMap.setOnMarkerClickListener(this); // Take snapshots until the dust settles. timer.schedule(newSnapshotTask(), 0); timer.schedule(newSnapshotTask(), 500); @@ -229,11 +216,20 @@ @Override public void onCameraMoveStarted(int reason) { + onCameraMoveListener.onCameraMoveStarted(reason); cancelSnapshotTimerTasks(); } @Override + public void onCameraMove() { + if (trackCameraPosition && onCameraMoveListener != null) { + onCameraMoveListener.onCameraMove(googleMap.getCameraPosition()); + } + } + + @Override public void onCameraIdle() { + onCameraMoveListener.onCameraIdle(); // Take snapshots until the dust settles. timer.schedule(newSnapshotTask(), 500); timer.schedule(newSnapshotTask(), 1500); @@ -241,6 +237,12 @@ } @Override + public boolean onMarkerClick(Marker marker) { + final MarkerController markerController = markers.get(marker.getId()); + return (markerController != null && markerController.onTap()); + } + + @Override public void onSnapshotReady(Bitmap bitmap) { updateTexture(); } @@ -337,4 +339,62 @@ googleMap.snapshot(GoogleMapController.this, bitmap); } } + + // GoogleMapOptionsSink methods + + @Override + public void setCameraPosition(CameraPosition position) { + googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(position)); + } + + @Override + public void setCompassEnabled(boolean compassEnabled) { + googleMap.getUiSettings().setCompassEnabled(compassEnabled); + } + + @Override + public void setMapType(int mapType) { + googleMap.setMapType(mapType); + } + + @Override + public void setLatLngBoundsForCameraTarget(LatLngBounds bounds) { + googleMap.setLatLngBoundsForCameraTarget(bounds); + } + + @Override + public void setTrackCameraPosition(boolean trackCameraPosition) { + this.trackCameraPosition = trackCameraPosition; + } + + @Override + public void setRotateGesturesEnabled(boolean rotateGesturesEnabled) { + googleMap.getUiSettings().setRotateGesturesEnabled(rotateGesturesEnabled); + } + + @Override + public void setScrollGesturesEnabled(boolean scrollGesturesEnabled) { + googleMap.getUiSettings().setScrollGesturesEnabled(scrollGesturesEnabled); + } + + @Override + public void setTiltGesturesEnabled(boolean tiltGesturesEnabled) { + googleMap.getUiSettings().setTiltGesturesEnabled(tiltGesturesEnabled); + } + + @Override + public void setMinMaxZoomPreference(Float min, Float max) { + googleMap.resetMinMaxZoomPreference(); + if (min != null) { + googleMap.setMinZoomPreference(min); + } + if (max != null) { + googleMap.setMaxZoomPreference(max); + } + } + + @Override + public void setZoomGesturesEnabled(boolean zoomGesturesEnabled) { + googleMap.getUiSettings().setZoomGesturesEnabled(zoomGesturesEnabled); + } }
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapOptionsSink.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapOptionsSink.java new file mode 100644 index 0000000..6eb764d --- /dev/null +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMapOptionsSink.java
@@ -0,0 +1,31 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemobilemaps; + +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLngBounds; + +/** Receiver of GoogleMap configuration options. */ +interface GoogleMapOptionsSink { + void setCameraPosition(CameraPosition position); + + void setLatLngBoundsForCameraTarget(LatLngBounds bounds); + + void setCompassEnabled(boolean compassEnabled); + + void setMapType(int mapType); + + void setTrackCameraPosition(boolean reportCameraMoveEvents); + + void setRotateGesturesEnabled(boolean rotateGesturesEnabled); + + void setScrollGesturesEnabled(boolean scrollGesturesEnabled); + + void setTiltGesturesEnabled(boolean tiltGesturesEnabled); + + void setMinMaxZoomPreference(Float min, Float max); + + void setZoomGesturesEnabled(boolean zoomGesturesEnabled); +}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java index e581a89..542f1aa 100644 --- a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/GoogleMobileMapsPlugin.java
@@ -8,16 +8,24 @@ import android.app.Application; import android.os.Bundle; import com.google.android.gms.maps.CameraUpdate; -import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.Marker; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +/** + * Plugin for controlling a set of GoogleMap views to be shown as overlays on top of the Flutter + * view. The overlay should be hidden during transformations or while Flutter is rendering on top of + * the map. A Texture drawn using GoogleMap bitmap snapshots can then be shown instead of the + * overlay. + */ public class GoogleMobileMapsPlugin implements MethodCallHandler, Application.ActivityLifecycleCallbacks { static final int CREATED = 1; @@ -28,18 +36,20 @@ static final int DESTROYED = 6; private final Map<Long, GoogleMapController> googleMaps = new HashMap<>(); private final Registrar registrar; + private final MethodChannel channel; private final AtomicInteger state = new AtomicInteger(0); public static void registerWith(Registrar registrar) { - final GoogleMobileMapsPlugin plugin = new GoogleMobileMapsPlugin(registrar); final MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/google_mobile_maps"); + final GoogleMobileMapsPlugin plugin = new GoogleMobileMapsPlugin(registrar, channel); channel.setMethodCallHandler(plugin); registrar.activity().getApplication().registerActivityLifecycleCallbacks(plugin); } - private GoogleMobileMapsPlugin(Registrar registrar) { + private GoogleMobileMapsPlugin(Registrar registrar, MethodChannel channel) { this.registrar = registrar; + this.channel = channel; } @Override @@ -59,23 +69,52 @@ final int width = Convert.toInt(call.argument("width")); final int height = Convert.toInt(call.argument("height")); final Map<?, ?> options = Convert.toMap(call.argument("options")); + final GoogleMapBuilder builder = new GoogleMapBuilder(); + Convert.interpretGoogleMapOptions(options, builder); final GoogleMapController controller = - new GoogleMapController(state, registrar, width, height, options, result); + builder.build(state, registrar, width, height, result); googleMaps.put(controller.id(), controller); - controller.init(); + controller.setOnCameraMoveListener( + new OnCameraMoveListener() { + @Override + public void onCameraMoveStarted(int reason) { + final Map<String, Object> arguments = new HashMap<>(2); + arguments.put("map", controller.id()); + arguments.put("reason", reason); + channel.invokeMethod("map#onCameraMoveStarted", arguments); + } + + @Override + public void onCameraMove(CameraPosition position) { + final Map<String, Object> arguments = new HashMap<>(2); + arguments.put("map", controller.id()); + arguments.put("position", Convert.toJson(position)); + channel.invokeMethod("map#onCameraMove", arguments); + } + + @Override + public void onCameraIdle() { + channel.invokeMethod( + "map#onCameraIdle", Collections.singletonMap("map", controller.id())); + } + }); + controller.setOnMarkerTappedListener( + new OnMarkerTappedListener() { + @Override + public void onMarkerTapped(Marker marker) { + final Map<String, Object> arguments = new HashMap<>(2); + arguments.put("map", controller.id()); + arguments.put("marker", marker.getId()); + channel.invokeMethod("marker#onTap", arguments); + } + }); // result.success is called from controller when the GoogleMaps instance is ready break; } - case "getMapOptions": - { - final GoogleMapController controller = mapsController(call); - result.success(controller.getMapOptions()); - break; - } case "setMapOptions": { final GoogleMapController controller = mapsController(call); - controller.setMapOptions(call.argument("options")); + Convert.interpretGoogleMapOptions(call.argument("options"), controller); result.success(null); break; } @@ -98,9 +137,9 @@ case "addMarker": { final GoogleMapController controller = mapsController(call); - final MarkerOptions markerOptions = - Convert.toMarkerOptions(call.argument("markerOptions")); - final String markerId = controller.addMarker(markerOptions); + final MarkerBuilder markerBuilder = controller.newMarkerBuilder(); + Convert.interpretMarkerOptions(call.argument("options"), markerBuilder); + final String markerId = markerBuilder.build(); result.success(markerId); break; } @@ -112,29 +151,12 @@ result.success(null); break; } - case "marker#hideInfoWindow": - { - final GoogleMapController controller = mapsController(call); - final String markerId = call.argument("marker"); - controller.hideMarkerInfoWindow(markerId); - result.success(null); - break; - } - case "marker#showInfoWindow": - { - final GoogleMapController controller = mapsController(call); - final String markerId = call.argument("marker"); - controller.showMarkerInfoWindow(markerId); - result.success(null); - break; - } case "marker#update": { final GoogleMapController controller = mapsController(call); final String markerId = call.argument("marker"); - final MarkerOptions markerOptions = - Convert.toMarkerOptions(call.argument("markerOptions")); - controller.updateMarker(markerId, markerOptions); + final MarkerController marker = controller.marker(markerId); + Convert.interpretMarkerOptions(call.argument("options"), marker); result.success(null); break; }
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerBuilder.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerBuilder.java new file mode 100644 index 0000000..dc06eb7 --- /dev/null +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerBuilder.java
@@ -0,0 +1,96 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemobilemaps; + +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; + +class MarkerBuilder implements MarkerOptionsSink { + private final GoogleMapController mapController; + private final MarkerOptions markerOptions; + private boolean consumesTapEvents; + private boolean infoWindowShown; + + MarkerBuilder(GoogleMapController mapController) { + this.mapController = mapController; + this.markerOptions = new MarkerOptions(); + } + + String build() { + final Marker marker = mapController.addMarker(markerOptions, consumesTapEvents); + if (infoWindowShown) { + marker.showInfoWindow(); + } + return marker.getId(); + } + + @Override + public void setAlpha(float alpha) { + markerOptions.alpha(alpha); + } + + @Override + public void setAnchor(float u, float v) { + markerOptions.anchor(u, v); + } + + @Override + public void setConsumesTapEvents(boolean consumesTapEvents) { + this.consumesTapEvents = consumesTapEvents; + } + + @Override + public void setDraggable(boolean draggable) { + markerOptions.draggable(draggable); + } + + @Override + public void setFlat(boolean flat) { + markerOptions.flat(flat); + } + + @Override + public void setIcon(BitmapDescriptor bitmapDescriptor) { + markerOptions.icon(bitmapDescriptor); + } + + @Override + public void setInfoWindowAnchor(float u, float v) { + markerOptions.infoWindowAnchor(u, v); + } + + @Override + public void setInfoWindowShown(boolean infoWindowShown) { + this.infoWindowShown = infoWindowShown; + } + + @Override + public void setInfoWindowText(String title, String snippet) { + markerOptions.title(title); + markerOptions.snippet(snippet); + } + + @Override + public void setPosition(LatLng position) { + markerOptions.position(position); + } + + @Override + public void setRotation(float rotation) { + markerOptions.rotation(rotation); + } + + @Override + public void setVisible(boolean visible) { + markerOptions.visible(visible); + } + + @Override + public void setZIndex(float zIndex) { + markerOptions.zIndex(zIndex); + } +}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerController.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerController.java new file mode 100644 index 0000000..bd6e660 --- /dev/null +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerController.java
@@ -0,0 +1,104 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemobilemaps; + +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; + +/** Controller of a single Marker on the map. */ +class MarkerController implements MarkerOptionsSink { + private final Marker marker; + private final OnMarkerTappedListener onTappedListener; + private boolean consumesTapEvents; + + MarkerController( + Marker marker, boolean consumesTapEvents, OnMarkerTappedListener onTappedListener) { + this.marker = marker; + this.consumesTapEvents = consumesTapEvents; + this.onTappedListener = onTappedListener; + } + + boolean onTap() { + if (onTappedListener != null) { + onTappedListener.onMarkerTapped(marker); + } + return consumesTapEvents; + } + + void remove() { + marker.remove(); + } + + @Override + public void setAlpha(float alpha) { + marker.setAlpha(alpha); + } + + @Override + public void setAnchor(float u, float v) { + marker.setAnchor(u, v); + } + + @Override + public void setConsumesTapEvents(boolean consumesTapEvents) { + this.consumesTapEvents = consumesTapEvents; + } + + @Override + public void setDraggable(boolean draggable) { + marker.setDraggable(draggable); + } + + @Override + public void setFlat(boolean flat) { + marker.setFlat(flat); + } + + @Override + public void setIcon(BitmapDescriptor bitmapDescriptor) { + marker.setIcon(bitmapDescriptor); + } + + @Override + public void setInfoWindowAnchor(float u, float v) { + marker.setInfoWindowAnchor(u, v); + } + + @Override + public void setInfoWindowShown(boolean infoWindowShown) { + if (infoWindowShown) { + marker.showInfoWindow(); + } else { + marker.hideInfoWindow(); + } + } + + @Override + public void setInfoWindowText(String title, String snippet) { + marker.setTitle(title); + marker.setSnippet(snippet); + } + + @Override + public void setPosition(LatLng position) { + marker.setPosition(position); + } + + @Override + public void setRotation(float rotation) { + marker.setRotation(rotation); + } + + @Override + public void setVisible(boolean visible) { + marker.setVisible(visible); + } + + @Override + public void setZIndex(float zIndex) { + marker.setZIndex(zIndex); + } +}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerOptionsSink.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerOptionsSink.java new file mode 100644 index 0000000..7499e40 --- /dev/null +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/MarkerOptionsSink.java
@@ -0,0 +1,37 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemobilemaps; + +import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.LatLng; + +/** Receiver of Marker configuration options. */ +interface MarkerOptionsSink { + void setAlpha(float alpha); + + void setAnchor(float u, float v); + + void setConsumesTapEvents(boolean consumesTapEvents); + + void setDraggable(boolean draggable); + + void setFlat(boolean flat); + + void setIcon(BitmapDescriptor bitmapDescriptor); + + void setInfoWindowAnchor(float u, float v); + + void setInfoWindowShown(boolean shown); + + void setInfoWindowText(String title, String snippet); + + void setPosition(LatLng position); + + void setRotation(float rotation); + + void setVisible(boolean visible); + + void setZIndex(float zIndex); +}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnCameraMoveListener.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnCameraMoveListener.java new file mode 100644 index 0000000..671c9f2 --- /dev/null +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnCameraMoveListener.java
@@ -0,0 +1,15 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemobilemaps; + +import com.google.android.gms.maps.model.CameraPosition; + +interface OnCameraMoveListener { + void onCameraMoveStarted(int reason); + + void onCameraMove(CameraPosition newPosition); + + void onCameraIdle(); +}
diff --git a/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnMarkerTappedListener.java b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnMarkerTappedListener.java new file mode 100644 index 0000000..ce9319c --- /dev/null +++ b/packages/google_mobile_maps/android/src/main/java/io/flutter/plugins/googlemobilemaps/OnMarkerTappedListener.java
@@ -0,0 +1,11 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemobilemaps; + +import com.google.android.gms.maps.model.Marker; + +interface OnMarkerTappedListener { + void onMarkerTapped(Marker marker); +}
diff --git a/packages/google_mobile_maps/example/lib/animate_camera.dart b/packages/google_mobile_maps/example/lib/animate_camera.dart index f867e86..abb9f1d 100644 --- a/packages/google_mobile_maps/example/lib/animate_camera.dart +++ b/packages/google_mobile_maps/example/lib/animate_camera.dart
@@ -11,12 +11,9 @@ AnimateCameraPage() : super(const Icon(Icons.map), "Camera control, animated"); - final GoogleMapsOverlayController controller = - new GoogleMapsOverlayController.fromSize(width: 300.0, height: 200.0); - @override - PlatformOverlayController get overlayController => - controller.overlayController; + final GoogleMapOverlayController controller = + new GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0); @override Widget build(BuildContext context) { @@ -24,7 +21,7 @@ mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - new Center(child: new GoogleMapsOverlay(controller: controller)), + new Center(child: new GoogleMapOverlay(controller: controller)), new Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ @@ -32,7 +29,7 @@ children: <Widget>[ new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, @@ -47,7 +44,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), @@ -57,7 +54,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.newLatLngBounds( const LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), @@ -71,7 +68,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.newLatLngZoom( const LatLng(37.4231613, -122.087159), 11.0, @@ -82,7 +79,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.scrollBy(150.0, -225.0), ); }, @@ -94,7 +91,7 @@ children: <Widget>[ new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.zoomBy( -0.5, const Offset(30.0, 20.0), @@ -105,7 +102,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.zoomBy(-0.5), ); }, @@ -113,7 +110,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.zoomIn(), ); }, @@ -121,7 +118,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.zoomOut(), ); }, @@ -129,7 +126,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.animateCamera( + controller.mapController.animateCamera( CameraUpdate.zoomTo(16.0), ); },
diff --git a/packages/google_mobile_maps/example/lib/main.dart b/packages/google_mobile_maps/example/lib/main.dart index f22e075..63c28b4 100644 --- a/packages/google_mobile_maps/example/lib/main.dart +++ b/packages/google_mobile_maps/example/lib/main.dart
@@ -43,10 +43,10 @@ } void main() { - GoogleMapsController.init(); + GoogleMapController.init(); final List<NavigatorObserver> observers = <NavigatorObserver>[]; for (Page p in _allPages) { - observers.add(p.overlayController); + observers.add(p.controller.overlayController); } runApp(new MaterialApp(home: new MapsDemo(), navigatorObservers: observers)); }
diff --git a/packages/google_mobile_maps/example/lib/map_ui.dart b/packages/google_mobile_maps/example/lib/map_ui.dart index 6565caa..3fc73ec 100644 --- a/packages/google_mobile_maps/example/lib/map_ui.dart +++ b/packages/google_mobile_maps/example/lib/map_ui.dart
@@ -15,8 +15,9 @@ class MapUiPage extends Page { MapUiPage() : super(const Icon(Icons.map), "User interface"); - final GoogleMapsOverlayController controller = - new GoogleMapsOverlayController.fromSize( + @override + final GoogleMapOverlayController controller = + new GoogleMapOverlayController.fromSize( width: 300.0, height: 200.0, options: const GoogleMapOptions( @@ -24,21 +25,18 @@ target: const LatLng(-33.852, 151.211), zoom: 11.0, ), + trackCameraPosition: true, ), ); @override - PlatformOverlayController get overlayController => - controller.overlayController; - - @override Widget build(BuildContext context) { return new MapUiBody(controller); } } class MapUiBody extends StatefulWidget { - final GoogleMapsOverlayController controller; + final GoogleMapOverlayController controller; const MapUiBody(this.controller); @@ -47,144 +45,141 @@ } class MapUiBodyState extends State<MapUiBody> { - Future<GoogleMapOptions> _optionsFuture; + CameraPosition _position; + GoogleMapOptions _options; @override void initState() { super.initState(); - _optionsFuture = widget.controller.mapsController.getMapOptions(); + final GoogleMapController mapController = widget.controller.mapController; + mapController.addListener(() { + setState(() { + _options = mapController.options; + _position = mapController.cameraPosition; + }); + }); + _options = mapController.options; + _position = mapController.cameraPosition; } - Widget _compassToggler(GoogleMapOptions options) { + Widget _compassToggler() { return new FlatButton( child: - new Text('${options.compassEnabled ? 'disable' : 'enable'} compass'), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( - new GoogleMapOptions(compassEnabled: !options.compassEnabled), + new Text('${_options.compassEnabled ? 'disable' : 'enable'} compass'), + onPressed: () { + widget.controller.mapController.updateMapOptions( + new GoogleMapOptions(compassEnabled: !_options.compassEnabled), ); - _reloadOptions(); }, ); } - Widget _latLngBoundsToggler(GoogleMapOptions options) { + Widget _latLngBoundsToggler() { return new FlatButton( child: new Text( - options.cameraTargetBounds.isBounded - ? 'release camera target' - : 'bound camera target', + _options.latLngCameraTargetBounds.bounds == null + ? 'bound camera target' + : 'release camera target', ), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( + onPressed: () { + widget.controller.mapController.updateMapOptions( new GoogleMapOptions( - cameraTargetBounds: options.cameraTargetBounds.isBounded - ? CameraTargetBounds.unbounded - : const CameraTargetBounds(sydneyBounds), + latLngCameraTargetBounds: + _options.latLngCameraTargetBounds.bounds == null + ? const LatLngCameraTargetBounds(sydneyBounds) + : LatLngCameraTargetBounds.unbounded, ), ); - _reloadOptions(); }, ); } - Widget _zoomBoundsToggler(GoogleMapOptions options) { + Widget _zoomBoundsToggler() { return new FlatButton( - child: new Text( - options.zoomBounds.isBounded ? 'release zoom' : 'bound zoom'), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( + child: new Text(_options.minMaxZoomPreference.minZoom == null + ? 'bound zoom' + : 'release zoom'), + onPressed: () { + widget.controller.mapController.updateMapOptions( new GoogleMapOptions( - zoomBounds: options.zoomBounds.isBounded - ? ZoomBounds.unbounded - : const ZoomBounds(12.0, 16.0), + minMaxZoomPreference: _options.minMaxZoomPreference.minZoom == null + ? const MinMaxZoomPreference(12.0, 16.0) + : MinMaxZoomPreference.unbounded, ), ); - _reloadOptions(); }, ); } - Widget _mapTypeCycler(GoogleMapOptions options) { + Widget _mapTypeCycler() { final MapType nextType = - MapType.values[(options.mapType.index + 1) % MapType.values.length]; + MapType.values[(_options.mapType.index + 1) % MapType.values.length]; return new FlatButton( child: new Text('change map type to $nextType'), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( + onPressed: () { + widget.controller.mapController.updateMapOptions( new GoogleMapOptions(mapType: nextType), ); - _reloadOptions(); }, ); } - Widget _rotateToggler(GoogleMapOptions options) { + Widget _rotateToggler() { return new FlatButton( child: new Text( - '${options.rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( + '${_options.rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), + onPressed: () { + widget.controller.mapController.updateMapOptions( new GoogleMapOptions( - rotateGesturesEnabled: !options.rotateGesturesEnabled, + rotateGesturesEnabled: !_options.rotateGesturesEnabled, ), ); - _reloadOptions(); }, ); } - Widget _scrollToggler(GoogleMapOptions options) { + Widget _scrollToggler() { return new FlatButton( child: new Text( - '${options.scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( + '${_options.scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), + onPressed: () { + widget.controller.mapController.updateMapOptions( new GoogleMapOptions( - scrollGesturesEnabled: !options.scrollGesturesEnabled, + scrollGesturesEnabled: !_options.scrollGesturesEnabled, ), ); - _reloadOptions(); }, ); } - Widget _tiltToggler(GoogleMapOptions options) { + Widget _tiltToggler() { return new FlatButton( child: new Text( - '${options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( + '${_options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), + onPressed: () { + widget.controller.mapController.updateMapOptions( new GoogleMapOptions( - tiltGesturesEnabled: !options.tiltGesturesEnabled, + tiltGesturesEnabled: !_options.tiltGesturesEnabled, ), ); - _reloadOptions(); }, ); } - Widget _zoomToggler(GoogleMapOptions options) { + Widget _zoomToggler() { return new FlatButton( child: new Text( - '${options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), - onPressed: () async { - await widget.controller.mapsController.setMapOptions( + '${_options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), + onPressed: () { + widget.controller.mapController.updateMapOptions( new GoogleMapOptions( - zoomGesturesEnabled: !options.zoomGesturesEnabled, + zoomGesturesEnabled: !_options.zoomGesturesEnabled, ), ); - _reloadOptions(); }, ); } - void _reloadOptions() { - setState(() { - _optionsFuture = widget.controller.mapsController.getMapOptions(); - }); - } - @override Widget build(BuildContext context) { return new Column( @@ -194,35 +189,26 @@ new Padding( padding: const EdgeInsets.all(10.0), child: new Center( - child: new GoogleMapsOverlay(controller: widget.controller), + child: new GoogleMapOverlay(controller: widget.controller), ), ), - new FutureBuilder<GoogleMapOptions>( - future: _optionsFuture, - builder: (_, AsyncSnapshot<GoogleMapOptions> snapshot) { - if (!snapshot.hasData) { - return const Text('Loading settings'); - } else { - final GoogleMapOptions options = snapshot.data; - return new Column( - children: <Widget>[ - new Text('camera bearing: ${options.cameraPosition.bearing}'), - new Text( - 'camera target: ${options.cameraPosition.target.latitude},${options.cameraPosition.target.longitude}'), - new Text('camera zoom: ${options.cameraPosition.zoom}'), - new Text('camera tilt: ${options.cameraPosition.tilt}'), - _compassToggler(options), - _latLngBoundsToggler(options), - _mapTypeCycler(options), - _zoomBoundsToggler(options), - _rotateToggler(options), - _scrollToggler(options), - _tiltToggler(options), - _zoomToggler(options), - ], - ); - } - }, + new Column( + children: <Widget>[ + new Text('camera bearing: ${_position.bearing}'), + new Text( + 'camera target: ${_position.target.latitude.toStringAsFixed(4)},' + '${_position.target.longitude.toStringAsFixed(4)}'), + new Text('camera zoom: ${_position.zoom}'), + new Text('camera tilt: ${_position.tilt}'), + _compassToggler(), + _latLngBoundsToggler(), + _mapTypeCycler(), + _zoomBoundsToggler(), + _rotateToggler(), + _scrollToggler(), + _tiltToggler(), + _zoomToggler(), + ], ), ], );
diff --git a/packages/google_mobile_maps/example/lib/move_camera.dart b/packages/google_mobile_maps/example/lib/move_camera.dart index 4a396f9..209d2bb 100644 --- a/packages/google_mobile_maps/example/lib/move_camera.dart +++ b/packages/google_mobile_maps/example/lib/move_camera.dart
@@ -10,12 +10,9 @@ class MoveCameraPage extends Page { MoveCameraPage() : super(const Icon(Icons.map), "Camera control"); - final GoogleMapsOverlayController controller = - new GoogleMapsOverlayController.fromSize(width: 300.0, height: 200.0); - @override - PlatformOverlayController get overlayController => - controller.overlayController; + final GoogleMapOverlayController controller = + new GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0); @override Widget build(BuildContext context) { @@ -23,7 +20,7 @@ mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - new Center(child: new GoogleMapsOverlay(controller: controller)), + new Center(child: new GoogleMapOverlay(controller: controller)), new Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ @@ -31,7 +28,7 @@ children: <Widget>[ new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, @@ -46,7 +43,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), @@ -56,7 +53,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.newLatLngBounds( const LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), @@ -70,7 +67,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.newLatLngZoom( const LatLng(37.4231613, -122.087159), 11.0, @@ -81,7 +78,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.scrollBy(150.0, -225.0), ); }, @@ -93,7 +90,7 @@ children: <Widget>[ new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.zoomBy( -0.5, const Offset(30.0, 20.0), @@ -104,7 +101,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.zoomBy(-0.5), ); }, @@ -112,7 +109,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.zoomIn(), ); }, @@ -120,7 +117,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.zoomOut(), ); }, @@ -128,7 +125,7 @@ ), new FlatButton( onPressed: () { - controller.mapsController.moveCamera( + controller.mapController.moveCamera( CameraUpdate.zoomTo(16.0), ); },
diff --git a/packages/google_mobile_maps/example/lib/page.dart b/packages/google_mobile_maps/example/lib/page.dart index d47125d..0971918 100644 --- a/packages/google_mobile_maps/example/lib/page.dart +++ b/packages/google_mobile_maps/example/lib/page.dart
@@ -11,5 +11,5 @@ final Widget leading; final String title; - PlatformOverlayController get overlayController; + GoogleMapOverlayController get controller; }
diff --git a/packages/google_mobile_maps/example/lib/place_marker.dart b/packages/google_mobile_maps/example/lib/place_marker.dart index 2bb5341..8551c3e 100644 --- a/packages/google_mobile_maps/example/lib/place_marker.dart +++ b/packages/google_mobile_maps/example/lib/place_marker.dart
@@ -12,16 +12,18 @@ class PlaceMarkerPage extends Page { PlaceMarkerPage() : super(const Icon(Icons.place), "Place marker"); - final GoogleMapsOverlayController controller = - new GoogleMapsOverlayController.fromSize(width: 300.0, height: 200.0) - ..mapsController.moveCamera(CameraUpdate.newLatLngZoom( - const LatLng(-33.852, 151.211), - 11.0, - )); - @override - PlatformOverlayController get overlayController => - controller.overlayController; + final GoogleMapOverlayController controller = + new GoogleMapOverlayController.fromSize( + width: 300.0, + height: 200.0, + options: const GoogleMapOptions( + cameraPosition: const CameraPosition( + target: const LatLng(-33.852, 151.211), + zoom: 11.0, + ), + ), + ); @override Widget build(BuildContext context) { @@ -30,7 +32,7 @@ } class PlaceMarkerBody extends StatefulWidget { - final GoogleMapsOverlayController controller; + final GoogleMapOverlayController controller; const PlaceMarkerBody(this.controller); @@ -41,151 +43,128 @@ class PlaceMarkerBodyState extends State<PlaceMarkerBody> { static const LatLng center = const LatLng(-33.86711, 151.1947171); - final List<Future<Marker>> markers = <Future<Marker>>[]; - int _nextAlpha = 1; - int _nextHue = 1; - int _nextPosition = 1; - int _nextRotation = 1; - int _nextAnchor = 1; - int _nextInfoWindowAnchor = 7; - int _nextTitle = 1; - int _nextZIndex = 1; + int _markerCount = 0; + Marker _selectedMarker; + + @override + void initState() { + super.initState(); + widget.controller.mapController.onMarkerTapped.add((Marker marker) { + if (_selectedMarker != null) { + _selectedMarker + .update(const MarkerOptions(icon: BitmapDescriptor.defaultMarker)); + } + setState(() { + _selectedMarker = marker; + }); + _selectedMarker.update(new MarkerOptions( + icon: BitmapDescriptor + .defaultMarkerWithHue(BitmapDescriptor.hueGreen))); + }); + } void _add() { + widget.controller.mapController.addMarker(new MarkerOptions( + position: new LatLng( + center.latitude + sin(_markerCount * pi / 6.0) / 20.0, + center.longitude + cos(_markerCount * pi / 6.0) / 20.0, + ), + infoWindowText: new InfoWindowText('Marker #${_markerCount + 1}', '*'), + )); setState(() { - markers.add( - widget.controller.mapsController.addMarker( - new MarkerOptions( - position: new LatLng( - center.latitude + sin(markers.length * pi / 6.0) / 20.0, - center.longitude + cos(markers.length * pi / 6.0) / 20.0, - ), - zIndex: markers.length.toDouble(), - icon: BitmapDescriptor.defaultMarkerWithHue(30.0 * markers.length), - ), - ), - ); + _markerCount += 1; }); } - Future<void> _remove() async { - final Marker marker = await markers.last; + void _remove() { + _selectedMarker.remove(); setState(() { - markers.removeLast(); + _selectedMarker = null; + _markerCount -= 1; }); - await marker.remove(); } - Future<void> _showInfo() async { - final Marker marker = await markers.last; - await marker.showInfoWindow(); - } - - Future<void> _hideInfo() async { - final Marker marker = await markers.last; - await marker.hideInfoWindow(); - } - - Future<void> _changePosition() async { - final Marker marker = await markers.last; - final LatLng position = new LatLng( - center.latitude + sin(_nextPosition * pi / 6.0) / 20.0, - center.longitude + cos(_nextPosition * pi / 6.0) / 20.0, - ); - setState(() { - _nextPosition = (_nextPosition + 1) % 12; - }); - await marker.update(marker.options.copyWith(position: position)); - } - - Future<void> _changeAnchor() async { - final Marker marker = await markers.last; + void _changePosition() { + final LatLng current = _selectedMarker.options.position; final Offset offset = new Offset( - (sin(_nextAnchor * pi / 6.0) + 1.0) / 2.0, - (cos(_nextAnchor * pi / 6.0) + 1.0) / 2.0, + center.latitude - current.latitude, + center.longitude - current.longitude, ); - setState(() { - _nextAnchor = (_nextAnchor + 1) % 12; - }); - await marker.update(marker.options.copyWith(anchor: offset)); + _selectedMarker.update( + new MarkerOptions( + position: new LatLng( + center.latitude + offset.dy, + center.longitude + offset.dx, + ), + ), + ); + } + + void _changeAnchor() { + final Offset currentAnchor = _selectedMarker.options.anchor; + final Offset newAnchor = + new Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + _selectedMarker.update(new MarkerOptions(anchor: newAnchor)); } Future<void> _changeInfoAnchor() async { - final Marker marker = await markers.last; - final Offset offset = new Offset( - (sin(_nextInfoWindowAnchor * pi / 6.0) + 1.0) / 2.0, - (cos(_nextInfoWindowAnchor * pi / 6.0) + 1.0) / 2.0, - ); - setState(() { - _nextInfoWindowAnchor = (_nextInfoWindowAnchor + 1) % 12; - }); - await marker.update(marker.options.copyWith(infoWindowAnchor: offset)); + final Offset currentAnchor = _selectedMarker.options.infoWindowAnchor; + final Offset newAnchor = + new Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + _selectedMarker.update(new MarkerOptions(infoWindowAnchor: newAnchor)); } Future<void> _toggleDraggable() async { - final Marker marker = await markers.last; - await marker.update( - marker.options.copyWith(draggable: !marker.options.draggable), - ); + _selectedMarker.update( + new MarkerOptions(draggable: !_selectedMarker.options.draggable)); } Future<void> _toggleFlat() async { - final Marker marker = await markers.last; - await marker.update( - marker.options.copyWith(flat: !marker.options.flat), - ); + _selectedMarker + .update(new MarkerOptions(flat: !_selectedMarker.options.flat)); } Future<void> _changeInfo() async { - final Marker marker = await markers.last; - final String title = _nextTitle == 0 ? null : 'Title $_nextTitle'; - final String snippet = _nextTitle % 2 == 0 ? null : 'Snippet $_nextTitle'; - setState(() { - _nextTitle = (_nextTitle + 1) % 12; - }); - await marker.update( - marker.options.copyWith(title: title, snippet: snippet), - ); + final InfoWindowText currentInfo = _selectedMarker.options.infoWindowText; + _selectedMarker.update(new MarkerOptions( + infoWindowText: new InfoWindowText( + currentInfo.title, + currentInfo.snippet + '*', + ), + )); } - Future<void> _changeIcon() async { - final Marker marker = await markers.last; - final BitmapDescriptor icon = BitmapDescriptor.defaultMarkerWithHue( - _nextHue * 30.0, + Future<void> _toggleInfoShown() async { + _selectedMarker.update( + new MarkerOptions( + infoWindowShown: !_selectedMarker.options.infoWindowShown), ); - setState(() { - _nextHue = (_nextHue + 1) % 12; - }); - await marker.update(marker.options.copyWith(icon: icon)); } Future<void> _changeAlpha() async { - final Marker marker = await markers.last; - final double alpha = 1.0 - _nextAlpha / 12.0; - setState(() { - _nextAlpha = (_nextAlpha + 1) % 12; - }); - await marker.update(marker.options.copyWith(alpha: alpha)); - } - - Future<void> _changeRotation() async { - final Marker marker = await markers.last; - final double rotation = _nextRotation * 30.0; - setState(() { - _nextRotation = (_nextRotation + 1) % 12; - }); - await marker.update( - marker.options.copyWith(rotation: rotation), + final double current = _selectedMarker.options.alpha; + _selectedMarker.update( + new MarkerOptions(alpha: current < 0.1 ? 1.0 : current * 0.75), ); } + Future<void> _changeRotation() async { + final double current = _selectedMarker.options.rotation; + _selectedMarker.update( + new MarkerOptions(rotation: current == 330.0 ? 0.0 : current + 30.0), + ); + } + + Future<void> _toggleVisible() async { + _selectedMarker + .update(new MarkerOptions(visible: !_selectedMarker.options.visible)); + } + Future<void> _changeZIndex() async { - final Marker marker = await markers.last; - final double zIndex = _nextZIndex.toDouble(); - setState(() { - _nextZIndex = (_nextZIndex + 1) % 12; - }); - await marker.update(marker.options.copyWith(zIndex: zIndex)); + final double current = _selectedMarker.options.zIndex; + _selectedMarker.update( + new MarkerOptions(zIndex: current == 12.0 ? 0.0 : current + 1.0), + ); } @override @@ -194,7 +173,7 @@ mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - new Center(child: new GoogleMapsOverlay(controller: widget.controller)), + new Center(child: new GoogleMapOverlay(controller: widget.controller)), new Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ @@ -204,63 +183,68 @@ children: <Widget>[ new FlatButton( child: const Text('add'), - onPressed: (markers.length == 12) ? null : _add, + onPressed: (_markerCount == 12) ? null : _add, ), new FlatButton( child: const Text('remove'), - onPressed: (markers.isEmpty) ? null : _remove, + onPressed: (_selectedMarker == null) ? null : _remove, ), new FlatButton( - child: const Text('show info'), - onPressed: (markers.isEmpty) ? null : _showInfo, + child: const Text('change info'), + onPressed: (_selectedMarker == null) ? null : _changeInfo, ), new FlatButton( - child: const Text('hide info'), - onPressed: (markers.isEmpty) ? null : _hideInfo, + child: const Text('change info anchor'), + onPressed: + (_selectedMarker == null) ? null : _changeInfoAnchor, + ), + new FlatButton( + child: const Text('toggle info shown'), + onPressed: + (_selectedMarker == null) ? null : _toggleInfoShown, ), ], ), new Column( children: <Widget>[ new FlatButton( - child: const Text('change position'), - onPressed: (markers.isEmpty) ? null : _changePosition, + child: const Text('change alpha'), + onPressed: + (_selectedMarker == null) ? null : _changeAlpha, ), new FlatButton( child: const Text('change anchor'), - onPressed: (markers.isEmpty) ? null : _changeAnchor, - ), - new FlatButton( - child: const Text('change info anchor'), - onPressed: (markers.isEmpty) ? null : _changeInfoAnchor, + onPressed: + (_selectedMarker == null) ? null : _changeAnchor, ), new FlatButton( child: const Text('toggle draggable'), - onPressed: (markers.isEmpty) ? null : _toggleDraggable, + onPressed: + (_selectedMarker == null) ? null : _toggleDraggable, ), new FlatButton( child: const Text('toggle flat'), - onPressed: (markers.isEmpty) ? null : _toggleFlat, + onPressed: (_selectedMarker == null) ? null : _toggleFlat, ), new FlatButton( - child: const Text('change info'), - onPressed: (markers.isEmpty) ? null : _changeInfo, - ), - new FlatButton( - child: const Text('change color'), - onPressed: (markers.isEmpty) ? null : _changeIcon, - ), - new FlatButton( - child: const Text('change alpha'), - onPressed: (markers.isEmpty) ? null : _changeAlpha, + child: const Text('change position'), + onPressed: + (_selectedMarker == null) ? null : _changePosition, ), new FlatButton( child: const Text('change rotation'), - onPressed: (markers.isEmpty) ? null : _changeRotation, + onPressed: + (_selectedMarker == null) ? null : _changeRotation, + ), + new FlatButton( + child: const Text('toggle visible'), + onPressed: + (_selectedMarker == null) ? null : _toggleVisible, ), new FlatButton( child: const Text('change zIndex'), - onPressed: (markers.isEmpty) ? null : _changeZIndex, + onPressed: + (_selectedMarker == null) ? null : _changeZIndex, ), ], ),
diff --git a/packages/google_mobile_maps/lib/google_mobile_maps.dart b/packages/google_mobile_maps/lib/google_mobile_maps.dart index 2b12899..a58533e 100644 --- a/packages/google_mobile_maps/lib/google_mobile_maps.dart +++ b/packages/google_mobile_maps/lib/google_mobile_maps.dart
@@ -15,6 +15,7 @@ export 'dart:async'; part 'src/bitmap.dart'; +part 'src/callbacks.dart'; part 'src/camera.dart'; part 'src/controller.dart'; part 'src/ui.dart';
diff --git a/packages/google_mobile_maps/lib/src/callbacks.dart b/packages/google_mobile_maps/lib/src/callbacks.dart new file mode 100644 index 0000000..82a9abe --- /dev/null +++ b/packages/google_mobile_maps/lib/src/callbacks.dart
@@ -0,0 +1,44 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_mobile_maps; + +/// Callback function taking a single argument. +typedef void ArgumentCallback<T>(T argument); + +/// Mutable collection of [ArgumentCallback] instances, itself an [ArgumentCallback]. +/// +/// Additions and removals happening during a single [call] invocation do not +/// change who gets a callback until the next such invocation. +class ArgumentCallbacks<T> { + final List<ArgumentCallback<T>> _callbacks = <ArgumentCallback<T>>[]; + VoidCallback _onEmptyChanged; + + void call(T argument) { + final int length = _callbacks.length; + if (length == 1) { + _callbacks[0].call(argument); + } else if (0 < length) { + for (ArgumentCallback<T> callback + in new List<ArgumentCallback<T>>.from(_callbacks)) { + callback(argument); + } + } + } + + void add(ArgumentCallback<T> callback) { + _callbacks.add(callback); + if (_onEmptyChanged != null && _callbacks.length == 1) _onEmptyChanged(); + } + + void remove(ArgumentCallback<T> callback) { + final bool removed = _callbacks.remove(callback); + if (_onEmptyChanged != null && removed && _callbacks.isEmpty) + _onEmptyChanged(); + } + + bool get isEmpty => _callbacks.isEmpty; + + bool get isNotEmpty => _callbacks.isNotEmpty; +}
diff --git a/packages/google_mobile_maps/lib/src/controller.dart b/packages/google_mobile_maps/lib/src/controller.dart index 779d65e..a291840 100644 --- a/packages/google_mobile_maps/lib/src/controller.dart +++ b/packages/google_mobile_maps/lib/src/controller.dart
@@ -7,35 +7,107 @@ final MethodChannel _channel = const MethodChannel('plugins.flutter.io/google_mobile_maps'); -/// Controller for a single GoogleMaps instance. +/// Controller for a single GoogleMap instance. /// -/// Used for programmatically controlling a platform-specific -/// GoogleMaps view, once created. -class GoogleMapsController { +/// Used for programmatically controlling a platform-specific GoogleMap view. +/// +/// Change listeners are notified upon changes to any of +/// +/// * the [options] property, +/// * the collection of [Marker]s added to this map +/// * the [cameraPosition] property, +/// +/// Listeners are notified when changes have been applied on the platform side. +/// +/// Marker tap events can be received by adding callbacks to [onMarkerTapped]. +class GoogleMapController extends ChangeNotifier { + GoogleMapController._({ + this.id, + GoogleMapOptions options, + }) : _options = options { + id.then((int id) { + _controllers[id] = this; + }); + if (options.trackCameraPosition) { + _cameraPosition = options.cameraPosition; + } + } + /// An ID identifying the GoogleMaps instance, once created. final Future<int> id; - GoogleMapsController(this.id); + final ArgumentCallbacks<Marker> onMarkerTapped = + new ArgumentCallbacks<Marker>(); + + /// The configuration options most recently applied via controller + /// initialization or [updateMapOptions]. + GoogleMapOptions get options => _options; + GoogleMapOptions _options; + + Set<Marker> get markers => new Set<Marker>.from(_markers.values); + final Map<String, Marker> _markers = <String, Marker>{}; + + bool get isCameraMoving => _isCameraMoving; + bool _isCameraMoving = false; + + /// Returns the most recent camera position reported by the platform side. + /// Will be null, if camera position tracking is not enabled via + /// [GoogleMapOptions]. + CameraPosition get cameraPosition => _cameraPosition; + CameraPosition _cameraPosition; + + static Map<int, GoogleMapController> _controllers = + <int, GoogleMapController>{}; static Future<void> init() async { await _channel.invokeMethod('init'); + _channel.setMethodCallHandler((MethodCall call) { + final int mapId = call.arguments['map']; + final GoogleMapController controller = _controllers[mapId]; + if (controller != null) { + controller._handleMethodCall(call); + } + }); } - Future<void> setMapOptions(GoogleMapOptions options) async { + void _handleMethodCall(MethodCall call) { + switch (call.method) { + case "marker#onTap": + final String markerId = call.arguments['marker']; + final Marker marker = _markers[markerId]; + if (marker != null) { + onMarkerTapped(marker); + } + break; + case "map#onCameraMoveStarted": + _isCameraMoving = true; + notifyListeners(); + break; + case "map#onCameraMove": + _cameraPosition = CameraPosition._fromJson(call.arguments['position']); + notifyListeners(); + break; + case "map#onCameraIdle": + _isCameraMoving = false; + notifyListeners(); + break; + default: + throw new MissingPluginException(); + } + } + + Future<void> updateMapOptions(GoogleMapOptions options) async { + assert(options != null); final int id = await this.id; await _channel.invokeMethod('setMapOptions', <String, dynamic>{ 'map': id, 'options': options._toJson(), }); - } - - Future<GoogleMapOptions> getMapOptions() async { - final int id = await this.id; - final dynamic json = await _channel.invokeMethod( - 'getMapOptions', - <String, dynamic>{'map': id}, - ); - return GoogleMapOptions._fromJson(json); + _options = _options._updateWith(options); + if (!_options.trackCameraPosition) { + _cameraPosition = null; + } + notifyListeners(); } Future<void> animateCamera(CameraUpdate cameraUpdate) async { @@ -54,31 +126,62 @@ }); } - Future<Marker> addMarker(MarkerOptions markerOptions) async { + Future<Marker> addMarker(MarkerOptions options) async { + assert(options != null); + assert(options.position != null); final int id = await this.id; + final MarkerOptions effectiveOptions = + MarkerOptions.defaultOptions._updateWith(options); final String markerId = await _channel.invokeMethod( 'addMarker', <String, dynamic>{ 'map': id, - 'markerOptions': markerOptions._toJson(), + 'options': effectiveOptions._toJson(), }, ); - return new Marker._(id, markerId, markerOptions); + final Marker marker = new Marker._(this, markerId, effectiveOptions); + _markers[markerId] = marker; + notifyListeners(); + return marker; + } + + Future<void> _updateMarker(Marker marker, MarkerOptions changes) async { + assert(_markers[marker.id] == marker); + assert(changes != null); + final int id = await this.id; + await _channel.invokeMethod('marker#update', <String, dynamic>{ + 'map': id, + 'marker': marker.id, + 'options': changes._toJson(), + }); + marker._options = marker._options._updateWith(changes); + notifyListeners(); + } + + Future<void> _removeMarker(Marker marker) async { + assert(_markers[marker.id] == marker); + final int id = await this.id; + await _channel.invokeMethod('marker#remove', <String, dynamic>{ + 'map': id, + 'marker': marker.id, + }); + _markers.remove(marker.id); + notifyListeners(); } } -/// Controller for a GoogleMaps instance that is integrated as a +/// Controller for a GoogleMap instance that is integrated as a /// platform overlay. /// /// *Warning*: Platform overlays cannot be freely composed with /// other widgets. See [PlatformOverlayController] for caveats and /// limitations. -class GoogleMapsOverlayController { - GoogleMapsOverlayController._(this.mapsController, this.overlayController); +class GoogleMapOverlayController { + GoogleMapOverlayController._(this.mapController, this.overlayController); /// Creates a controller for a GoogleMaps of the specified size in /// logical pixels. - factory GoogleMapsOverlayController.fromSize({ + factory GoogleMapOverlayController.fromSize({ @required double width, @required double height, GoogleMapOptions options = const GoogleMapOptions(), @@ -86,16 +189,21 @@ assert(width != null); assert(height != null); assert(options != null); + final GoogleMapOptions effectiveOptions = + GoogleMapOptions.defaultOptions._updateWith(options); final _GoogleMapsPlatformOverlay overlay = - new _GoogleMapsPlatformOverlay(options); - return new GoogleMapsOverlayController._( - new GoogleMapsController(overlay._textureId.future), + new _GoogleMapsPlatformOverlay(effectiveOptions); + return new GoogleMapOverlayController._( + new GoogleMapController._( + id: overlay._textureId.future, + options: effectiveOptions, + ), new PlatformOverlayController(width, height, overlay), ); } /// The controller of the GoogleMaps instance. - final GoogleMapsController mapsController; + final GoogleMapController mapController; /// The controller of the platform overlay. final PlatformOverlayController overlayController; @@ -149,16 +257,16 @@ } /// A Widget covered by a GoogleMaps platform overlay. -class GoogleMapsOverlay extends StatefulWidget { - final GoogleMapsOverlayController controller; +class GoogleMapOverlay extends StatefulWidget { + final GoogleMapOverlayController controller; - GoogleMapsOverlay({Key key, @required this.controller}) : super(key: key); + GoogleMapOverlay({Key key, @required this.controller}) : super(key: key); @override - State<StatefulWidget> createState() => new _GoogleMapsOverlayState(); + State<StatefulWidget> createState() => new _GoogleMapOverlayState(); } -class _GoogleMapsOverlayState extends State<GoogleMapsOverlay> { +class _GoogleMapOverlayState extends State<GoogleMapOverlay> { @override void initState() { super.initState(); @@ -175,7 +283,7 @@ Widget build(BuildContext context) { return new SizedBox( child: new FutureBuilder<int>( - future: widget.controller.mapsController.id, + future: widget.controller.mapController.id, builder: (_, AsyncSnapshot<int> snapshot) { if (snapshot.hasData) { return new Texture(textureId: snapshot.data);
diff --git a/packages/google_mobile_maps/lib/src/marker.dart b/packages/google_mobile_maps/lib/src/marker.dart index 14b9b91..a328978 100644 --- a/packages/google_mobile_maps/lib/src/marker.dart +++ b/packages/google_mobile_maps/lib/src/marker.dart
@@ -4,127 +4,134 @@ part of google_mobile_maps; +/// An icon placed at a particular point on the map's surface. A marker icon is +/// drawn oriented against the device's screen rather than the map's surface; +/// that is, it will not necessarily change orientation due to map rotations, +/// tilting, or zooming. +/// +/// Markers are owned by a single [GoogleMapController] which fires change +/// events when markers are added, updated, or removed. class Marker { - Marker._(this.mapId, this.id, MarkerOptions options) : _options = options; + Marker._(this._mapController, this.id, this._options); - final int mapId; + final GoogleMapController _mapController; final String id; MarkerOptions _options; - Future<void> update(MarkerOptions options) async { - assert(options != null); - _options = options; - await _channel.invokeMethod('marker#update', <String, dynamic>{ - 'map': mapId, - 'marker': id, - 'markerOptions': options._toJson(), - }); + Future<void> remove() { + return _mapController._removeMarker(this); } + Future<void> update(MarkerOptions changes) { + return _mapController._updateMarker(this, changes); + } + + /// The configuration options most recently applied programmatically. + /// + /// The returned value does not reflect any changes made to the marker through + /// touch events. Add listeners to track those. MarkerOptions get options => _options; - - Future<void> remove() async { - await _channel.invokeMethod('marker#remove', <String, dynamic>{ - 'map': mapId, - 'marker': id, - }); - } - - Future<void> hideInfoWindow() async { - await _channel.invokeMethod('marker#hideInfoWindow', <String, dynamic>{ - 'map': mapId, - 'marker': id, - }); - } - - Future<void> showInfoWindow() async { - await _channel.invokeMethod('marker#showInfoWindow', <String, dynamic>{ - 'map': mapId, - 'marker': id, - }); - } } +dynamic _offsetToJson(Offset offset) => + offset == null ? null : <dynamic>[offset.dx, offset.dy]; + +/// Text labels for a [Marker] info window. +class InfoWindowText { + const InfoWindowText(this.title, this.snippet); + + static const InfoWindowText noText = const InfoWindowText(null, null); + + final String title; + final String snippet; + + dynamic _toJson() => <dynamic>[title, snippet]; +} + +/// Configuration options for [Marker] instances. +/// +/// When used to change configuration, null values will be interpreted as +/// "do not change this configuration item". When used to represent current +/// configuration, all values will be non-null. class MarkerOptions { - static const String unspecified = 'Unspecified'; - final LatLng position; final double alpha; final Offset anchor; + final bool consumesTapEvents; final bool draggable; final bool flat; final BitmapDescriptor icon; final Offset infoWindowAnchor; + final bool infoWindowShown; + final InfoWindowText infoWindowText; + final LatLng position; final double rotation; - final String snippet; - final String title; final bool visible; final double zIndex; const MarkerOptions({ - @required this.position, - this.alpha = 1.0, - this.anchor = const Offset(0.5, 1.0), - this.draggable = false, - this.flat = false, - this.icon = BitmapDescriptor.defaultMarker, - this.infoWindowAnchor = const Offset(0.5, 0.0), - this.rotation = 0.0, - this.snippet, - this.title, - this.visible = true, - this.zIndex = 0.0, - }) : assert(position != null), - assert(alpha != null), - assert(anchor != null), - assert(draggable != null), - assert(flat != null), - assert(icon != null), - assert(infoWindowAnchor != null), - assert(rotation != null), - assert(zIndex != null), - assert(visible != null); + this.alpha, + this.anchor, + this.consumesTapEvents, + this.draggable, + this.flat, + this.icon, + this.infoWindowAnchor, + this.infoWindowShown, + this.infoWindowText, + this.position, + this.rotation, + this.visible, + this.zIndex, + }); - MarkerOptions copyWith({ - LatLng position, - double alpha, - Offset anchor, - bool draggable, - bool flat, - BitmapDescriptor icon, - Offset infoWindowAnchor, - double rotation, - String snippet = unspecified, - String title = unspecified, - double zIndex, - bool visible, - }) => - new MarkerOptions( - position: position ?? this.position, - alpha: alpha ?? this.alpha, - anchor: anchor ?? this.anchor, - draggable: draggable ?? this.draggable, - flat: flat ?? this.flat, - icon: icon ?? this.icon, - infoWindowAnchor: infoWindowAnchor ?? this.infoWindowAnchor, - rotation: rotation ?? this.rotation, - snippet: identical(snippet, unspecified) ? this.snippet : snippet, - title: identical(title, unspecified) ? this.title : title, - zIndex: zIndex ?? this.zIndex, - visible: visible ?? this.visible, - ); + static const MarkerOptions defaultOptions = const MarkerOptions( + alpha: 1.0, + anchor: const Offset(0.5, 1.0), + consumesTapEvents: false, + draggable: false, + flat: false, + icon: BitmapDescriptor.defaultMarker, + infoWindowAnchor: const Offset(0.5, 0.0), + infoWindowShown: false, + infoWindowText: InfoWindowText.noText, + rotation: 0.0, + visible: true, + zIndex: 0.0, + ); - dynamic _toJson() => <String, dynamic>{ - 'position': position._toJson(), - 'alpha': alpha, - 'anchor': <double>[anchor.dx, anchor.dy], - 'draggable': draggable, - 'flat': flat, - 'icon': icon._toJson(), - 'infoWindowAnchor': <double>[infoWindowAnchor.dx, infoWindowAnchor.dy], - 'rotation': rotation, - 'snippet': snippet, - 'title': title, - 'visible': visible, - 'zIndex': zIndex, - }; + MarkerOptions _updateWith(MarkerOptions changes) { + return new MarkerOptions( + alpha: changes.alpha ?? alpha, + anchor: changes.anchor ?? anchor, + consumesTapEvents: changes.consumesTapEvents ?? consumesTapEvents, + draggable: changes.draggable ?? draggable, + flat: changes.flat ?? flat, + icon: changes.icon ?? icon, + infoWindowAnchor: changes.infoWindowAnchor ?? infoWindowAnchor, + infoWindowShown: changes.infoWindowShown ?? infoWindowShown, + infoWindowText: changes.infoWindowText ?? infoWindowText, + position: changes.position ?? position, + rotation: changes.rotation ?? rotation, + visible: changes.visible ?? visible, + zIndex: changes.zIndex ?? zIndex, + ); + } + + dynamic _toJson() { + return <String, dynamic>{ + 'alpha': alpha, + 'anchor': _offsetToJson(anchor), + 'consumesTapEvents': consumesTapEvents, + 'draggable': draggable, + 'flat': flat, + 'icon': icon?._toJson(), + 'infoWindowAnchor': _offsetToJson(infoWindowAnchor), + 'infoWindowShown': infoWindowShown, + 'infoWindowText': infoWindowText?._toJson(), + 'position': position?._toJson(), + 'rotation': rotation, + 'visible': visible, + 'zIndex': zIndex, + }; + } }
diff --git a/packages/google_mobile_maps/lib/src/platform_overlay.dart b/packages/google_mobile_maps/lib/src/platform_overlay.dart index 8baff43..067fd67 100644 --- a/packages/google_mobile_maps/lib/src/platform_overlay.dart +++ b/packages/google_mobile_maps/lib/src/platform_overlay.dart
@@ -4,7 +4,7 @@ part of google_mobile_maps; -/// Controller of platform overlays, supporting a limited form +/// Controller of platform overlays, supporting a very limited form /// of compositing with Flutter Widgets. /// /// Platform overlays are normal platform-specific views that are @@ -207,19 +207,19 @@ /// Platform overlay. abstract class PlatformOverlay { - /// Create a platform view of the specified [physicalSize] (in device pixels). + /// Creates a platform view of the specified [physicalSize] (in device pixels). /// /// The platform view should remain hidden until explicitly shown by calling /// [showOverlay]. Future<int> create(Size physicalSize); - /// Show the platform view at the specified [physicalOffset] (in device + /// Shows the platform view at the specified [physicalOffset] (in device /// pixels). Future<void> show(Offset physicalOffset); - /// Hide the platform view. + /// Hides the platform view. Future<void> hide(); - /// Dispose of the platform view. + /// Disposes of the platform view. Future<void> dispose(); }
diff --git a/packages/google_mobile_maps/lib/src/ui.dart b/packages/google_mobile_maps/lib/src/ui.dart index b86744c..1323136 100644 --- a/packages/google_mobile_maps/lib/src/ui.dart +++ b/packages/google_mobile_maps/lib/src/ui.dart
@@ -17,39 +17,22 @@ hybrid, } -MapType _mapTypeFromJson(dynamic json) { - if (json == null) { - return null; - } - return MapType.values[json]; -} - /// Bounds for the map camera target. -class CameraTargetBounds { - static const CameraTargetBounds unbounded = const CameraTargetBounds(null); - - const CameraTargetBounds(this.bounds); +class LatLngCameraTargetBounds { + const LatLngCameraTargetBounds(this.bounds); /// The current bounds or null, if the camera target is unbounded. final LatLngBounds bounds; - bool get isBounded => bounds != null; + static const LatLngCameraTargetBounds unbounded = + const LatLngCameraTargetBounds(null); dynamic _toJson() => <dynamic>[bounds?._toJson()]; - - static CameraTargetBounds _fromJson(dynamic json) { - if (json == null) { - return null; - } - return new CameraTargetBounds(LatLngBounds._fromJson(json[0])); - } } -/// Bounds for map camera zoom level. -class ZoomBounds { - static const ZoomBounds unbounded = const ZoomBounds(null, null); - - const ZoomBounds(this.minZoom, this.maxZoom) +/// Preferred bounds for map camera zoom level. +class MinMaxZoomPreference { + const MinMaxZoomPreference(this.minZoom, this.maxZoom) : assert(minZoom == null || maxZoom == null || minZoom <= maxZoom); /// The current minimum zoom level or null, if unbounded from below. @@ -58,16 +41,10 @@ /// The current maximum zoom level or null, if unbounded from above. final double maxZoom; - bool get isBounded => minZoom != null || maxZoom != null; + static const MinMaxZoomPreference unbounded = + const MinMaxZoomPreference(null, null); dynamic _toJson() => <dynamic>[minZoom, maxZoom]; - - static ZoomBounds _fromJson(dynamic json) { - if (json == null) { - return null; - } - return new ZoomBounds(json[0], json[1]); - } } /// Configuration options for the GoogleMaps user interface. @@ -77,55 +54,71 @@ /// configuration, all values will be non-null. class GoogleMapOptions { final CameraPosition cameraPosition; - final CameraTargetBounds cameraTargetBounds; final bool compassEnabled; + final LatLngCameraTargetBounds latLngCameraTargetBounds; final MapType mapType; + final MinMaxZoomPreference minMaxZoomPreference; final bool rotateGesturesEnabled; final bool scrollGesturesEnabled; final bool tiltGesturesEnabled; - final ZoomBounds zoomBounds; + final bool trackCameraPosition; final bool zoomGesturesEnabled; const GoogleMapOptions({ this.cameraPosition, - this.cameraTargetBounds, this.compassEnabled, + this.latLngCameraTargetBounds, this.mapType, + this.minMaxZoomPreference, this.rotateGesturesEnabled, this.scrollGesturesEnabled, this.tiltGesturesEnabled, - this.zoomBounds, + this.trackCameraPosition, this.zoomGesturesEnabled, }); - static GoogleMapOptions _fromJson(dynamic json) { - if (json == null) { - return null; - } + static const GoogleMapOptions defaultOptions = const GoogleMapOptions( + compassEnabled: true, + latLngCameraTargetBounds: LatLngCameraTargetBounds.unbounded, + mapType: MapType.normal, + minMaxZoomPreference: MinMaxZoomPreference.unbounded, + rotateGesturesEnabled: true, + scrollGesturesEnabled: true, + tiltGesturesEnabled: true, + trackCameraPosition: false, + zoomGesturesEnabled: true, + ); + + GoogleMapOptions _updateWith(GoogleMapOptions change) { return new GoogleMapOptions( - cameraPosition: CameraPosition._fromJson(json['cameraPosition']), - cameraTargetBounds: - CameraTargetBounds._fromJson(json['cameraTargetBounds']), - compassEnabled: json['compassEnabled'], - mapType: _mapTypeFromJson(json['mapType']), - rotateGesturesEnabled: json['rotateGesturesEnabled'], - scrollGesturesEnabled: json['scrollGesturesEnabled'], - tiltGesturesEnabled: json['tiltGesturesEnabled'], - zoomBounds: ZoomBounds._fromJson(json['zoomBounds']), - zoomGesturesEnabled: json['zoomGesturesEnabled'], + cameraPosition: change.cameraPosition ?? cameraPosition, + compassEnabled: change.compassEnabled ?? compassEnabled, + latLngCameraTargetBounds: + change.latLngCameraTargetBounds ?? latLngCameraTargetBounds, + mapType: change.mapType ?? mapType, + minMaxZoomPreference: change.minMaxZoomPreference ?? minMaxZoomPreference, + rotateGesturesEnabled: + change.rotateGesturesEnabled ?? rotateGesturesEnabled, + scrollGesturesEnabled: + change.scrollGesturesEnabled ?? scrollGesturesEnabled, + tiltGesturesEnabled: change.tiltGesturesEnabled ?? tiltGesturesEnabled, + trackCameraPosition: change.trackCameraPosition ?? trackCameraPosition, + zoomGesturesEnabled: change.zoomGesturesEnabled ?? zoomGesturesEnabled, ); } dynamic _toJson() { final Map<String, dynamic> json = <String, dynamic>{}; json['cameraPosition'] = cameraPosition?._toJson(); - json['cameraTargetBounds'] = cameraTargetBounds?._toJson(); json['compassEnabled'] = compassEnabled; + json['latLngCameraTargetBounds'] = latLngCameraTargetBounds?._toJson(); json['mapType'] = mapType?.index; + json['minMaxZoomPreference'] = minMaxZoomPreference?._toJson(); + json['reportCameraMoveEvents'] = trackCameraPosition; json['rotateGesturesEnabled'] = rotateGesturesEnabled; json['scrollGesturesEnabled'] = scrollGesturesEnabled; json['tiltGesturesEnabled'] = tiltGesturesEnabled; - json['zoomBounds'] = zoomBounds?._toJson(); + json['trackCameraPosition'] = trackCameraPosition; json['zoomGesturesEnabled'] = zoomGesturesEnabled; return json; }