Adds support for polygon overlays to the Google Maps plugin (#1551)
This is relatively trivial, requiring only some additional logic
to disambiguate click events between the various possible overlays.
Also adds a page to the example app demonstrating polygons,
which I tested on iOS and Android.
diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md
index 9ecdd88..d7e623d 100644
--- a/packages/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.5.15
+
+* Add support for Polygons.
+
## 0.5.14+1
* Example app update(comment out usage of the ImageStreamListener API which has a breaking change
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
index 46ae935..b8775b9 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
@@ -170,6 +170,15 @@
return data;
}
+ static Object polygonIdToJson(String polygonId) {
+ if (polygonId == null) {
+ return null;
+ }
+ final Map<String, Object> data = new HashMap<>(1);
+ data.put("polygonId", polygonId);
+ return data;
+ }
+
static Object polylineIdToJson(String polylineId) {
if (polylineId == null) {
return null;
@@ -364,6 +373,48 @@
}
}
+ static String interpretPolygonOptions(Object o, PolygonOptionsSink sink) {
+ final Map<?, ?> data = toMap(o);
+ final Object consumeTapEvents = data.get("consumeTapEvents");
+ if (consumeTapEvents != null) {
+ sink.setConsumeTapEvents(toBoolean(consumeTapEvents));
+ }
+ final Object geodesic = data.get("geodesic");
+ if (geodesic != null) {
+ sink.setGeodesic(toBoolean(geodesic));
+ }
+ final Object visible = data.get("visible");
+ if (visible != null) {
+ sink.setVisible(toBoolean(visible));
+ }
+ final Object fillColor = data.get("fillColor");
+ if (fillColor != null) {
+ sink.setFillColor(toInt(fillColor));
+ }
+ final Object strokeColor = data.get("strokeColor");
+ if (strokeColor != null) {
+ sink.setStrokeColor(toInt(strokeColor));
+ }
+ final Object strokeWidth = data.get("strokeWidth");
+ if (strokeWidth != null) {
+ sink.setStrokeWidth(toInt(strokeWidth));
+ }
+ final Object zIndex = data.get("zIndex");
+ if (zIndex != null) {
+ sink.setZIndex(toFloat(zIndex));
+ }
+ final Object points = data.get("points");
+ if (points != null) {
+ sink.setPoints(toPoints(points));
+ }
+ final String polygonId = (String) data.get("polygonId");
+ if (polygonId == null) {
+ throw new IllegalArgumentException("polygonId was null");
+ } else {
+ return polygonId;
+ }
+ }
+
static String interpretPolylineOptions(Object o, PolylineOptionsSink sink) {
final Map<?, ?> data = toMap(o);
final Object consumeTapEvents = data.get("consumeTapEvents");
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
index bb3d750..49bad46 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java
@@ -17,6 +17,7 @@
private boolean myLocationEnabled = false;
private boolean myLocationButtonEnabled = false;
private Object initialMarkers;
+ private Object initialPolygons;
private Object initialPolylines;
private Object initialCircles;
@@ -29,6 +30,7 @@
controller.setMyLocationButtonEnabled(myLocationButtonEnabled);
controller.setTrackCameraPosition(trackCameraPosition);
controller.setInitialMarkers(initialMarkers);
+ controller.setInitialPolygons(initialPolygons);
controller.setInitialPolylines(initialPolylines);
controller.setInitialCircles(initialCircles);
return controller;
@@ -104,6 +106,11 @@
}
@Override
+ public void setInitialPolygons(Object initialPolygons) {
+ this.initialPolygons = initialPolygons;
+ }
+
+ @Override
public void setInitialPolylines(Object initialPolylines) {
this.initialPolylines = initialPolylines;
}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
index 338f33a..31f532a 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -30,6 +30,7 @@
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.Polygon;
import com.google.android.gms.maps.model.Polyline;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
@@ -50,6 +51,7 @@
GoogleMap.OnCameraMoveStartedListener,
GoogleMap.OnInfoWindowClickListener,
GoogleMap.OnMarkerClickListener,
+ GoogleMap.OnPolygonClickListener,
GoogleMap.OnPolylineClickListener,
GoogleMap.OnCircleClickListener,
GoogleMapOptionsSink,
@@ -75,9 +77,11 @@
private final int registrarActivityHashCode;
private final Context context;
private final MarkersController markersController;
+ private final PolygonsController polygonsController;
private final PolylinesController polylinesController;
private final CirclesController circlesController;
private List<Object> initialMarkers;
+ private List<Object> initialPolygons;
private List<Object> initialPolylines;
private List<Object> initialCircles;
@@ -98,6 +102,7 @@
methodChannel.setMethodCallHandler(this);
this.registrarActivityHashCode = registrar.activity().hashCode();
this.markersController = new MarkersController(methodChannel);
+ this.polygonsController = new PolygonsController(methodChannel);
this.polylinesController = new PolylinesController(methodChannel);
this.circlesController = new CirclesController(methodChannel);
}
@@ -169,15 +174,18 @@
googleMap.setOnCameraMoveListener(this);
googleMap.setOnCameraIdleListener(this);
googleMap.setOnMarkerClickListener(this);
+ googleMap.setOnPolygonClickListener(this);
googleMap.setOnPolylineClickListener(this);
googleMap.setOnCircleClickListener(this);
googleMap.setOnMapClickListener(this);
googleMap.setOnMapLongClickListener(this);
updateMyLocationSettings();
markersController.setGoogleMap(googleMap);
+ polygonsController.setGoogleMap(googleMap);
polylinesController.setGoogleMap(googleMap);
circlesController.setGoogleMap(googleMap);
updateInitialMarkers();
+ updateInitialPolygons();
updateInitialPolylines();
updateInitialCircles();
}
@@ -238,6 +246,17 @@
result.success(null);
break;
}
+ case "polygons#update":
+ {
+ Object polygonsToAdd = call.argument("polygonsToAdd");
+ polygonsController.addPolygons((List<Object>) polygonsToAdd);
+ Object polygonsToChange = call.argument("polygonsToChange");
+ polygonsController.changePolygons((List<Object>) polygonsToChange);
+ Object polygonIdsToRemove = call.argument("polygonIdsToRemove");
+ polygonsController.removePolygons((List<Object>) polygonIdsToRemove);
+ result.success(null);
+ break;
+ }
case "polylines#update":
{
Object polylinesToAdd = call.argument("polylinesToAdd");
@@ -351,6 +370,11 @@
}
@Override
+ public void onPolygonClick(Polygon polygon) {
+ polygonsController.onPolygonTap(polygon.getId());
+ }
+
+ @Override
public void onPolylineClick(Polyline polyline) {
polylinesController.onPolylineTap(polyline.getId());
}
@@ -515,6 +539,18 @@
}
@Override
+ public void setInitialPolygons(Object initialPolygons) {
+ this.initialPolygons = (List<Object>) initialPolygons;
+ if (googleMap != null) {
+ updateInitialPolygons();
+ }
+ }
+
+ private void updateInitialPolygons() {
+ polygonsController.addPolygons(initialPolygons);
+ }
+
+ @Override
public void setInitialPolylines(Object initialPolylines) {
this.initialPolylines = (List<Object>) initialPolylines;
if (googleMap != null) {
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
index 1e1082a..bc19fa5 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java
@@ -35,6 +35,9 @@
if (params.containsKey("markersToAdd")) {
builder.setInitialMarkers(params.get("markersToAdd"));
}
+ if (params.containsKey("polygonsToAdd")) {
+ builder.setInitialPolygons(params.get("polygonsToAdd"));
+ }
if (params.containsKey("polylinesToAdd")) {
builder.setInitialPolylines(params.get("polylinesToAdd"));
}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
index 347b85d..5e11eb2 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java
@@ -32,6 +32,8 @@
void setInitialMarkers(Object initialMarkers);
+ void setInitialPolygons(Object initialPolygons);
+
void setInitialPolylines(Object initialPolylines);
void setInitialCircles(Object initialCircles);
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java
new file mode 100644
index 0000000..68c3586
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java
@@ -0,0 +1,63 @@
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.PolygonOptions;
+import java.util.List;
+
+class PolygonBuilder implements PolygonOptionsSink {
+ private final PolygonOptions polygonOptions;
+ private boolean consumeTapEvents;
+
+ PolygonBuilder() {
+ this.polygonOptions = new PolygonOptions();
+ }
+
+ PolygonOptions build() {
+ return polygonOptions;
+ }
+
+ boolean consumeTapEvents() {
+ return consumeTapEvents;
+ }
+
+ @Override
+ public void setFillColor(int color) {
+ polygonOptions.fillColor(color);
+ }
+
+ @Override
+ public void setStrokeColor(int color) {
+ polygonOptions.strokeColor(color);
+ }
+
+ @Override
+ public void setPoints(List<LatLng> points) {
+ polygonOptions.addAll(points);
+ }
+
+ @Override
+ public void setConsumeTapEvents(boolean consumeTapEvents) {
+ this.consumeTapEvents = consumeTapEvents;
+ polygonOptions.clickable(consumeTapEvents);
+ }
+
+ @Override
+ public void setGeodesic(boolean geodisc) {
+ polygonOptions.geodesic(geodisc);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ polygonOptions.visible(visible);
+ }
+
+ @Override
+ public void setStrokeWidth(float width) {
+ polygonOptions.strokeWidth(width);
+ }
+
+ @Override
+ public void setZIndex(float zIndex) {
+ polygonOptions.zIndex(zIndex);
+ }
+}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java
new file mode 100644
index 0000000..d77c869
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java
@@ -0,0 +1,71 @@
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Polygon;
+import java.util.List;
+
+/** Controller of a single Polygon on the map. */
+class PolygonController implements PolygonOptionsSink {
+ private final Polygon polygon;
+ private final String googleMapsPolygonId;
+ private boolean consumeTapEvents;
+
+ PolygonController(Polygon polygon, boolean consumeTapEvents) {
+ this.polygon = polygon;
+ this.consumeTapEvents = consumeTapEvents;
+ this.googleMapsPolygonId = polygon.getId();
+ }
+
+ void remove() {
+ polygon.remove();
+ }
+
+ @Override
+ public void setConsumeTapEvents(boolean consumeTapEvents) {
+ this.consumeTapEvents = consumeTapEvents;
+ polygon.setClickable(consumeTapEvents);
+ }
+
+ @Override
+ public void setFillColor(int color) {
+ polygon.setFillColor(color);
+ }
+
+ @Override
+ public void setStrokeColor(int color) {
+ polygon.setStrokeColor(color);
+ }
+
+ @Override
+ public void setGeodesic(boolean geodesic) {
+ polygon.setGeodesic(geodesic);
+ }
+
+ @Override
+ public void setPoints(List<LatLng> points) {
+ polygon.setPoints(points);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ polygon.setVisible(visible);
+ }
+
+ @Override
+ public void setStrokeWidth(float width) {
+ polygon.setStrokeWidth(width);
+ }
+
+ @Override
+ public void setZIndex(float zIndex) {
+ polygon.setZIndex(zIndex);
+ }
+
+ String getGoogleMapsPolygonId() {
+ return googleMapsPolygonId;
+ }
+
+ boolean consumeTapEvents() {
+ return consumeTapEvents;
+ }
+}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java
new file mode 100644
index 0000000..7abbcfa
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java
@@ -0,0 +1,24 @@
+package io.flutter.plugins.googlemaps;
+
+import com.google.android.gms.maps.model.LatLng;
+import java.util.List;
+
+/** Receiver of Polygon configuration options. */
+interface PolygonOptionsSink {
+
+ void setConsumeTapEvents(boolean consumetapEvents);
+
+ void setFillColor(int color);
+
+ void setStrokeColor(int color);
+
+ void setGeodesic(boolean geodesic);
+
+ void setPoints(List<LatLng> points);
+
+ void setVisible(boolean visible);
+
+ void setStrokeWidth(float width);
+
+ void setZIndex(float zIndex);
+}
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java
new file mode 100644
index 0000000..992e866
--- /dev/null
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java
@@ -0,0 +1,112 @@
+// Copyright 2019 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.googlemaps;
+
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.Polygon;
+import com.google.android.gms.maps.model.PolygonOptions;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class PolygonsController {
+
+ private final Map<String, PolygonController> polygonIdToController;
+ private final Map<String, String> googleMapsPolygonIdToDartPolygonId;
+ private final MethodChannel methodChannel;
+ private GoogleMap googleMap;
+
+ PolygonsController(MethodChannel methodChannel) {
+ this.polygonIdToController = new HashMap<>();
+ this.googleMapsPolygonIdToDartPolygonId = new HashMap<>();
+ this.methodChannel = methodChannel;
+ }
+
+ void setGoogleMap(GoogleMap googleMap) {
+ this.googleMap = googleMap;
+ }
+
+ void addPolygons(List<Object> polygonsToAdd) {
+ if (polygonsToAdd != null) {
+ for (Object polygonToAdd : polygonsToAdd) {
+ addPolygon(polygonToAdd);
+ }
+ }
+ }
+
+ void changePolygons(List<Object> polygonsToChange) {
+ if (polygonsToChange != null) {
+ for (Object polygonToChange : polygonsToChange) {
+ changePolygon(polygonToChange);
+ }
+ }
+ }
+
+ void removePolygons(List<Object> polygonIdsToRemove) {
+ if (polygonIdsToRemove == null) {
+ return;
+ }
+ for (Object rawPolygonId : polygonIdsToRemove) {
+ if (rawPolygonId == null) {
+ continue;
+ }
+ String polygonId = (String) rawPolygonId;
+ final PolygonController polygonController = polygonIdToController.remove(polygonId);
+ if (polygonController != null) {
+ polygonController.remove();
+ googleMapsPolygonIdToDartPolygonId.remove(polygonController.getGoogleMapsPolygonId());
+ }
+ }
+ }
+
+ boolean onPolygonTap(String googlePolygonId) {
+ String polygonId = googleMapsPolygonIdToDartPolygonId.get(googlePolygonId);
+ if (polygonId == null) {
+ return false;
+ }
+ methodChannel.invokeMethod("polygon#onTap", Convert.polygonIdToJson(polygonId));
+ PolygonController polygonController = polygonIdToController.get(polygonId);
+ if (polygonController != null) {
+ return polygonController.consumeTapEvents();
+ }
+ return false;
+ }
+
+ private void addPolygon(Object polygon) {
+ if (polygon == null) {
+ return;
+ }
+ PolygonBuilder polygonBuilder = new PolygonBuilder();
+ String polygonId = Convert.interpretPolygonOptions(polygon, polygonBuilder);
+ PolygonOptions options = polygonBuilder.build();
+ addPolygon(polygonId, options, polygonBuilder.consumeTapEvents());
+ }
+
+ private void addPolygon(
+ String polygonId, PolygonOptions polygonOptions, boolean consumeTapEvents) {
+ final Polygon polygon = googleMap.addPolygon(polygonOptions);
+ PolygonController controller = new PolygonController(polygon, consumeTapEvents);
+ polygonIdToController.put(polygonId, controller);
+ googleMapsPolygonIdToDartPolygonId.put(polygon.getId(), polygonId);
+ }
+
+ private void changePolygon(Object polygon) {
+ if (polygon == null) {
+ return;
+ }
+ String polygonId = getPolygonId(polygon);
+ PolygonController polygonController = polygonIdToController.get(polygonId);
+ if (polygonController != null) {
+ Convert.interpretPolygonOptions(polygon, polygonController);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static String getPolygonId(Object polygon) {
+ Map<String, Object> polygonMap = (Map<String, Object>) polygon;
+ return (String) polygonMap.get("polygonId");
+ }
+}
diff --git a/packages/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/example/lib/main.dart
index 5a8d28c..1d17f13 100644
--- a/packages/google_maps_flutter/example/lib/main.dart
+++ b/packages/google_maps_flutter/example/lib/main.dart
@@ -12,6 +12,7 @@
import 'page.dart';
import 'place_circle.dart';
import 'place_marker.dart';
+import 'place_polygon.dart';
import 'place_polyline.dart';
import 'scrolling_map.dart';
@@ -25,6 +26,7 @@
MarkerIconsPage(),
ScrollingMapPage(),
PlacePolylinePage(),
+ PlacePolygonPage(),
PlaceCirclePage(),
];
diff --git a/packages/google_maps_flutter/example/lib/place_polygon.dart b/packages/google_maps_flutter/example/lib/place_polygon.dart
new file mode 100644
index 0000000..1e48dfe
--- /dev/null
+++ b/packages/google_maps_flutter/example/lib/place_polygon.dart
@@ -0,0 +1,235 @@
+import 'package:flutter/material.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+import 'page.dart';
+
+class PlacePolygonPage extends Page {
+ PlacePolygonPage() : super(const Icon(Icons.linear_scale), 'Place polygon');
+
+ @override
+ Widget build(BuildContext context) {
+ return const PlacePolygonBody();
+ }
+}
+
+class PlacePolygonBody extends StatefulWidget {
+ const PlacePolygonBody();
+
+ @override
+ State<StatefulWidget> createState() => PlacePolygonBodyState();
+}
+
+class PlacePolygonBodyState extends State<PlacePolygonBody> {
+ PlacePolygonBodyState();
+
+ GoogleMapController controller;
+ Map<PolygonId, Polygon> polygons = <PolygonId, Polygon>{};
+ int _polygonIdCounter = 1;
+ PolygonId selectedPolygon;
+
+ // Values when toggling polygon color
+ int strokeColorsIndex = 0;
+ int fillColorsIndex = 0;
+ List<Color> colors = <Color>[
+ Colors.purple,
+ Colors.red,
+ Colors.green,
+ Colors.pink,
+ ];
+
+ // Values when toggling polygon width
+ int widthsIndex = 0;
+ List<int> widths = <int>[10, 20, 5];
+
+ void _onMapCreated(GoogleMapController controller) {
+ this.controller = controller;
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ }
+
+ void _onPolygonTapped(PolygonId polygonId) {
+ setState(() {
+ selectedPolygon = polygonId;
+ });
+ }
+
+ void _remove() {
+ setState(() {
+ if (polygons.containsKey(selectedPolygon)) {
+ polygons.remove(selectedPolygon);
+ }
+ selectedPolygon = null;
+ });
+ }
+
+ void _add() {
+ final int polygonCount = polygons.length;
+
+ if (polygonCount == 12) {
+ return;
+ }
+
+ final String polygonIdVal = 'polygon_id_$_polygonIdCounter';
+ _polygonIdCounter++;
+ final PolygonId polygonId = PolygonId(polygonIdVal);
+
+ final Polygon polygon = Polygon(
+ polygonId: polygonId,
+ consumeTapEvents: true,
+ strokeColor: Colors.orange,
+ strokeWidth: 5,
+ fillColor: Colors.green,
+ points: _createPoints(),
+ onTap: () {
+ _onPolygonTapped(polygonId);
+ },
+ );
+
+ setState(() {
+ polygons[polygonId] = polygon;
+ });
+ }
+
+ void _toggleGeodesic() {
+ final Polygon polygon = polygons[selectedPolygon];
+ setState(() {
+ polygons[selectedPolygon] = polygon.copyWith(
+ geodesicParam: !polygon.geodesic,
+ );
+ });
+ }
+
+ void _toggleVisible() {
+ final Polygon polygon = polygons[selectedPolygon];
+ setState(() {
+ polygons[selectedPolygon] = polygon.copyWith(
+ visibleParam: !polygon.visible,
+ );
+ });
+ }
+
+ void _changeStrokeColor() {
+ final Polygon polygon = polygons[selectedPolygon];
+ setState(() {
+ polygons[selectedPolygon] = polygon.copyWith(
+ strokeColorParam: colors[++strokeColorsIndex % colors.length],
+ );
+ });
+ }
+
+ void _changeFillColor() {
+ final Polygon polygon = polygons[selectedPolygon];
+ setState(() {
+ polygons[selectedPolygon] = polygon.copyWith(
+ fillColorParam: colors[++fillColorsIndex % colors.length],
+ );
+ });
+ }
+
+ void _changeWidth() {
+ final Polygon polygon = polygons[selectedPolygon];
+ setState(() {
+ polygons[selectedPolygon] = polygon.copyWith(
+ strokeWidthParam: widths[++widthsIndex % widths.length],
+ );
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ Center(
+ child: SizedBox(
+ width: 350.0,
+ height: 300.0,
+ child: GoogleMap(
+ initialCameraPosition: const CameraPosition(
+ target: LatLng(52.4478, -3.5402),
+ zoom: 7.0,
+ ),
+ polygons: Set<Polygon>.of(polygons.values),
+ onMapCreated: _onMapCreated,
+ ),
+ ),
+ ),
+ Expanded(
+ child: SingleChildScrollView(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: <Widget>[
+ Row(
+ children: <Widget>[
+ Column(
+ children: <Widget>[
+ FlatButton(
+ child: const Text('add'),
+ onPressed: _add,
+ ),
+ FlatButton(
+ child: const Text('remove'),
+ onPressed: (selectedPolygon == null) ? null : _remove,
+ ),
+ FlatButton(
+ child: const Text('toggle visible'),
+ onPressed:
+ (selectedPolygon == null) ? null : _toggleVisible,
+ ),
+ FlatButton(
+ child: const Text('toggle geodesic'),
+ onPressed: (selectedPolygon == null)
+ ? null
+ : _toggleGeodesic,
+ ),
+ ],
+ ),
+ Column(
+ children: <Widget>[
+ FlatButton(
+ child: const Text('change stroke width'),
+ onPressed:
+ (selectedPolygon == null) ? null : _changeWidth,
+ ),
+ FlatButton(
+ child: const Text('change stroke color'),
+ onPressed: (selectedPolygon == null)
+ ? null
+ : _changeStrokeColor,
+ ),
+ FlatButton(
+ child: const Text('change fill color'),
+ onPressed: (selectedPolygon == null)
+ ? null
+ : _changeFillColor,
+ ),
+ ],
+ )
+ ],
+ )
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ List<LatLng> _createPoints() {
+ final List<LatLng> points = <LatLng>[];
+ final double offset = _polygonIdCounter.ceilToDouble();
+ points.add(_createLatLng(51.2395 + offset, -3.4314));
+ points.add(_createLatLng(53.5234 + offset, -3.5314));
+ points.add(_createLatLng(52.4351 + offset, -4.5235));
+ points.add(_createLatLng(52.1231 + offset, -5.0829));
+ return points;
+ }
+
+ LatLng _createLatLng(double lat, double lng) {
+ return LatLng(lat, lng);
+ }
+}
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
index 60e1d42..8c39537 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
@@ -6,6 +6,7 @@
#import <GoogleMaps/GoogleMaps.h>
#import "GoogleMapCircleController.h"
#import "GoogleMapMarkerController.h"
+#import "GoogleMapPolygonController.h"
#import "GoogleMapPolylineController.h"
// Defines map UI options writable from Flutter.
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
index 5dd1d25..fa49638 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
@@ -54,6 +54,7 @@
// https://github.com/flutter/flutter/issues/27550
BOOL _cameraDidInitialSetup;
FLTMarkersController* _markersController;
+ FLTPolygonsController* _polygonsController;
FLTPolylinesController* _polylinesController;
FLTCirclesController* _circlesController;
}
@@ -86,6 +87,9 @@
_markersController = [[FLTMarkersController alloc] init:_channel
mapView:_mapView
registrar:registrar];
+ _polygonsController = [[FLTPolygonsController alloc] init:_channel
+ mapView:_mapView
+ registrar:registrar];
_polylinesController = [[FLTPolylinesController alloc] init:_channel
mapView:_mapView
registrar:registrar];
@@ -96,6 +100,10 @@
if ([markersToAdd isKindOfClass:[NSArray class]]) {
[_markersController addMarkers:markersToAdd];
}
+ id polygonsToAdd = args[@"polygonToAdd"];
+ if ([polygonsToAdd isKindOfClass:[NSArray class]]) {
+ [_polygonsController addPolygons:polygonsToAdd];
+ }
id polylinesToAdd = args[@"polylinesToAdd"];
if ([polylinesToAdd isKindOfClass:[NSArray class]]) {
[_polylinesController addPolylines:polylinesToAdd];
@@ -155,6 +163,20 @@
[_markersController removeMarkerIds:markerIdsToRemove];
}
result(nil);
+ } else if ([call.method isEqualToString:@"polygons#update"]) {
+ id polygonsToAdd = call.arguments[@"polygonsToAdd"];
+ if ([polygonsToAdd isKindOfClass:[NSArray class]]) {
+ [_polygonsController addPolygons:polygonsToAdd];
+ }
+ id polygonsToChange = call.arguments[@"polygonsToChange"];
+ if ([polygonsToChange isKindOfClass:[NSArray class]]) {
+ [_polygonsController changePolygons:polygonsToChange];
+ }
+ id polygonIdsToRemove = call.arguments[@"polygonIdsToRemove"];
+ if ([polygonIdsToRemove isKindOfClass:[NSArray class]]) {
+ [_polygonsController removePolygonIds:polygonIdsToRemove];
+ }
+ result(nil);
} else if ([call.method isEqualToString:@"polylines#update"]) {
id polylinesToAdd = call.arguments[@"polylinesToAdd"];
if ([polylinesToAdd isKindOfClass:[NSArray class]]) {
@@ -325,6 +347,8 @@
NSString* overlayId = overlay.userData[0];
if ([_polylinesController hasPolylineWithId:overlayId]) {
[_polylinesController onPolylineTap:overlayId];
+ } else if ([_polygonsController hasPolygonWithId:overlayId]) {
+ [_polygonsController onPolygonTap:overlayId];
} else if ([_circlesController hasCircleWithId:overlayId]) {
[_circlesController onCircleTap:overlayId];
}
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h
new file mode 100644
index 0000000..c7613fd
--- /dev/null
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h
@@ -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.
+
+#import <Flutter/Flutter.h>
+#import <GoogleMaps/GoogleMaps.h>
+
+// Defines polygon UI options writable from Flutter.
+@protocol FLTGoogleMapPolygonOptionsSink
+- (void)setConsumeTapEvents:(BOOL)consume;
+- (void)setVisible:(BOOL)visible;
+- (void)setFillColor:(UIColor*)color;
+- (void)setStrokeColor:(UIColor*)color;
+- (void)setStrokeWidth:(CGFloat)width;
+- (void)setPoints:(NSArray<CLLocation*>*)points;
+- (void)setZIndex:(int)zIndex;
+@end
+
+// Defines polygon controllable by Flutter.
+@interface FLTGoogleMapPolygonController : NSObject <FLTGoogleMapPolygonOptionsSink>
+@property(atomic, readonly) NSString* polygonId;
+- (instancetype)initPolygonWithPath:(GMSMutablePath*)path
+ polygonId:(NSString*)polygonId
+ mapView:(GMSMapView*)mapView;
+- (void)removePolygon;
+@end
+
+@interface FLTPolygonsController : NSObject
+- (instancetype)init:(FlutterMethodChannel*)methodChannel
+ mapView:(GMSMapView*)mapView
+ registrar:(NSObject<FlutterPluginRegistrar>*)registrar;
+- (void)addPolygons:(NSArray*)polygonsToAdd;
+- (void)changePolygons:(NSArray*)polygonsToChange;
+- (void)removePolygonIds:(NSArray*)polygonIdsToRemove;
+- (void)onPolygonTap:(NSString*)polygonId;
+- (bool)hasPolygonWithId:(NSString*)polygonId;
+@end
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m
new file mode 100644
index 0000000..4bc4f17
--- /dev/null
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m
@@ -0,0 +1,189 @@
+// 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.
+
+#import "GoogleMapPolygonController.h"
+#import "JsonConversions.h"
+
+@implementation FLTGoogleMapPolygonController {
+ GMSPolygon* _polygon;
+ GMSMapView* _mapView;
+}
+- (instancetype)initPolygonWithPath:(GMSMutablePath*)path
+ polygonId:(NSString*)polygonId
+ mapView:(GMSMapView*)mapView {
+ self = [super init];
+ if (self) {
+ _polygon = [GMSPolygon polygonWithPath:path];
+ _mapView = mapView;
+ _polygonId = polygonId;
+ _polygon.userData = @[ polygonId ];
+ }
+ return self;
+}
+
+- (void)removePolygon {
+ _polygon.map = nil;
+}
+
+#pragma mark - FLTGoogleMapPolygonOptionsSink methods
+
+- (void)setConsumeTapEvents:(BOOL)consumes {
+ _polygon.tappable = consumes;
+}
+- (void)setVisible:(BOOL)visible {
+ _polygon.map = visible ? _mapView : nil;
+}
+- (void)setZIndex:(int)zIndex {
+ _polygon.zIndex = zIndex;
+}
+- (void)setPoints:(NSArray<CLLocation*>*)points {
+ GMSMutablePath* path = [GMSMutablePath path];
+
+ for (CLLocation* location in points) {
+ [path addCoordinate:location.coordinate];
+ }
+ _polygon.path = path;
+}
+
+- (void)setFillColor:(UIColor*)color {
+ _polygon.fillColor = color;
+}
+- (void)setStrokeColor:(UIColor*)color {
+ _polygon.strokeColor = color;
+}
+- (void)setStrokeWidth:(CGFloat)width {
+ _polygon.strokeWidth = width;
+}
+@end
+
+static int ToInt(NSNumber* data) { return [FLTGoogleMapJsonConversions toInt:data]; }
+
+static BOOL ToBool(NSNumber* data) { return [FLTGoogleMapJsonConversions toBool:data]; }
+
+static NSArray<CLLocation*>* ToPoints(NSArray* data) {
+ return [FLTGoogleMapJsonConversions toPoints:data];
+}
+
+static UIColor* ToColor(NSNumber* data) { return [FLTGoogleMapJsonConversions toColor:data]; }
+
+static void InterpretPolygonOptions(NSDictionary* data, id<FLTGoogleMapPolygonOptionsSink> sink,
+ NSObject<FlutterPluginRegistrar>* registrar) {
+ NSNumber* consumeTapEvents = data[@"consumeTapEvents"];
+ if (consumeTapEvents) {
+ [sink setConsumeTapEvents:ToBool(consumeTapEvents)];
+ }
+
+ NSNumber* visible = data[@"visible"];
+ if (visible) {
+ [sink setVisible:ToBool(visible)];
+ }
+
+ NSNumber* zIndex = data[@"zIndex"];
+ if (zIndex) {
+ [sink setZIndex:ToInt(zIndex)];
+ }
+
+ NSArray* points = data[@"points"];
+ if (points) {
+ [sink setPoints:ToPoints(points)];
+ }
+
+ NSNumber* fillColor = data[@"fillColor"];
+ if (fillColor) {
+ [sink setFillColor:ToColor(fillColor)];
+ }
+
+ NSNumber* strokeColor = data[@"strokeColor"];
+ if (strokeColor) {
+ [sink setStrokeColor:ToColor(strokeColor)];
+ }
+
+ NSNumber* strokeWidth = data[@"strokeWidth"];
+ if (strokeWidth) {
+ [sink setStrokeWidth:ToInt(strokeWidth)];
+ }
+}
+
+@implementation FLTPolygonsController {
+ NSMutableDictionary* _polygonIdToController;
+ FlutterMethodChannel* _methodChannel;
+ NSObject<FlutterPluginRegistrar>* _registrar;
+ GMSMapView* _mapView;
+}
+- (instancetype)init:(FlutterMethodChannel*)methodChannel
+ mapView:(GMSMapView*)mapView
+ registrar:(NSObject<FlutterPluginRegistrar>*)registrar {
+ self = [super init];
+ if (self) {
+ _methodChannel = methodChannel;
+ _mapView = mapView;
+ _polygonIdToController = [NSMutableDictionary dictionaryWithCapacity:1];
+ _registrar = registrar;
+ }
+ return self;
+}
+- (void)addPolygons:(NSArray*)polygonsToAdd {
+ for (NSDictionary* polygon in polygonsToAdd) {
+ GMSMutablePath* path = [FLTPolygonsController getPath:polygon];
+ NSString* polygonId = [FLTPolygonsController getPolygonId:polygon];
+ FLTGoogleMapPolygonController* controller =
+ [[FLTGoogleMapPolygonController alloc] initPolygonWithPath:path
+ polygonId:polygonId
+ mapView:_mapView];
+ InterpretPolygonOptions(polygon, controller, _registrar);
+ _polygonIdToController[polygonId] = controller;
+ }
+}
+- (void)changePolygons:(NSArray*)polygonsToChange {
+ for (NSDictionary* polygon in polygonsToChange) {
+ NSString* polygonId = [FLTPolygonsController getPolygonId:polygon];
+ FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId];
+ if (!controller) {
+ continue;
+ }
+ InterpretPolygonOptions(polygon, controller, _registrar);
+ }
+}
+- (void)removePolygonIds:(NSArray*)polygonIdsToRemove {
+ for (NSString* polygonId in polygonIdsToRemove) {
+ if (!polygonId) {
+ continue;
+ }
+ FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId];
+ if (!controller) {
+ continue;
+ }
+ [controller removePolygon];
+ [_polygonIdToController removeObjectForKey:polygonId];
+ }
+}
+- (void)onPolygonTap:(NSString*)polygonId {
+ if (!polygonId) {
+ return;
+ }
+ FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId];
+ if (!controller) {
+ return;
+ }
+ [_methodChannel invokeMethod:@"polygon#onTap" arguments:@{@"polygonId" : polygonId}];
+}
+- (bool)hasPolygonWithId:(NSString*)polygonId {
+ if (!polygonId) {
+ return false;
+ }
+ return _polygonIdToController[polygonId] != nil;
+}
++ (GMSMutablePath*)getPath:(NSDictionary*)polygon {
+ NSArray* pointArray = polygon[@"points"];
+ NSArray<CLLocation*>* points = ToPoints(pointArray);
+ GMSMutablePath* path = [GMSMutablePath path];
+ for (CLLocation* location in points) {
+ [path addCoordinate:location.coordinate];
+ }
+ return path;
+}
++ (NSString*)getPolygonId:(NSDictionary*)polygon {
+ return polygon[@"polygonId"];
+}
+@end
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h
index 5b89429..645ace3 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h
@@ -7,6 +7,7 @@
#import "GoogleMapCircleController.h"
#import "GoogleMapController.h"
#import "GoogleMapMarkerController.h"
+#import "GoogleMapPolygonController.h"
#import "GoogleMapPolylineController.h"
@interface FLTGoogleMapsPlugin : NSObject <FlutterPlugin>
diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart
index 14adf45..91f0371 100644
--- a/packages/google_maps_flutter/lib/google_maps_flutter.dart
+++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart
@@ -24,6 +24,8 @@
part 'src/marker_updates.dart';
part 'src/location.dart';
part 'src/pattern_item.dart';
+part 'src/polygon.dart';
+part 'src/polygon_updates.dart';
part 'src/polyline.dart';
part 'src/polyline_updates.dart';
part 'src/circle.dart';
diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart
index 5cc4065..b3f3990 100644
--- a/packages/google_maps_flutter/lib/src/controller.dart
+++ b/packages/google_maps_flutter/lib/src/controller.dart
@@ -66,6 +66,9 @@
case 'polyline#onTap':
_googleMapState.onPolylineTap(call.arguments['polylineId']);
break;
+ case 'polygon#onTap':
+ _googleMapState.onPolygonTap(call.arguments['polygonId']);
+ break;
case 'circle#onTap':
_googleMapState.onCircleTap(call.arguments['circleId']);
break;
@@ -117,6 +120,23 @@
);
}
+ /// Updates polygon configuration.
+ ///
+ /// Change listeners are notified once the update has been made on the
+ /// platform side.
+ ///
+ /// The returned [Future] completes after listeners have been notified.
+ Future<void> _updatePolygons(_PolygonUpdates polygonUpdates) async {
+ assert(polygonUpdates != null);
+ // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
+ // https://github.com/flutter/flutter/issues/26431
+ // ignore: strong_mode_implicit_dynamic_method
+ await channel.invokeMethod(
+ 'polygons#update',
+ polygonUpdates._toMap(),
+ );
+ }
+
/// Updates polyline configuration.
///
/// Change listeners are notified once the update has been made on the
diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart
index 2a2562f..775b94a 100644
--- a/packages/google_maps_flutter/lib/src/google_map.dart
+++ b/packages/google_maps_flutter/lib/src/google_map.dart
@@ -31,6 +31,7 @@
this.myLocationEnabled = false,
this.myLocationButtonEnabled = true,
this.markers,
+ this.polygons,
this.polylines,
this.circles,
this.onCameraMoveStarted,
@@ -75,6 +76,9 @@
/// Markers to be placed on the map.
final Set<Marker> markers;
+ /// Polygons to be placed on the map.
+ final Set<Polygon> polygons;
+
/// Polylines to be placed on the map.
final Set<Polyline> polylines;
@@ -166,6 +170,7 @@
Completer<GoogleMapController>();
Map<MarkerId, Marker> _markers = <MarkerId, Marker>{};
+ Map<PolygonId, Polygon> _polygons = <PolygonId, Polygon>{};
Map<PolylineId, Polyline> _polylines = <PolylineId, Polyline>{};
Map<CircleId, Circle> _circles = <CircleId, Circle>{};
_GoogleMapOptions _googleMapOptions;
@@ -176,6 +181,7 @@
'initialCameraPosition': widget.initialCameraPosition?._toMap(),
'options': _googleMapOptions.toMap(),
'markersToAdd': _serializeMarkerSet(widget.markers),
+ 'polygonsToAdd': _serializePolygonSet(widget.polygons),
'polylinesToAdd': _serializePolylineSet(widget.polylines),
'circlesToAdd': _serializeCircleSet(widget.circles),
};
@@ -206,6 +212,7 @@
super.initState();
_googleMapOptions = _GoogleMapOptions.fromWidget(widget);
_markers = _keyByMarkerId(widget.markers);
+ _polygons = _keyByPolygonId(widget.polygons);
_polylines = _keyByPolylineId(widget.polylines);
_circles = _keyByCircleId(widget.circles);
}
@@ -215,6 +222,7 @@
super.didUpdateWidget(oldWidget);
_updateOptions();
_updateMarkers();
+ _updatePolygons();
_updatePolylines();
_updateCircles();
}
@@ -238,6 +246,13 @@
_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(
@@ -272,6 +287,12 @@
}
}
+ void onPolygonTap(String polygonIdParam) {
+ assert(polygonIdParam != null);
+ final PolygonId polygonId = PolygonId(polygonIdParam);
+ _polygons[polygonId].onTap();
+ }
+
void onPolylineTap(String polylineIdParam) {
assert(polylineIdParam != null);
final PolylineId polylineId = PolylineId(polylineIdParam);
diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart
new file mode 100644
index 0000000..439a5f5
--- /dev/null
+++ b/packages/google_maps_flutter/lib/src/polygon.dart
@@ -0,0 +1,179 @@
+part of google_maps_flutter;
+
+/// Uniquely identifies a [Polygon] among [GoogleMap] polygons.
+///
+/// This does not have to be globally unique, only unique among the list.
+@immutable
+class PolygonId {
+ PolygonId(this.value) : assert(value != null);
+
+ /// value of the [PolygonId].
+ final String value;
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+ if (other.runtimeType != runtimeType) return false;
+ final PolygonId typedOther = other;
+ return value == typedOther.value;
+ }
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() {
+ return 'PolygonId{value: $value}';
+ }
+}
+
+/// Draws a polygon through geographical locations on the map.
+@immutable
+class Polygon {
+ const Polygon({
+ @required this.polygonId,
+ this.consumeTapEvents = false,
+ this.fillColor = Colors.black,
+ this.geodesic = false,
+ this.points = const <LatLng>[],
+ this.strokeColor = Colors.black,
+ this.strokeWidth = 10,
+ this.visible = true,
+ this.zIndex = 0,
+ this.onTap,
+ });
+
+ /// Uniquely identifies a [Polygon].
+ final PolygonId polygonId;
+
+ /// True if the [Polygon] consumes tap events.
+ ///
+ /// If this is false, [onTap] callback will not be triggered.
+ final bool consumeTapEvents;
+
+ /// Fill color in ARGB format, the same format used by Color. The default value is black (0xff000000).
+ final Color fillColor;
+
+ /// Indicates whether the segments of the polygon should be drawn as geodesics, as opposed to straight lines
+ /// on the Mercator projection.
+ ///
+ /// A geodesic is the shortest path between two points on the Earth's surface.
+ /// The geodesic curve is constructed assuming the Earth is a sphere
+ final bool geodesic;
+
+ /// The vertices of the polygon to be drawn.
+ ///
+ /// Line segments are drawn between consecutive points. A polygon is not closed by
+ /// default; to form a closed polygon, the start and end points must be the same.
+ final List<LatLng> points;
+
+ /// True if the marker is visible.
+ final bool visible;
+
+ /// Line color in ARGB format, the same format used by Color. The default value is black (0xff000000).
+ final Color strokeColor;
+
+ /// Width of the polygon, used to define the width of the line to be drawn.
+ ///
+ /// The width is constant and independent of the camera's zoom level.
+ /// The default value is 10.
+ final int strokeWidth;
+
+ /// The z-index of the polygon, used to determine relative drawing order of
+ /// map overlays.
+ ///
+ /// Overlays are drawn in order of z-index, so that lower values means drawn
+ /// earlier, and thus appearing to be closer to the surface of the Earth.
+ final int zIndex;
+
+ /// Callbacks to receive tap events for polygon placed on this map.
+ final VoidCallback onTap;
+
+ /// Creates a new [Polygon] object whose values are the same as this instance,
+ /// unless overwritten by the specified parameters.
+ Polygon copyWith({
+ bool consumeTapEventsParam,
+ Color fillColorParam,
+ bool geodesicParam,
+ List<LatLng> pointsParam,
+ Color strokeColorParam,
+ int strokeWidthParam,
+ bool visibleParam,
+ int zIndexParam,
+ VoidCallback onTapParam,
+ }) {
+ return Polygon(
+ polygonId: polygonId,
+ consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents,
+ fillColor: fillColorParam ?? fillColor,
+ geodesic: geodesicParam ?? geodesic,
+ points: pointsParam ?? points,
+ strokeColor: strokeColorParam ?? strokeColor,
+ strokeWidth: strokeWidthParam ?? strokeWidth,
+ visible: visibleParam ?? visible,
+ onTap: onTapParam ?? onTap,
+ zIndex: zIndexParam ?? zIndex,
+ );
+ }
+
+ dynamic _toJson() {
+ final Map<String, dynamic> json = <String, dynamic>{};
+
+ void addIfPresent(String fieldName, dynamic value) {
+ if (value != null) {
+ json[fieldName] = value;
+ }
+ }
+
+ addIfPresent('polygonId', polygonId.value);
+ addIfPresent('consumeTapEvents', consumeTapEvents);
+ addIfPresent('fillColor', fillColor.value);
+ addIfPresent('geodesic', geodesic);
+ addIfPresent('strokeColor', strokeColor.value);
+ addIfPresent('strokeWidth', strokeWidth);
+ addIfPresent('visible', visible);
+ addIfPresent('zIndex', zIndex);
+
+ if (points != null) {
+ json['points'] = _pointsToJson();
+ }
+
+ return json;
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+ if (other.runtimeType != runtimeType) return false;
+ final Polygon typedOther = other;
+ return polygonId == typedOther.polygonId;
+ }
+
+ @override
+ int get hashCode => polygonId.hashCode;
+
+ dynamic _pointsToJson() {
+ final List<dynamic> result = <dynamic>[];
+ for (final LatLng point in points) {
+ result.add(point._toJson());
+ }
+ return result;
+ }
+}
+
+Map<PolygonId, Polygon> _keyByPolygonId(Iterable<Polygon> polygons) {
+ if (polygons == null) {
+ return <PolygonId, Polygon>{};
+ }
+ return Map<PolygonId, Polygon>.fromEntries(polygons.map((Polygon polygon) =>
+ MapEntry<PolygonId, Polygon>(polygon.polygonId, polygon)));
+}
+
+List<Map<String, dynamic>> _serializePolygonSet(Set<Polygon> polygons) {
+ if (polygons == null) {
+ return null;
+ }
+ return polygons
+ .map<Map<String, dynamic>>((Polygon p) => p._toJson())
+ .toList();
+}
diff --git a/packages/google_maps_flutter/lib/src/polygon_updates.dart b/packages/google_maps_flutter/lib/src/polygon_updates.dart
new file mode 100644
index 0000000..c7a04f4
--- /dev/null
+++ b/packages/google_maps_flutter/lib/src/polygon_updates.dart
@@ -0,0 +1,90 @@
+// 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_maps_flutter;
+
+/// [Polygon] update events to be applied to the [GoogleMap].
+///
+/// Used in [GoogleMapController] when the map is updated.
+class _PolygonUpdates {
+ /// Computes [_PolygonUpdates] given previous and current [Polygon]s.
+ _PolygonUpdates.from(Set<Polygon> previous, Set<Polygon> current) {
+ if (previous == null) {
+ previous = Set<Polygon>.identity();
+ }
+
+ if (current == null) {
+ current = Set<Polygon>.identity();
+ }
+
+ final Map<PolygonId, Polygon> previousPolygons = _keyByPolygonId(previous);
+ final Map<PolygonId, Polygon> currentPolygons = _keyByPolygonId(current);
+
+ final Set<PolygonId> prevPolygonIds = previousPolygons.keys.toSet();
+ final Set<PolygonId> currentPolygonIds = currentPolygons.keys.toSet();
+
+ Polygon idToCurrentPolygon(PolygonId id) {
+ return currentPolygons[id];
+ }
+
+ final Set<PolygonId> _polygonIdsToRemove =
+ prevPolygonIds.difference(currentPolygonIds);
+
+ final Set<Polygon> _polygonsToAdd = currentPolygonIds
+ .difference(prevPolygonIds)
+ .map(idToCurrentPolygon)
+ .toSet();
+
+ final Set<Polygon> _polygonsToChange = currentPolygonIds
+ .intersection(prevPolygonIds)
+ .map(idToCurrentPolygon)
+ .toSet();
+
+ polygonsToAdd = _polygonsToAdd;
+ polygonIdsToRemove = _polygonIdsToRemove;
+ polygonsToChange = _polygonsToChange;
+ }
+
+ Set<Polygon> polygonsToAdd;
+ Set<PolygonId> polygonIdsToRemove;
+ Set<Polygon> polygonsToChange;
+
+ Map<String, dynamic> _toMap() {
+ final Map<String, dynamic> updateMap = <String, dynamic>{};
+
+ void addIfNonNull(String fieldName, dynamic value) {
+ if (value != null) {
+ updateMap[fieldName] = value;
+ }
+ }
+
+ addIfNonNull('polygonsToAdd', _serializePolygonSet(polygonsToAdd));
+ addIfNonNull('polygonsToChange', _serializePolygonSet(polygonsToChange));
+ addIfNonNull('polygonIdsToRemove',
+ polygonIdsToRemove.map<dynamic>((PolygonId m) => m.value).toList());
+
+ return updateMap;
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+ if (other.runtimeType != runtimeType) return false;
+ final _PolygonUpdates typedOther = other;
+ return setEquals(polygonsToAdd, typedOther.polygonsToAdd) &&
+ setEquals(polygonIdsToRemove, typedOther.polygonIdsToRemove) &&
+ setEquals(polygonsToChange, typedOther.polygonsToChange);
+ }
+
+ @override
+ int get hashCode =>
+ hashValues(polygonsToAdd, polygonIdsToRemove, polygonsToChange);
+
+ @override
+ String toString() {
+ return '_PolygonUpdates{polygonsToAdd: $polygonsToAdd, '
+ 'polygonIdsToRemove: $polygonIdsToRemove, '
+ 'polygonsToChange: $polygonsToChange}';
+ }
+}
diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml
index ecf9467..9af1da6 100644
--- a/packages/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
author: Flutter Team <flutter-dev@googlegroups.com>
homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter
-version: 0.5.14+1
+version: 0.5.15
dependencies:
flutter:
diff --git a/packages/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/test/fake_maps_controllers.dart
index 805fa7a..f9dd2c7 100644
--- a/packages/google_maps_flutter/test/fake_maps_controllers.dart
+++ b/packages/google_maps_flutter/test/fake_maps_controllers.dart
@@ -16,6 +16,7 @@
channel.setMockMethodCallHandler(onMethodCall);
updateOptions(params['options']);
updateMarkers(params);
+ updatePolygons(params);
updatePolylines(params);
updateCircles(params);
}
@@ -52,6 +53,12 @@
Set<Marker> markersToChange;
+ Set<PolygonId> polygonIdsToRemove;
+
+ Set<Polygon> polygonsToAdd;
+
+ Set<Polygon> polygonsToChange;
+
Set<PolylineId> polylineIdsToRemove;
Set<Polyline> polylinesToAdd;
@@ -72,6 +79,9 @@
case 'markers#update':
updateMarkers(call.arguments);
return Future<void>.sync(() {});
+ case 'polygons#update':
+ updatePolygons(call.arguments);
+ return Future<void>.sync(() {});
case 'polylines#update':
updatePolylines(call.arguments);
return Future<void>.sync(() {});
@@ -141,6 +151,53 @@
return result;
}
+ void updatePolygons(Map<dynamic, dynamic> polygonUpdates) {
+ if (polygonUpdates == null) {
+ return;
+ }
+ polygonsToAdd = _deserializePolygons(polygonUpdates['polygonsToAdd']);
+ polygonIdsToRemove =
+ _deserializePolygonIds(polygonUpdates['polygonIdsToRemove']);
+ polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']);
+ }
+
+ Set<PolygonId> _deserializePolygonIds(List<dynamic> polygonIds) {
+ if (polygonIds == null) {
+ // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+ // https://github.com/flutter/flutter/issues/28312
+ // ignore: prefer_collection_literals
+ return Set<PolygonId>();
+ }
+ return polygonIds.map((dynamic polygonId) => PolygonId(polygonId)).toSet();
+ }
+
+ Set<Polygon> _deserializePolygons(dynamic polygons) {
+ if (polygons == null) {
+ // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+ // https://github.com/flutter/flutter/issues/28312
+ // ignore: prefer_collection_literals
+ return Set<Polygon>();
+ }
+ final List<dynamic> polygonsData = polygons;
+ // TODO(iskakaushik): Remove this when collection literals makes it to stable.
+ // https://github.com/flutter/flutter/issues/28312
+ // ignore: prefer_collection_literals
+ final Set<Polygon> result = Set<Polygon>();
+ for (Map<dynamic, dynamic> polygonData in polygonsData) {
+ final String polygonId = polygonData['polygonId'];
+ final bool visible = polygonData['visible'];
+ final bool geodesic = polygonData['geodesic'];
+
+ result.add(Polygon(
+ polygonId: PolygonId(polygonId),
+ visible: visible,
+ geodesic: geodesic,
+ ));
+ }
+
+ return result;
+ }
+
void updatePolylines(Map<dynamic, dynamic> polylineUpdates) {
if (polylineUpdates == null) {
return;
diff --git a/packages/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/test/polygon_updates_test.dart
new file mode 100644
index 0000000..f7b2c1c
--- /dev/null
+++ b/packages/google_maps_flutter/test/polygon_updates_test.dart
@@ -0,0 +1,200 @@
+// 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.
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+import 'fake_maps_controllers.dart';
+
+Set<Polygon> _toSet({Polygon p1, Polygon p2, Polygon p3}) {
+ final Set<Polygon> res = Set<Polygon>.identity();
+ if (p1 != null) {
+ res.add(p1);
+ }
+ if (p2 != null) {
+ res.add(p2);
+ }
+ if (p3 != null) {
+ res.add(p3);
+ }
+ return res;
+}
+
+Widget _mapWithPolygons(Set<Polygon> polygons) {
+ return Directionality(
+ textDirection: TextDirection.ltr,
+ child: GoogleMap(
+ initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)),
+ polygons: polygons,
+ ),
+ );
+}
+
+void main() {
+ final FakePlatformViewsController fakePlatformViewsController =
+ FakePlatformViewsController();
+
+ setUpAll(() {
+ SystemChannels.platform_views.setMockMethodCallHandler(
+ fakePlatformViewsController.fakePlatformViewsMethodHandler);
+ });
+
+ setUp(() {
+ fakePlatformViewsController.reset();
+ });
+
+ testWidgets('Initializing a polygon', (WidgetTester tester) async {
+ final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+ expect(platformGoogleMap.polygonsToAdd.length, 1);
+
+ final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first;
+ expect(initializedPolygon, equals(p1));
+ expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+ expect(platformGoogleMap.polygonsToChange.isEmpty, true);
+ });
+
+ testWidgets("Adding a polygon", (WidgetTester tester) async {
+ final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+ final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1, p2: p2)));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+ expect(platformGoogleMap.polygonsToAdd.length, 1);
+
+ final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first;
+ expect(addedPolygon, equals(p2));
+ expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+
+ expect(platformGoogleMap.polygonsToChange.length, 1);
+ expect(platformGoogleMap.polygonsToChange.first, equals(p1));
+ });
+
+ testWidgets("Removing a polygon", (WidgetTester tester) async {
+ final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+ await tester.pumpWidget(_mapWithPolygons(null));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+ expect(platformGoogleMap.polygonIdsToRemove.length, 1);
+ expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId));
+
+ expect(platformGoogleMap.polygonsToChange.isEmpty, true);
+ expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+ });
+
+ testWidgets("Updating a polygon", (WidgetTester tester) async {
+ final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+ final Polygon p2 =
+ Polygon(polygonId: PolygonId("polygon_1"), geodesic: true);
+
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2)));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+ expect(platformGoogleMap.polygonsToChange.length, 1);
+ expect(platformGoogleMap.polygonsToChange.first, equals(p2));
+
+ expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+ expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+ });
+
+ testWidgets("Updating a polygon", (WidgetTester tester) async {
+ final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+ final Polygon p2 =
+ Polygon(polygonId: PolygonId("polygon_1"), geodesic: true);
+
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1)));
+ await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2)));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+ expect(platformGoogleMap.polygonsToChange.length, 1);
+
+ final Polygon update = platformGoogleMap.polygonsToChange.first;
+ expect(update, equals(p2));
+ expect(update.geodesic, true);
+ });
+
+ testWidgets("Multi Update", (WidgetTester tester) async {
+ Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+ Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+ final Set<Polygon> prev = _toSet(p1: p1, p2: p2);
+ p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false);
+ p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true);
+ final Set<Polygon> cur = _toSet(p1: p1, p2: p2);
+
+ await tester.pumpWidget(_mapWithPolygons(prev));
+ await tester.pumpWidget(_mapWithPolygons(cur));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+
+ expect(platformGoogleMap.polygonsToChange, cur);
+ expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+ expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+ });
+
+ testWidgets("Multi Update", (WidgetTester tester) async {
+ Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+ final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3"));
+ final Set<Polygon> prev = _toSet(p2: p2, p3: p3);
+
+ // p1 is added, p2 is updated, p3 is removed.
+ final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+ p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true);
+ final Set<Polygon> cur = _toSet(p1: p1, p2: p2);
+
+ await tester.pumpWidget(_mapWithPolygons(prev));
+ await tester.pumpWidget(_mapWithPolygons(cur));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+
+ expect(platformGoogleMap.polygonsToChange.length, 1);
+ expect(platformGoogleMap.polygonsToAdd.length, 1);
+ expect(platformGoogleMap.polygonIdsToRemove.length, 1);
+
+ expect(platformGoogleMap.polygonsToChange.first, equals(p2));
+ expect(platformGoogleMap.polygonsToAdd.first, equals(p1));
+ expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId));
+ });
+
+ testWidgets(
+ "Partial Update",
+ (WidgetTester tester) async {
+ final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1"));
+ Polygon p2 = Polygon(polygonId: PolygonId("polygon_2"));
+ final Set<Polygon> prev = _toSet(p1: p1, p2: p2);
+ p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true);
+ final Set<Polygon> cur = _toSet(p1: p1, p2: p2);
+
+ await tester.pumpWidget(_mapWithPolygons(prev));
+ await tester.pumpWidget(_mapWithPolygons(cur));
+
+ final FakePlatformGoogleMap platformGoogleMap =
+ fakePlatformViewsController.lastCreatedView;
+
+ expect(platformGoogleMap.polygonsToChange, _toSet(p2: p2));
+ expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true);
+ expect(platformGoogleMap.polygonsToAdd.isEmpty, true);
+ },
+ // The test is currently broken due to a bug (we're updating all polygons
+ // instead of just the ones that were changed):
+ // https://github.com/flutter/flutter/issues/30764
+ // TODO(amirh): enable this test when the issue is fixed.
+ skip: true,
+ );
+}