diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 5215e7a577d..243c79cd75f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.19.0 + +* Adds support for advanced markers. + ## 2.18.4 * Updates Java compatibility version to 17 and minimum supported SDK version to Flutter 3.35/Dart 3.9. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle index e280be37488..4f8d648a15d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle @@ -38,7 +38,7 @@ android { dependencies { implementation("androidx.annotation:annotation:1.9.1") implementation("com.google.android.gms:play-services-maps:19.2.0") - implementation("com.google.maps.android:android-maps-utils:3.6.0") + implementation("com.google.maps.android:android-maps-utils:3.7.0") androidTestImplementation("androidx.test:runner:1.7.0") androidTestImplementation("androidx.test:rules:1.7.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java index 60832bf5a1e..0c25b6dc03f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java @@ -7,15 +7,20 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.AdvancedMarkerOptions; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.maps.android.clustering.Cluster; import com.google.maps.android.clustering.ClusterItem; import com.google.maps.android.clustering.ClusterManager; +import com.google.maps.android.clustering.view.ClusterRenderer; +import com.google.maps.android.clustering.view.DefaultAdvancedMarkersClusterRenderer; import com.google.maps.android.clustering.view.DefaultClusterRenderer; import com.google.maps.android.collections.MarkerManager; import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,10 +34,14 @@ class ClusterManagersController implements GoogleMap.OnCameraIdleListener, ClusterManager.OnClusterClickListener { @NonNull private final Context context; - @NonNull private final HashMap> clusterManagerIdToManager; + + @VisibleForTesting @NonNull + protected final HashMap> clusterManagerIdToManager; + @NonNull private final MapsCallbackApi flutterApi; @Nullable private MarkerManager markerManager; @Nullable private GoogleMap googleMap; + @NonNull private PlatformMarkerType markerType; @Nullable private ClusterManager.OnClusterItemClickListener clusterItemClickListener; @@ -41,10 +50,14 @@ class ClusterManagersController private ClusterManagersController.OnClusterItemRendered clusterItemRenderedListener; - ClusterManagersController(@NonNull MapsCallbackApi flutterApi, Context context) { + ClusterManagersController( + @NonNull MapsCallbackApi flutterApi, + @NonNull Context context, + @NonNull PlatformMarkerType markerType) { this.clusterManagerIdToManager = new HashMap<>(); this.context = context; this.flutterApi = flutterApi; + this.markerType = markerType; } void init(GoogleMap googleMap, MarkerManager markerManager) { @@ -89,11 +102,21 @@ void addClusterManagers(@NonNull List clusterMa void addClusterManager(String clusterManagerId) { ClusterManager clusterManager = new ClusterManager(context, googleMap, markerManager); - ClusterRenderer clusterRenderer = - new ClusterRenderer(context, googleMap, clusterManager, this); + initializeRenderer(clusterManager); + clusterManagerIdToManager.put(clusterManagerId, clusterManager); + } + + /** + * Initializes cluster renderer based on marker type. AdvancedMarkerCluster renderer is used for + * advanced markers and MarkerClusterRenderer is used for default markers. + */ + private void initializeRenderer(ClusterManager clusterManager) { + final ClusterRenderer clusterRenderer = + markerType == PlatformMarkerType.ADVANCED_MARKER + ? new AdvancedMarkerClusterRenderer<>(context, googleMap, clusterManager, this) + : new MarkerClusterRenderer<>(context, googleMap, clusterManager, this); clusterManager.setRenderer(clusterRenderer); initListenersForClusterManager(clusterManager, this, clusterItemClickListener); - clusterManagerIdToManager.put(clusterManagerId, clusterManager); } /** Removes ClusterManagers by given cluster manager IDs from the controller. */ @@ -195,13 +218,14 @@ public boolean onClusterClick(Cluster cluster) { } /** - * ClusterRenderer builds marker options for new markers to be rendered to the map. After cluster - * item (marker) is rendered, it is sent to the listeners for control. + * MarkerClusterRenderer builds marker options for new markers to be rendered to the map. After + * cluster item (marker) is rendered, it is sent to the listeners for control. */ - private static class ClusterRenderer extends DefaultClusterRenderer { + @VisibleForTesting + static class MarkerClusterRenderer extends DefaultClusterRenderer { private final ClusterManagersController clusterManagersController; - public ClusterRenderer( + public MarkerClusterRenderer( Context context, GoogleMap map, ClusterManager clusterManager, @@ -225,6 +249,35 @@ protected void onClusterItemRendered(@NonNull T item, @NonNull Marker marker) { } } + /** AdvancedMarkerClusterRenderer is a ClusterRenderer that supports AdvancedMarkers. */ + @VisibleForTesting + static class AdvancedMarkerClusterRenderer + extends DefaultAdvancedMarkersClusterRenderer { + + private final ClusterManagersController clusterManagersController; + + public AdvancedMarkerClusterRenderer( + Context context, + GoogleMap map, + ClusterManager clusterManager, + ClusterManagersController clusterManagersController) { + super(context, map, clusterManager); + this.clusterManagersController = clusterManagersController; + } + + @Override + protected void onBeforeClusterItemRendered( + @NonNull T item, @NonNull AdvancedMarkerOptions markerOptions) { + item.update(markerOptions); + } + + @Override + protected void onClusterItemRendered(@NonNull T item, @NonNull Marker marker) { + super.onClusterItemRendered(item, marker); + clusterManagersController.onClusterItemRendered(item, marker); + } + } + /** Interface for handling situations where clusterManager adds new visible marker to the map. */ public interface OnClusterItemRendered { void onClusterItemRendered(@NonNull T item, @NonNull Marker marker); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 871f1eb24d7..f151a0b3e12 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -20,6 +20,7 @@ import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.MapsInitializer; +import com.google.android.gms.maps.model.AdvancedMarkerOptions; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.ButtCap; @@ -34,6 +35,7 @@ import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.PatternItem; +import com.google.android.gms.maps.model.PinConfig; import com.google.android.gms.maps.model.RoundCap; import com.google.android.gms.maps.model.SquareCap; import com.google.android.gms.maps.model.Tile; @@ -117,6 +119,10 @@ private static BitmapDescriptor toBitmapDescriptor( Messages.PlatformBitmapBytesMap typedBitmap = (Messages.PlatformBitmapBytesMap) bitmap; return getBitmapFromBytes(typedBitmap, density, wrapper); } + if (bitmap instanceof Messages.PlatformBitmapPinConfig) { + Messages.PlatformBitmapPinConfig pinConfigBitmap = (Messages.PlatformBitmapPinConfig) bitmap; + return getBitmapFromPinConfigBuilder(pinConfigBitmap, assetManager, density, wrapper); + } throw new IllegalArgumentException("PlatformBitmap did not contain a supported subtype."); } @@ -187,6 +193,75 @@ public static BitmapDescriptor getBitmapFromBytes( } } + public static BitmapDescriptor getBitmapFromPinConfigBuilder( + Messages.PlatformBitmapPinConfig pinConfigBitmap, + AssetManager assetManager, + float density, + BitmapDescriptorFactoryWrapper bitmapDescriptorFactory) { + try { + final PinConfig pinConfig = + getPinConfigFromPlatformPinConfig( + pinConfigBitmap, assetManager, density, bitmapDescriptorFactory); + return bitmapDescriptorFactory.fromPinConfig(pinConfig); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to interpret pin config as a valid image.", e); + } + } + + @VisibleForTesting + public static PinConfig getPinConfigFromPlatformPinConfig( + Messages.PlatformBitmapPinConfig pinConfigBitmap, + AssetManager assetManager, + float density, + BitmapDescriptorFactoryWrapper bitmapDescriptorFactory) { + final Integer backgroundColor = + pinConfigBitmap.getBackgroundColor() != null + ? toInt(pinConfigBitmap.getBackgroundColor()) + : null; + final Integer borderColor = + pinConfigBitmap.getBorderColor() != null ? toInt(pinConfigBitmap.getBorderColor()) : null; + final String glyphText = + pinConfigBitmap.getGlyphText() != null ? pinConfigBitmap.getGlyphText() : null; + final Integer glyphTextColor = + pinConfigBitmap.getGlyphTextColor() != null + ? toInt(pinConfigBitmap.getGlyphTextColor()) + : null; + final Integer glyphColor = + pinConfigBitmap.getGlyphColor() != null ? toInt(pinConfigBitmap.getGlyphColor()) : null; + final BitmapDescriptor glyphBitmapDescriptor = + pinConfigBitmap.getGlyphBitmap() != null + ? toBitmapDescriptor( + pinConfigBitmap.getGlyphBitmap(), assetManager, density, bitmapDescriptorFactory) + : null; + + final PinConfig.Builder pinConfigBuilder = PinConfig.builder(); + if (backgroundColor != null) { + pinConfigBuilder.setBackgroundColor(backgroundColor); + } + + if (borderColor != null) { + pinConfigBuilder.setBorderColor(borderColor); + } + + PinConfig.Glyph glyph = null; + if (glyphText != null) { + glyph = + glyphTextColor != null + ? new PinConfig.Glyph(glyphText, glyphTextColor) + : new PinConfig.Glyph(glyphText); + } else if (glyphBitmapDescriptor != null) { + glyph = new PinConfig.Glyph(glyphBitmapDescriptor); + } else if (glyphColor != null) { + glyph = new PinConfig.Glyph(glyphColor); + } + + if (glyph != null) { + pinConfigBuilder.setGlyph(glyph); + } + + return pinConfigBuilder.build(); + } + /** * Creates a BitmapDescriptor object from asset, using given details and density. * @@ -610,6 +685,7 @@ static void interpretMarkerOptions( sink.setRotation(marker.getRotation().floatValue()); sink.setVisible(marker.getVisible()); sink.setZIndex(marker.getZIndex().floatValue()); + sink.setCollisionBehavior(collisionBehaviorFromPigeon(marker.getCollisionBehavior())); } private static void interpretInfoWindowOptions( @@ -648,6 +724,20 @@ static int jointTypeFromPigeon(Messages.PlatformJointType jointType) { return JointType.DEFAULT; } + static int collisionBehaviorFromPigeon( + Messages.PlatformMarkerCollisionBehavior collisionBehavior) { + switch (collisionBehavior) { + case REQUIRED_DISPLAY: + return AdvancedMarkerOptions.CollisionBehavior.REQUIRED; + case OPTIONAL_AND_HIDES_LOWER_PRIORITY: + return AdvancedMarkerOptions.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY; + case REQUIRED_AND_HIDES_OPTIONAL: + return AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL; + default: + return AdvancedMarkerOptions.CollisionBehavior.REQUIRED; + } + } + static String interpretPolylineOptions( Messages.PlatformPolyline polyline, PolylineOptionsSink sink, @@ -1029,6 +1119,11 @@ public BitmapDescriptor fromAsset(String assetKey) { public BitmapDescriptor fromBitmap(Bitmap bitmap) { return BitmapDescriptorFactory.fromBitmap(bitmap); } + + @VisibleForTesting + public BitmapDescriptor fromPinConfig(PinConfig pinConfig) { + return BitmapDescriptorFactory.fromPinConfig(pinConfig); + } } @VisibleForTesting diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index 3b2afd8a9bb..884aa2f4e1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -12,6 +12,7 @@ import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLngBounds; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.util.List; class GoogleMapBuilder implements GoogleMapOptionsSink { @@ -37,9 +38,11 @@ GoogleMapController build( int id, Context context, BinaryMessenger binaryMessenger, - LifecycleProvider lifecycleProvider) { + LifecycleProvider lifecycleProvider, + PlatformMarkerType markerType) { final GoogleMapController controller = - new GoogleMapController(id, context, binaryMessenger, lifecycleProvider, options); + new GoogleMapController( + id, context, binaryMessenger, lifecycleProvider, options, markerType); controller.init(); controller.setMyLocationEnabled(myLocationEnabled); controller.setMyLocationButtonEnabled(myLocationButtonEnabled); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index bd87c923cce..eb830dd4fe3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -35,6 +35,7 @@ import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.MapCapabilities; import com.google.android.gms.maps.model.MapStyleOptions; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.Polygon; @@ -50,6 +51,7 @@ import io.flutter.plugins.googlemaps.Messages.MapsApi; import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi; import io.flutter.plugins.googlemaps.Messages.MapsInspectorApi; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; @@ -116,7 +118,8 @@ class GoogleMapController Context context, BinaryMessenger binaryMessenger, LifecycleProvider lifecycleProvider, - GoogleMapOptions options) { + GoogleMapOptions options, + PlatformMarkerType markerType) { this.id = id; this.context = context; this.options = options; @@ -128,14 +131,15 @@ class GoogleMapController MapsInspectorApi.setUp(binaryMessenger, Integer.toString(id), this); AssetManager assetManager = context.getAssets(); this.lifecycleProvider = lifecycleProvider; - this.clusterManagersController = new ClusterManagersController(flutterApi, context); + this.clusterManagersController = new ClusterManagersController(flutterApi, context, markerType); this.markersController = new MarkersController( flutterApi, clusterManagersController, assetManager, density, - new Convert.BitmapDescriptorFactoryWrapper()); + new Convert.BitmapDescriptorFactoryWrapper(), + markerType); this.polygonsController = new PolygonsController(flutterApi, density); this.polylinesController = new PolylinesController(flutterApi, assetManager, density); this.circlesController = new CirclesController(flutterApi, density); @@ -1026,6 +1030,18 @@ public Boolean isInfoWindowShown(@NonNull String markerId) { return lastSetStyleSucceeded; } + @Override + public @NonNull Boolean isAdvancedMarkersAvailable() { + if (googleMap == null) { + throw new FlutterError( + "GoogleMap uninitialized", + "getMapCapabilities() called prior to map initialization", + null); + } + final MapCapabilities mapCapabilities = googleMap.getMapCapabilities(); + return mapCapabilities.isAdvancedMarkersAvailable(); + } + @Override public void clearTileCache(@NonNull String tileOverlayId) { tileOverlaysController.clearTileCache(tileOverlayId); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index a0a25948efd..424140ce4d9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -48,11 +48,12 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar builder.setInitialTileOverlays(params.getInitialTileOverlays()); builder.setInitialGroundOverlays(params.getInitialGroundOverlays()); - final String cloudMapId = mapConfig.getCloudMapId(); - if (cloudMapId != null) { - builder.setMapId(cloudMapId); + final String mapId = mapConfig.getMapId(); + if (mapId != null && !mapId.isEmpty()) { + builder.setMapId(mapId); } - return builder.build(id, context, binaryMessenger, lifecycleProvider); + return builder.build( + id, context, binaryMessenger, lifecycleProvider, mapConfig.getMarkerType()); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java index 297f5357681..43539c0603a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java @@ -4,10 +4,12 @@ package io.flutter.plugins.googlemaps; +import com.google.android.gms.maps.model.AdvancedMarkerOptions; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MarkerOptions; import com.google.maps.android.clustering.ClusterItem; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; class MarkerBuilder implements MarkerOptionsSink, ClusterItem { private final MarkerOptions markerOptions; @@ -15,8 +17,11 @@ class MarkerBuilder implements MarkerOptionsSink, ClusterItem { private String markerId; private boolean consumeTapEvents; - MarkerBuilder(String markerId, String clusterManagerId) { - this.markerOptions = new MarkerOptions(); + MarkerBuilder(String markerId, String clusterManagerId, PlatformMarkerType markerType) { + this.markerOptions = + markerType == PlatformMarkerType.ADVANCED_MARKER + ? new AdvancedMarkerOptions() + : new MarkerOptions(); this.markerId = markerId; this.clusterManagerId = clusterManagerId; } @@ -115,6 +120,13 @@ public void setZIndex(float zIndex) { markerOptions.zIndex(zIndex); } + @Override + public void setCollisionBehavior(@AdvancedMarkerOptions.CollisionBehavior int collisionBehavior) { + if (markerOptions.getClass() == AdvancedMarkerOptions.class) { + ((AdvancedMarkerOptions) markerOptions).collisionBehavior(collisionBehavior); + } + } + @Override public LatLng getPosition() { return markerOptions.getPosition(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java index e3a88dc926d..0da62f40923 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java @@ -4,6 +4,7 @@ package io.flutter.plugins.googlemaps; +import com.google.android.gms.maps.model.AdvancedMarkerOptions; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; @@ -143,6 +144,10 @@ public void setZIndex(float zIndex) { marker.setZIndex(zIndex); } + @Override + public void setCollisionBehavior( + @AdvancedMarkerOptions.CollisionBehavior int collisionBehavior) {} + String getGoogleMapsMarkerId() { return googleMapsMarkerId; } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java index 6e29c8685a8..36ae5a6014a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java @@ -4,6 +4,7 @@ package io.flutter.plugins.googlemaps; +import com.google.android.gms.maps.model.AdvancedMarkerOptions; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.LatLng; @@ -32,4 +33,6 @@ interface MarkerOptionsSink { void setVisible(boolean visible); void setZIndex(float zIndex); + + void setCollisionBehavior(@AdvancedMarkerOptions.CollisionBehavior int collisionBehavior); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 66c06a4ebb5..9f30d40c23b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -11,6 +11,7 @@ import com.google.android.gms.maps.model.MarkerOptions; import com.google.maps.android.collections.MarkerManager; import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.util.HashMap; import java.util.List; import java.util.Objects; @@ -25,13 +26,15 @@ class MarkersController { private final AssetManager assetManager; private final float density; private final Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; + private PlatformMarkerType markerType; MarkersController( @NonNull MapsCallbackApi flutterApi, ClusterManagersController clusterManagersController, AssetManager assetManager, float density, - Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper) { + Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper, + @NonNull PlatformMarkerType markerType) { this.markerIdToMarkerBuilder = new HashMap<>(); this.markerIdToController = new HashMap<>(); this.googleMapsMarkerIdToDartMarkerId = new HashMap<>(); @@ -40,6 +43,7 @@ class MarkersController { this.assetManager = assetManager; this.density = density; this.bitmapDescriptorFactoryWrapper = bitmapDescriptorFactoryWrapper; + this.markerType = markerType; } void setCollection(MarkerManager.Collection markerCollection) { @@ -174,7 +178,7 @@ public void onClusterItemRendered(MarkerBuilder markerBuilder, Marker marker) { private void addMarker(@NonNull Messages.PlatformMarker marker) { String markerId = marker.getMarkerId(); String clusterManagerId = marker.getClusterManagerId(); - MarkerBuilder markerBuilder = new MarkerBuilder(markerId, clusterManagerId); + MarkerBuilder markerBuilder = new MarkerBuilder(markerId, clusterManagerId, markerType); Convert.interpretMarkerOptions( marker, markerBuilder, assetManager, density, bitmapDescriptorFactoryWrapper); addMarker(markerBuilder); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java index a9d298b900b..6b169468fd7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java @@ -100,6 +100,18 @@ public enum PlatformRendererType { } } + public enum PlatformMarkerCollisionBehavior { + REQUIRED_DISPLAY(0), + OPTIONAL_AND_HIDES_LOWER_PRIORITY(1), + REQUIRED_AND_HIDES_OPTIONAL(2); + + final int index; + + PlatformMarkerCollisionBehavior(final int index) { + this.index = index; + } + } + /** Join types for polyline joints. */ public enum PlatformJointType { MITERED(0), @@ -144,6 +156,17 @@ public enum PlatformPatternItemType { } } + public enum PlatformMarkerType { + MARKER(0), + ADVANCED_MARKER(1); + + final int index; + + PlatformMarkerType(final int index) { + this.index = index; + } + } + /** Pigeon equivalent of [MapBitmapScaling]. */ public enum PlatformMapBitmapScaling { AUTO(0), @@ -1874,6 +1897,19 @@ public void setClusterManagerId(@Nullable String setterArg) { this.clusterManagerId = setterArg; } + private @NonNull PlatformMarkerCollisionBehavior collisionBehavior; + + public @NonNull PlatformMarkerCollisionBehavior getCollisionBehavior() { + return collisionBehavior; + } + + public void setCollisionBehavior(@NonNull PlatformMarkerCollisionBehavior setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"collisionBehavior\" is null."); + } + this.collisionBehavior = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ PlatformMarker() {} @@ -1898,7 +1934,8 @@ public boolean equals(Object o) { && visible.equals(that.visible) && zIndex.equals(that.zIndex) && markerId.equals(that.markerId) - && Objects.equals(clusterManagerId, that.clusterManagerId); + && Objects.equals(clusterManagerId, that.clusterManagerId) + && collisionBehavior.equals(that.collisionBehavior); } @Override @@ -1916,7 +1953,8 @@ public int hashCode() { visible, zIndex, markerId, - clusterManagerId); + clusterManagerId, + collisionBehavior); } public static final class Builder { @@ -2025,6 +2063,15 @@ public static final class Builder { return this; } + private @Nullable PlatformMarkerCollisionBehavior collisionBehavior; + + @CanIgnoreReturnValue + public @NonNull Builder setCollisionBehavior( + @NonNull PlatformMarkerCollisionBehavior setterArg) { + this.collisionBehavior = setterArg; + return this; + } + public @NonNull PlatformMarker build() { PlatformMarker pigeonReturn = new PlatformMarker(); pigeonReturn.setAlpha(alpha); @@ -2040,13 +2087,14 @@ public static final class Builder { pigeonReturn.setZIndex(zIndex); pigeonReturn.setMarkerId(markerId); pigeonReturn.setClusterManagerId(clusterManagerId); + pigeonReturn.setCollisionBehavior(collisionBehavior); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(13); + ArrayList toListResult = new ArrayList<>(14); toListResult.add(alpha); toListResult.add(anchor); toListResult.add(consumeTapEvents); @@ -2060,6 +2108,7 @@ ArrayList toList() { toListResult.add(zIndex); toListResult.add(markerId); toListResult.add(clusterManagerId); + toListResult.add(collisionBehavior); return toListResult; } @@ -2091,6 +2140,8 @@ ArrayList toList() { pigeonResult.setMarkerId((String) markerId); Object clusterManagerId = pigeonVar_list.get(12); pigeonResult.setClusterManagerId((String) clusterManagerId); + Object collisionBehavior = pigeonVar_list.get(13); + pigeonResult.setCollisionBehavior((PlatformMarkerCollisionBehavior) collisionBehavior); return pigeonResult; } } @@ -4735,14 +4786,24 @@ public void setLiteModeEnabled(@Nullable Boolean setterArg) { this.liteModeEnabled = setterArg; } - private @Nullable String cloudMapId; + private @Nullable PlatformMarkerType markerType; - public @Nullable String getCloudMapId() { - return cloudMapId; + public @Nullable PlatformMarkerType getMarkerType() { + return markerType; } - public void setCloudMapId(@Nullable String setterArg) { - this.cloudMapId = setterArg; + public void setMarkerType(@Nullable PlatformMarkerType setterArg) { + this.markerType = setterArg; + } + + private @Nullable String mapId; + + public @Nullable String getMapId() { + return mapId; + } + + public void setMapId(@Nullable String setterArg) { + this.mapId = setterArg; } private @Nullable String style; @@ -4782,7 +4843,8 @@ public boolean equals(Object o) { && Objects.equals(trafficEnabled, that.trafficEnabled) && Objects.equals(buildingsEnabled, that.buildingsEnabled) && Objects.equals(liteModeEnabled, that.liteModeEnabled) - && Objects.equals(cloudMapId, that.cloudMapId) + && Objects.equals(markerType, that.markerType) + && Objects.equals(mapId, that.mapId) && Objects.equals(style, that.style); } @@ -4807,7 +4869,8 @@ public int hashCode() { trafficEnabled, buildingsEnabled, liteModeEnabled, - cloudMapId, + markerType, + mapId, style); } @@ -4958,11 +5021,19 @@ public static final class Builder { return this; } - private @Nullable String cloudMapId; + private @Nullable PlatformMarkerType markerType; @CanIgnoreReturnValue - public @NonNull Builder setCloudMapId(@Nullable String setterArg) { - this.cloudMapId = setterArg; + public @NonNull Builder setMarkerType(@Nullable PlatformMarkerType setterArg) { + this.markerType = setterArg; + return this; + } + + private @Nullable String mapId; + + @CanIgnoreReturnValue + public @NonNull Builder setMapId(@Nullable String setterArg) { + this.mapId = setterArg; return this; } @@ -4994,7 +5065,8 @@ public static final class Builder { pigeonReturn.setTrafficEnabled(trafficEnabled); pigeonReturn.setBuildingsEnabled(buildingsEnabled); pigeonReturn.setLiteModeEnabled(liteModeEnabled); - pigeonReturn.setCloudMapId(cloudMapId); + pigeonReturn.setMarkerType(markerType); + pigeonReturn.setMapId(mapId); pigeonReturn.setStyle(style); return pigeonReturn; } @@ -5002,7 +5074,7 @@ public static final class Builder { @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(20); + ArrayList toListResult = new ArrayList<>(21); toListResult.add(compassEnabled); toListResult.add(cameraTargetBounds); toListResult.add(mapType); @@ -5021,7 +5093,8 @@ ArrayList toList() { toListResult.add(trafficEnabled); toListResult.add(buildingsEnabled); toListResult.add(liteModeEnabled); - toListResult.add(cloudMapId); + toListResult.add(markerType); + toListResult.add(mapId); toListResult.add(style); return toListResult; } @@ -5064,9 +5137,11 @@ ArrayList toList() { pigeonResult.setBuildingsEnabled((Boolean) buildingsEnabled); Object liteModeEnabled = pigeonVar_list.get(17); pigeonResult.setLiteModeEnabled((Boolean) liteModeEnabled); - Object cloudMapId = pigeonVar_list.get(18); - pigeonResult.setCloudMapId((String) cloudMapId); - Object style = pigeonVar_list.get(19); + Object markerType = pigeonVar_list.get(18); + pigeonResult.setMarkerType((PlatformMarkerType) markerType); + Object mapId = pigeonVar_list.get(19); + pigeonResult.setMapId((String) mapId); + Object style = pigeonVar_list.get(20); pigeonResult.setStyle((String) style); return pigeonResult; } @@ -6202,6 +6277,187 @@ ArrayList toList() { } } + /** + * Pigeon equivalent of [PinConfig]. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformBitmapPinConfig { + private @Nullable Long backgroundColor; + + public @Nullable Long getBackgroundColor() { + return backgroundColor; + } + + public void setBackgroundColor(@Nullable Long setterArg) { + this.backgroundColor = setterArg; + } + + private @Nullable Long borderColor; + + public @Nullable Long getBorderColor() { + return borderColor; + } + + public void setBorderColor(@Nullable Long setterArg) { + this.borderColor = setterArg; + } + + private @Nullable Long glyphColor; + + public @Nullable Long getGlyphColor() { + return glyphColor; + } + + public void setGlyphColor(@Nullable Long setterArg) { + this.glyphColor = setterArg; + } + + private @Nullable PlatformBitmap glyphBitmap; + + public @Nullable PlatformBitmap getGlyphBitmap() { + return glyphBitmap; + } + + public void setGlyphBitmap(@Nullable PlatformBitmap setterArg) { + this.glyphBitmap = setterArg; + } + + private @Nullable String glyphText; + + public @Nullable String getGlyphText() { + return glyphText; + } + + public void setGlyphText(@Nullable String setterArg) { + this.glyphText = setterArg; + } + + private @Nullable Long glyphTextColor; + + public @Nullable Long getGlyphTextColor() { + return glyphTextColor; + } + + public void setGlyphTextColor(@Nullable Long setterArg) { + this.glyphTextColor = setterArg; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformBitmapPinConfig that = (PlatformBitmapPinConfig) o; + return Objects.equals(backgroundColor, that.backgroundColor) + && Objects.equals(borderColor, that.borderColor) + && Objects.equals(glyphColor, that.glyphColor) + && Objects.equals(glyphBitmap, that.glyphBitmap) + && Objects.equals(glyphText, that.glyphText) + && Objects.equals(glyphTextColor, that.glyphTextColor); + } + + @Override + public int hashCode() { + return Objects.hash( + backgroundColor, borderColor, glyphColor, glyphBitmap, glyphText, glyphTextColor); + } + + public static final class Builder { + + private @Nullable Long backgroundColor; + + @CanIgnoreReturnValue + public @NonNull Builder setBackgroundColor(@Nullable Long setterArg) { + this.backgroundColor = setterArg; + return this; + } + + private @Nullable Long borderColor; + + @CanIgnoreReturnValue + public @NonNull Builder setBorderColor(@Nullable Long setterArg) { + this.borderColor = setterArg; + return this; + } + + private @Nullable Long glyphColor; + + @CanIgnoreReturnValue + public @NonNull Builder setGlyphColor(@Nullable Long setterArg) { + this.glyphColor = setterArg; + return this; + } + + private @Nullable PlatformBitmap glyphBitmap; + + @CanIgnoreReturnValue + public @NonNull Builder setGlyphBitmap(@Nullable PlatformBitmap setterArg) { + this.glyphBitmap = setterArg; + return this; + } + + private @Nullable String glyphText; + + @CanIgnoreReturnValue + public @NonNull Builder setGlyphText(@Nullable String setterArg) { + this.glyphText = setterArg; + return this; + } + + private @Nullable Long glyphTextColor; + + @CanIgnoreReturnValue + public @NonNull Builder setGlyphTextColor(@Nullable Long setterArg) { + this.glyphTextColor = setterArg; + return this; + } + + public @NonNull PlatformBitmapPinConfig build() { + PlatformBitmapPinConfig pigeonReturn = new PlatformBitmapPinConfig(); + pigeonReturn.setBackgroundColor(backgroundColor); + pigeonReturn.setBorderColor(borderColor); + pigeonReturn.setGlyphColor(glyphColor); + pigeonReturn.setGlyphBitmap(glyphBitmap); + pigeonReturn.setGlyphText(glyphText); + pigeonReturn.setGlyphTextColor(glyphTextColor); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(6); + toListResult.add(backgroundColor); + toListResult.add(borderColor); + toListResult.add(glyphColor); + toListResult.add(glyphBitmap); + toListResult.add(glyphText); + toListResult.add(glyphTextColor); + return toListResult; + } + + static @NonNull PlatformBitmapPinConfig fromList(@NonNull ArrayList pigeonVar_list) { + PlatformBitmapPinConfig pigeonResult = new PlatformBitmapPinConfig(); + Object backgroundColor = pigeonVar_list.get(0); + pigeonResult.setBackgroundColor((Long) backgroundColor); + Object borderColor = pigeonVar_list.get(1); + pigeonResult.setBorderColor((Long) borderColor); + Object glyphColor = pigeonVar_list.get(2); + pigeonResult.setGlyphColor((Long) glyphColor); + Object glyphBitmap = pigeonVar_list.get(3); + pigeonResult.setGlyphBitmap((PlatformBitmap) glyphBitmap); + Object glyphText = pigeonVar_list.get(4); + pigeonResult.setGlyphText((String) glyphText); + Object glyphTextColor = pigeonVar_list.get(5); + pigeonResult.setGlyphTextColor((Long) glyphTextColor); + return pigeonResult; + } + } + private static class PigeonCodec extends StandardMessageCodec { public static final PigeonCodec INSTANCE = new PigeonCodec(); @@ -6223,109 +6479,123 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 131: { Object value = readValue(buffer); - return value == null ? null : PlatformJointType.values()[((Long) value).intValue()]; + return value == null + ? null + : PlatformMarkerCollisionBehavior.values()[((Long) value).intValue()]; } case (byte) 132: { Object value = readValue(buffer); - return value == null ? null : PlatformCapType.values()[((Long) value).intValue()]; + return value == null ? null : PlatformJointType.values()[((Long) value).intValue()]; } case (byte) 133: + { + Object value = readValue(buffer); + return value == null ? null : PlatformCapType.values()[((Long) value).intValue()]; + } + case (byte) 134: { Object value = readValue(buffer); return value == null ? null : PlatformPatternItemType.values()[((Long) value).intValue()]; } - case (byte) 134: + case (byte) 135: + { + Object value = readValue(buffer); + return value == null ? null : PlatformMarkerType.values()[((Long) value).intValue()]; + } + case (byte) 136: { Object value = readValue(buffer); return value == null ? null : PlatformMapBitmapScaling.values()[((Long) value).intValue()]; } - case (byte) 135: + case (byte) 137: return PlatformCameraPosition.fromList((ArrayList) readValue(buffer)); - case (byte) 136: + case (byte) 138: return PlatformCameraUpdate.fromList((ArrayList) readValue(buffer)); - case (byte) 137: + case (byte) 139: return PlatformCameraUpdateNewCameraPosition.fromList( (ArrayList) readValue(buffer)); - case (byte) 138: + case (byte) 140: return PlatformCameraUpdateNewLatLng.fromList((ArrayList) readValue(buffer)); - case (byte) 139: + case (byte) 141: return PlatformCameraUpdateNewLatLngBounds.fromList( (ArrayList) readValue(buffer)); - case (byte) 140: + case (byte) 142: return PlatformCameraUpdateNewLatLngZoom.fromList((ArrayList) readValue(buffer)); - case (byte) 141: + case (byte) 143: return PlatformCameraUpdateScrollBy.fromList((ArrayList) readValue(buffer)); - case (byte) 142: + case (byte) 144: return PlatformCameraUpdateZoomBy.fromList((ArrayList) readValue(buffer)); - case (byte) 143: + case (byte) 145: return PlatformCameraUpdateZoom.fromList((ArrayList) readValue(buffer)); - case (byte) 144: + case (byte) 146: return PlatformCameraUpdateZoomTo.fromList((ArrayList) readValue(buffer)); - case (byte) 145: + case (byte) 147: return PlatformCircle.fromList((ArrayList) readValue(buffer)); - case (byte) 146: + case (byte) 148: return PlatformHeatmap.fromList((ArrayList) readValue(buffer)); - case (byte) 147: + case (byte) 149: return PlatformClusterManager.fromList((ArrayList) readValue(buffer)); - case (byte) 148: + case (byte) 150: return PlatformDoublePair.fromList((ArrayList) readValue(buffer)); - case (byte) 149: + case (byte) 151: return PlatformInfoWindow.fromList((ArrayList) readValue(buffer)); - case (byte) 150: + case (byte) 152: return PlatformMarker.fromList((ArrayList) readValue(buffer)); - case (byte) 151: + case (byte) 153: return PlatformPolygon.fromList((ArrayList) readValue(buffer)); - case (byte) 152: + case (byte) 154: return PlatformPolyline.fromList((ArrayList) readValue(buffer)); - case (byte) 153: + case (byte) 155: return PlatformCap.fromList((ArrayList) readValue(buffer)); - case (byte) 154: + case (byte) 156: return PlatformPatternItem.fromList((ArrayList) readValue(buffer)); - case (byte) 155: + case (byte) 157: return PlatformTile.fromList((ArrayList) readValue(buffer)); - case (byte) 156: + case (byte) 158: return PlatformTileOverlay.fromList((ArrayList) readValue(buffer)); - case (byte) 157: + case (byte) 159: return PlatformEdgeInsets.fromList((ArrayList) readValue(buffer)); - case (byte) 158: + case (byte) 160: return PlatformLatLng.fromList((ArrayList) readValue(buffer)); - case (byte) 159: + case (byte) 161: return PlatformLatLngBounds.fromList((ArrayList) readValue(buffer)); - case (byte) 160: + case (byte) 162: return PlatformCluster.fromList((ArrayList) readValue(buffer)); - case (byte) 161: + case (byte) 163: return PlatformGroundOverlay.fromList((ArrayList) readValue(buffer)); - case (byte) 162: + case (byte) 164: return PlatformCameraTargetBounds.fromList((ArrayList) readValue(buffer)); - case (byte) 163: + case (byte) 165: return PlatformMapViewCreationParams.fromList((ArrayList) readValue(buffer)); - case (byte) 164: + case (byte) 166: return PlatformMapConfiguration.fromList((ArrayList) readValue(buffer)); - case (byte) 165: + case (byte) 167: return PlatformPoint.fromList((ArrayList) readValue(buffer)); - case (byte) 166: + case (byte) 168: return PlatformTileLayer.fromList((ArrayList) readValue(buffer)); - case (byte) 167: + case (byte) 169: return PlatformZoomRange.fromList((ArrayList) readValue(buffer)); - case (byte) 168: + case (byte) 170: return PlatformBitmap.fromList((ArrayList) readValue(buffer)); - case (byte) 169: + case (byte) 171: return PlatformBitmapDefaultMarker.fromList((ArrayList) readValue(buffer)); - case (byte) 170: + case (byte) 172: return PlatformBitmapBytes.fromList((ArrayList) readValue(buffer)); - case (byte) 171: + case (byte) 173: return PlatformBitmapAsset.fromList((ArrayList) readValue(buffer)); - case (byte) 172: + case (byte) 174: return PlatformBitmapAssetImage.fromList((ArrayList) readValue(buffer)); - case (byte) 173: + case (byte) 175: return PlatformBitmapAssetMap.fromList((ArrayList) readValue(buffer)); - case (byte) 174: + case (byte) 176: return PlatformBitmapBytesMap.fromList((ArrayList) readValue(buffer)); + case (byte) 177: + return PlatformBitmapPinConfig.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -6339,138 +6609,147 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformRendererType) { stream.write(130); writeValue(stream, value == null ? null : ((PlatformRendererType) value).index); - } else if (value instanceof PlatformJointType) { + } else if (value instanceof PlatformMarkerCollisionBehavior) { stream.write(131); + writeValue(stream, value == null ? null : ((PlatformMarkerCollisionBehavior) value).index); + } else if (value instanceof PlatformJointType) { + stream.write(132); writeValue(stream, value == null ? null : ((PlatformJointType) value).index); } else if (value instanceof PlatformCapType) { - stream.write(132); + stream.write(133); writeValue(stream, value == null ? null : ((PlatformCapType) value).index); } else if (value instanceof PlatformPatternItemType) { - stream.write(133); + stream.write(134); writeValue(stream, value == null ? null : ((PlatformPatternItemType) value).index); + } else if (value instanceof PlatformMarkerType) { + stream.write(135); + writeValue(stream, value == null ? null : ((PlatformMarkerType) value).index); } else if (value instanceof PlatformMapBitmapScaling) { - stream.write(134); + stream.write(136); writeValue(stream, value == null ? null : ((PlatformMapBitmapScaling) value).index); } else if (value instanceof PlatformCameraPosition) { - stream.write(135); + stream.write(137); writeValue(stream, ((PlatformCameraPosition) value).toList()); } else if (value instanceof PlatformCameraUpdate) { - stream.write(136); + stream.write(138); writeValue(stream, ((PlatformCameraUpdate) value).toList()); } else if (value instanceof PlatformCameraUpdateNewCameraPosition) { - stream.write(137); + stream.write(139); writeValue(stream, ((PlatformCameraUpdateNewCameraPosition) value).toList()); } else if (value instanceof PlatformCameraUpdateNewLatLng) { - stream.write(138); + stream.write(140); writeValue(stream, ((PlatformCameraUpdateNewLatLng) value).toList()); } else if (value instanceof PlatformCameraUpdateNewLatLngBounds) { - stream.write(139); + stream.write(141); writeValue(stream, ((PlatformCameraUpdateNewLatLngBounds) value).toList()); } else if (value instanceof PlatformCameraUpdateNewLatLngZoom) { - stream.write(140); + stream.write(142); writeValue(stream, ((PlatformCameraUpdateNewLatLngZoom) value).toList()); } else if (value instanceof PlatformCameraUpdateScrollBy) { - stream.write(141); + stream.write(143); writeValue(stream, ((PlatformCameraUpdateScrollBy) value).toList()); } else if (value instanceof PlatformCameraUpdateZoomBy) { - stream.write(142); + stream.write(144); writeValue(stream, ((PlatformCameraUpdateZoomBy) value).toList()); } else if (value instanceof PlatformCameraUpdateZoom) { - stream.write(143); + stream.write(145); writeValue(stream, ((PlatformCameraUpdateZoom) value).toList()); } else if (value instanceof PlatformCameraUpdateZoomTo) { - stream.write(144); + stream.write(146); writeValue(stream, ((PlatformCameraUpdateZoomTo) value).toList()); } else if (value instanceof PlatformCircle) { - stream.write(145); + stream.write(147); writeValue(stream, ((PlatformCircle) value).toList()); } else if (value instanceof PlatformHeatmap) { - stream.write(146); + stream.write(148); writeValue(stream, ((PlatformHeatmap) value).toList()); } else if (value instanceof PlatformClusterManager) { - stream.write(147); + stream.write(149); writeValue(stream, ((PlatformClusterManager) value).toList()); } else if (value instanceof PlatformDoublePair) { - stream.write(148); + stream.write(150); writeValue(stream, ((PlatformDoublePair) value).toList()); } else if (value instanceof PlatformInfoWindow) { - stream.write(149); + stream.write(151); writeValue(stream, ((PlatformInfoWindow) value).toList()); } else if (value instanceof PlatformMarker) { - stream.write(150); + stream.write(152); writeValue(stream, ((PlatformMarker) value).toList()); } else if (value instanceof PlatformPolygon) { - stream.write(151); + stream.write(153); writeValue(stream, ((PlatformPolygon) value).toList()); } else if (value instanceof PlatformPolyline) { - stream.write(152); + stream.write(154); writeValue(stream, ((PlatformPolyline) value).toList()); } else if (value instanceof PlatformCap) { - stream.write(153); + stream.write(155); writeValue(stream, ((PlatformCap) value).toList()); } else if (value instanceof PlatformPatternItem) { - stream.write(154); + stream.write(156); writeValue(stream, ((PlatformPatternItem) value).toList()); } else if (value instanceof PlatformTile) { - stream.write(155); + stream.write(157); writeValue(stream, ((PlatformTile) value).toList()); } else if (value instanceof PlatformTileOverlay) { - stream.write(156); + stream.write(158); writeValue(stream, ((PlatformTileOverlay) value).toList()); } else if (value instanceof PlatformEdgeInsets) { - stream.write(157); + stream.write(159); writeValue(stream, ((PlatformEdgeInsets) value).toList()); } else if (value instanceof PlatformLatLng) { - stream.write(158); + stream.write(160); writeValue(stream, ((PlatformLatLng) value).toList()); } else if (value instanceof PlatformLatLngBounds) { - stream.write(159); + stream.write(161); writeValue(stream, ((PlatformLatLngBounds) value).toList()); } else if (value instanceof PlatformCluster) { - stream.write(160); + stream.write(162); writeValue(stream, ((PlatformCluster) value).toList()); } else if (value instanceof PlatformGroundOverlay) { - stream.write(161); + stream.write(163); writeValue(stream, ((PlatformGroundOverlay) value).toList()); } else if (value instanceof PlatformCameraTargetBounds) { - stream.write(162); + stream.write(164); writeValue(stream, ((PlatformCameraTargetBounds) value).toList()); } else if (value instanceof PlatformMapViewCreationParams) { - stream.write(163); + stream.write(165); writeValue(stream, ((PlatformMapViewCreationParams) value).toList()); } else if (value instanceof PlatformMapConfiguration) { - stream.write(164); + stream.write(166); writeValue(stream, ((PlatformMapConfiguration) value).toList()); } else if (value instanceof PlatformPoint) { - stream.write(165); + stream.write(167); writeValue(stream, ((PlatformPoint) value).toList()); } else if (value instanceof PlatformTileLayer) { - stream.write(166); + stream.write(168); writeValue(stream, ((PlatformTileLayer) value).toList()); } else if (value instanceof PlatformZoomRange) { - stream.write(167); + stream.write(169); writeValue(stream, ((PlatformZoomRange) value).toList()); } else if (value instanceof PlatformBitmap) { - stream.write(168); + stream.write(170); writeValue(stream, ((PlatformBitmap) value).toList()); } else if (value instanceof PlatformBitmapDefaultMarker) { - stream.write(169); + stream.write(171); writeValue(stream, ((PlatformBitmapDefaultMarker) value).toList()); } else if (value instanceof PlatformBitmapBytes) { - stream.write(170); + stream.write(172); writeValue(stream, ((PlatformBitmapBytes) value).toList()); } else if (value instanceof PlatformBitmapAsset) { - stream.write(171); + stream.write(173); writeValue(stream, ((PlatformBitmapAsset) value).toList()); } else if (value instanceof PlatformBitmapAssetImage) { - stream.write(172); + stream.write(174); writeValue(stream, ((PlatformBitmapAssetImage) value).toList()); } else if (value instanceof PlatformBitmapAssetMap) { - stream.write(173); + stream.write(175); writeValue(stream, ((PlatformBitmapAssetMap) value).toList()); } else if (value instanceof PlatformBitmapBytesMap) { - stream.write(174); + stream.write(176); writeValue(stream, ((PlatformBitmapBytesMap) value).toList()); + } else if (value instanceof PlatformBitmapPinConfig) { + stream.write(177); + writeValue(stream, ((PlatformBitmapPinConfig) value).toList()); } else { super.writeValue(stream, value); } @@ -6600,6 +6879,13 @@ void animateCamera( */ @NonNull Boolean didLastStyleSucceed(); + /** + * Returns true if this map supports advanced markers. + * + *

This allows checking if the map supports advanced markers before attempting to use them. + */ + @NonNull + Boolean isAdvancedMarkersAvailable(); /** Clears the cache of tiles previously requseted from the tile provider. */ void clearTileCache(@NonNull String tileOverlayId); /** Takes a snapshot of the map and returns its image data. */ @@ -7159,6 +7445,29 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.MapsApi.isAdvancedMarkersAvailable" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + Boolean output = api.isAdvancedMarkersAvailable(); + wrapped.add(0, output); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ClusterManagersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ClusterManagersControllerTest.java index 4f476145f2d..b26f7f13b9e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ClusterManagersControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ClusterManagersControllerTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; @@ -21,10 +22,15 @@ import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; import com.google.maps.android.clustering.Cluster; +import com.google.maps.android.clustering.ClusterManager; import com.google.maps.android.clustering.algo.StaticCluster; import com.google.maps.android.collections.MarkerManager; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.googlemaps.ClusterManagersController.AdvancedMarkerClusterRenderer; +import io.flutter.plugins.googlemaps.ClusterManagersController.MarkerClusterRenderer; import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerCollisionBehavior; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Collections; @@ -61,7 +67,7 @@ public void setUp() { context = ApplicationProvider.getApplicationContext(); assetManager = context.getAssets(); flutterApi = spy(new MapsCallbackApi(mock(BinaryMessenger.class))); - controller = spy(new ClusterManagersController(flutterApi, context)); + controller = spy(new ClusterManagersController(flutterApi, context, PlatformMarkerType.MARKER)); googleMap = mock(GoogleMap.class); markerManager = new MarkerManager(googleMap); controller.init(googleMap, markerManager); @@ -97,8 +103,10 @@ public void AddClusterManagersAndMarkers() { clusterManagersToAdd.add(initialClusterManager); controller.addClusterManagers(clusterManagersToAdd); - MarkerBuilder markerBuilder1 = new MarkerBuilder(markerId1, clusterManagerId); - MarkerBuilder markerBuilder2 = new MarkerBuilder(markerId2, clusterManagerId); + MarkerBuilder markerBuilder1 = + new MarkerBuilder(markerId1, clusterManagerId, PlatformMarkerType.MARKER); + MarkerBuilder markerBuilder2 = + new MarkerBuilder(markerId2, clusterManagerId, PlatformMarkerType.MARKER); final Messages.PlatformMarker markerData1 = createPlatformMarker(markerId1, location1, clusterManagerId); @@ -128,6 +136,56 @@ public void AddClusterManagersAndMarkers() { assertEquals("Cluster should contain exactly 2 markers", 2, cluster.getSize()); } + @Test + public void SelectClusterRenderer() { + final String clusterManagerId1 = "cm_1"; + final String clusterManagerId2 = "cm_2"; + final String markerId1 = "mid_1"; + final String markerId2 = "mid_2"; + + when(googleMap.getCameraPosition()) + .thenReturn(CameraPosition.builder().target(new LatLng(0, 0)).build()); + + ClusterManagersController controller1 = + spy(new ClusterManagersController(flutterApi, context, PlatformMarkerType.MARKER)); + controller1.init(googleMap, markerManager); + ClusterManagersController controller2 = + spy(new ClusterManagersController(flutterApi, context, PlatformMarkerType.ADVANCED_MARKER)); + controller2.init(googleMap, markerManager); + + Messages.PlatformClusterManager initialClusterManager1 = + new Messages.PlatformClusterManager.Builder().setIdentifier(clusterManagerId1).build(); + List clusterManagersToAdd1 = new ArrayList<>(); + clusterManagersToAdd1.add(initialClusterManager1); + controller1.addClusterManagers(clusterManagersToAdd1); + + Messages.PlatformClusterManager initialClusterManager2 = + new Messages.PlatformClusterManager.Builder().setIdentifier(clusterManagerId2).build(); + List clusterManagersToAdd2 = new ArrayList<>(); + clusterManagersToAdd2.add(initialClusterManager2); + controller2.addClusterManagers(clusterManagersToAdd2); + + MarkerBuilder markerBuilder1 = + new MarkerBuilder(markerId1, clusterManagerId1, PlatformMarkerType.MARKER); + markerBuilder1.setPosition(new LatLng(10.0, 20.0)); + controller1.addItem(markerBuilder1); + + MarkerBuilder markerBuilder2 = + new MarkerBuilder(markerId2, clusterManagerId2, PlatformMarkerType.ADVANCED_MARKER); + markerBuilder2.setPosition(new LatLng(20.0, 10.0)); + controller2.addItem(markerBuilder2); + + ClusterManager clusterManager1 = + controller1.clusterManagerIdToManager.get(clusterManagerId1); + assertNotNull(clusterManager1); + assertSame(clusterManager1.getRenderer().getClass(), MarkerClusterRenderer.class); + + ClusterManager clusterManager2 = + controller2.clusterManagerIdToManager.get(clusterManagerId2); + assertNotNull(clusterManager2); + assertSame(clusterManager2.getRenderer().getClass(), AdvancedMarkerClusterRenderer.class); + } + @Test public void OnClusterClickCallsMethodChannel() { String clusterManagerId = "cm_1"; @@ -137,11 +195,11 @@ public void OnClusterClickCallsMethodChannel() { StaticCluster cluster = new StaticCluster<>(clusterPosition); - MarkerBuilder marker1 = new MarkerBuilder("m_1", clusterManagerId); + MarkerBuilder marker1 = new MarkerBuilder("m_1", clusterManagerId, PlatformMarkerType.MARKER); marker1.setPosition(markerPosition1); cluster.add(marker1); - MarkerBuilder marker2 = new MarkerBuilder("m_2", clusterManagerId); + MarkerBuilder marker2 = new MarkerBuilder("m_2", clusterManagerId, PlatformMarkerType.MARKER); marker2.setPosition(markerPosition2); cluster.add(marker2); @@ -208,6 +266,7 @@ private Messages.PlatformMarker createPlatformMarker( .setClusterManagerId(clusterManagerId) .setAnchor(anchor) .setInfoWindow(new Messages.PlatformInfoWindow.Builder().setAnchor(anchor).build()) + .setCollisionBehavior(PlatformMarkerCollisionBehavior.REQUIRED_DISPLAY) .build(); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java index db66ca92b49..5eafc63408d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java @@ -14,6 +14,7 @@ import static io.flutter.plugins.googlemaps.Convert.HEATMAP_MAX_INTENSITY_KEY; import static io.flutter.plugins.googlemaps.Convert.HEATMAP_OPACITY_KEY; import static io.flutter.plugins.googlemaps.Convert.HEATMAP_RADIUS_KEY; +import static io.flutter.plugins.googlemaps.Convert.getPinConfigFromPlatformPinConfig; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -30,6 +31,7 @@ import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.PinConfig; import com.google.maps.android.clustering.algo.StaticCluster; import com.google.maps.android.geometry.Point; import com.google.maps.android.heatmaps.Gradient; @@ -37,6 +39,7 @@ import com.google.maps.android.projection.SphericalMercatorProjection; import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; import io.flutter.plugins.googlemaps.Convert.FlutterInjectorWrapper; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.util.Collections; import java.util.List; import java.util.Map; @@ -51,6 +54,7 @@ @RunWith(RobolectricTestRunner.class) public class ConvertTest { + @Mock private AssetManager assetManager; @Mock private BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; @@ -97,11 +101,11 @@ public void ConvertClusterToPigeonReturnsCorrectData() { StaticCluster cluster = new StaticCluster<>(clusterPosition); - MarkerBuilder marker1 = new MarkerBuilder("m_1", clusterManagerId); + MarkerBuilder marker1 = new MarkerBuilder("m_1", clusterManagerId, PlatformMarkerType.MARKER); marker1.setPosition(markerPosition1); cluster.add(marker1); - MarkerBuilder marker2 = new MarkerBuilder("m_2", clusterManagerId); + MarkerBuilder marker2 = new MarkerBuilder("m_2", clusterManagerId, PlatformMarkerType.MARKER); marker2.setPosition(markerPosition2); cluster.add(marker2); @@ -327,6 +331,64 @@ public void GetBitmapFromBytesThrowsErrorIfInvalidImageData() { fail("Expected an IllegalArgumentException to be thrown"); } + @Test + public void GetPinConfigFromPlatformPinConfig_GlyphColor() { + Messages.PlatformBitmapPinConfig platformBitmap = + new Messages.PlatformBitmapPinConfig.Builder() + .setBackgroundColor(0x00FFFFL) + .setBorderColor(0xFF00FFL) + .setGlyphColor(0x112233L) + .build(); + + PinConfig pinConfig = + getPinConfigFromPlatformPinConfig( + platformBitmap, assetManager, 1, bitmapDescriptorFactoryWrapper); + Assert.assertEquals(0x00FFFFL, pinConfig.getBackgroundColor()); + Assert.assertEquals(0xFF00FFL, pinConfig.getBorderColor()); + Assert.assertEquals(0x112233L, pinConfig.getGlyph().getGlyphColor()); + } + + @Test + public void GetPinConfigFromPlatformPinConfig_Glyph() { + Messages.PlatformBitmapPinConfig platformBitmap = + new Messages.PlatformBitmapPinConfig.Builder() + .setGlyphText("Hi") + .setGlyphTextColor(0xFFFFFFL) + .build(); + PinConfig pinConfig = + getPinConfigFromPlatformPinConfig( + platformBitmap, assetManager, 1, bitmapDescriptorFactoryWrapper); + Assert.assertEquals("Hi", pinConfig.getGlyph().getText()); + Assert.assertEquals(0xFFFFFFL, pinConfig.getGlyph().getTextColor()); + } + + @Test + public void GetPinConfigFromPlatformPinConfig_GlyphBitmap() { + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bytesBitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + Messages.PlatformBitmap icon = + new Messages.PlatformBitmap.Builder().setBitmap(bytesBitmap).build(); + Messages.PlatformBitmapPinConfig platformBitmap = + new Messages.PlatformBitmapPinConfig.Builder() + .setBackgroundColor(0xFFFFFFL) + .setBorderColor(0x000000L) + .setGlyphBitmap(icon) + .build(); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + PinConfig pinConfig = + getPinConfigFromPlatformPinConfig( + platformBitmap, assetManager, 1, bitmapDescriptorFactoryWrapper); + + Assert.assertEquals(0xFFFFFFL, pinConfig.getBackgroundColor()); + Assert.assertEquals(0x000000L, pinConfig.getBorderColor()); + Assert.assertEquals(mockBitmapDescriptor, pinConfig.getGlyph().getBitmapDescriptor()); + } + @Test public void interpretMapConfiguration_handlesNulls() { final Messages.PlatformMapConfiguration config = diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index d2339531eba..42ca991ef71 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -25,9 +25,11 @@ import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.MapCapabilities; import com.google.android.gms.maps.model.Marker; import com.google.maps.android.clustering.ClusterManager; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.util.ArrayList; import java.util.List; import org.junit.After; @@ -59,6 +61,7 @@ public class GoogleMapControllerTest { @Mock HeatmapsController mockHeatmapsController; @Mock TileOverlaysController mockTileOverlaysController; @Mock GroundOverlaysController mockGroundOverlaysController; + @Mock MapCapabilities mapCapabilities; @Before public void before() { @@ -71,7 +74,8 @@ public void before() { // See getGoogleMapControllerWithMockedDependencies for version with dependency injections. public GoogleMapController getGoogleMapController() { GoogleMapController googleMapController = - new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null); + new GoogleMapController( + 0, context, mockMessenger, activity::getLifecycle, null, PlatformMarkerType.MARKER); googleMapController.init(); return googleMapController; } @@ -216,7 +220,7 @@ public void SetInitialClusterManagers() { @Test public void OnClusterItemRenderedCallsMarkersController() { GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); - MarkerBuilder markerBuilder = new MarkerBuilder("m_1", "cm_1"); + MarkerBuilder markerBuilder = new MarkerBuilder("m_1", "cm_1", PlatformMarkerType.MARKER); final Marker marker = mock(Marker.class); googleMapController.onClusterItemRendered(markerBuilder, marker); verify(mockMarkersController, times(1)).onClusterItemRendered(markerBuilder, marker); @@ -225,7 +229,7 @@ public void OnClusterItemRenderedCallsMarkersController() { @Test public void OnClusterItemClickCallsMarkersController() { GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); - MarkerBuilder markerBuilder = new MarkerBuilder("m_1", "cm_1"); + MarkerBuilder markerBuilder = new MarkerBuilder("m_1", "cm_1", PlatformMarkerType.MARKER); googleMapController.onClusterItemClick(markerBuilder); verify(mockMarkersController, times(1)).onMarkerTap(markerBuilder.markerId()); @@ -317,4 +321,17 @@ public void getCameraPositionReturnsCorrectData() { Assert.assertEquals(cameraPosition.tilt, result.getTilt(), 1e-15); Assert.assertEquals(cameraPosition.bearing, result.getBearing(), 1e-15); } + + @Test + public void isAdvancedMarkersAvailableReturnsCorrectData() { + GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); + googleMapController.onMapReady(mockGoogleMap); + + when(mockGoogleMap.getMapCapabilities()).thenReturn(mapCapabilities); + when(mapCapabilities.isAdvancedMarkersAvailable()).thenReturn(true); + Assert.assertEquals(true, googleMapController.isAdvancedMarkersAvailable()); + + when(mapCapabilities.isAdvancedMarkersAvailable()).thenReturn(false); + Assert.assertEquals(false, googleMapController.isAdvancedMarkersAvailable()); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java index 7b5e0dae851..e407e61fd6d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -17,16 +17,21 @@ import android.graphics.Bitmap; import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.AdvancedMarkerOptions; +import com.google.android.gms.maps.model.AdvancedMarkerOptions.CollisionBehavior; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.maps.android.collections.MarkerManager; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerCollisionBehavior; +import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType; import java.io.ByteArrayOutputStream; import java.util.Collections; import java.util.List; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -81,7 +86,8 @@ private static Messages.PlatformMarker.Builder defaultMarkerBuilder() { .setZIndex(0.0) .setConsumeTapEvents(false) .setIcon(icon) - .setInfoWindow(infoWindow); + .setInfoWindow(infoWindow) + .setCollisionBehavior(PlatformMarkerCollisionBehavior.REQUIRED_DISPLAY); } @Before @@ -90,14 +96,16 @@ public void setUp() { assetManager = ApplicationProvider.getApplicationContext().getAssets(); context = ApplicationProvider.getApplicationContext(); flutterApi = spy(new MapsCallbackApi(mock(BinaryMessenger.class))); - clusterManagersController = spy(new ClusterManagersController(flutterApi, context)); + clusterManagersController = + spy(new ClusterManagersController(flutterApi, context, PlatformMarkerType.MARKER)); controller = new MarkersController( flutterApi, clusterManagersController, assetManager, density, - bitmapDescriptorFactoryWrapper); + bitmapDescriptorFactoryWrapper, + PlatformMarkerType.MARKER); googleMap = mock(GoogleMap.class); markerManager = new MarkerManager(googleMap); markerCollection = markerManager.newCollection(); @@ -266,4 +274,40 @@ public void controller_AddChangeAndRemoveMarkerWithoutClusterManagerId() { Mockito.verify(spyMarkerCollection, times(1)).remove(marker); } + + @Test + public void markerBuilder_setCollisionBehavior() { + Messages.PlatformMarker platformMarker = defaultMarkerBuilder().setMarkerId("1").build(); + MarkerBuilder markerBuilder = new MarkerBuilder("m_1", "1", PlatformMarkerType.ADVANCED_MARKER); + + // Default collision behavior of an AdvancedMarker + Convert.interpretMarkerOptions( + platformMarker, markerBuilder, assetManager, 1, bitmapDescriptorFactoryWrapper); + MarkerOptions markerOptions = markerBuilder.build(); + Assert.assertEquals(AdvancedMarkerOptions.class, markerOptions.getClass()); + Assert.assertEquals( + CollisionBehavior.REQUIRED, ((AdvancedMarkerOptions) markerOptions).getCollisionBehavior()); + + // Customized collision behavior of an AdvancedMarker + platformMarker = + defaultMarkerBuilder() + .setMarkerId("1") + .setCollisionBehavior(PlatformMarkerCollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY) + .build(); + Convert.interpretMarkerOptions( + platformMarker, markerBuilder, assetManager, 1, bitmapDescriptorFactoryWrapper); + markerOptions = markerBuilder.build(); + Assert.assertEquals(AdvancedMarkerOptions.class, markerOptions.getClass()); + Assert.assertEquals( + CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY, + ((AdvancedMarkerOptions) markerOptions).getCollisionBehavior()); + + // Legacy markers don't have collision behavior in the marker options + platformMarker = defaultMarkerBuilder().setMarkerId("1").build(); + markerBuilder = new MarkerBuilder("m_1", "1", PlatformMarkerType.MARKER); + Convert.interpretMarkerOptions( + platformMarker, markerBuilder, assetManager, 1, bitmapDescriptorFactoryWrapper); + markerOptions = markerBuilder.build(); + Assert.assertEquals(MarkerOptions.class, markerOptions.getClass()); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart index d421bcf9718..d1bde552e15 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart @@ -22,7 +22,7 @@ const CameraPosition _kInitialCameraPosition = CameraPosition( target: _kInitialMapCenter, zoom: _kInitialZoomLevel, ); -const String _kCloudMapId = '000000000000000'; // Dummy map ID. +const String _kMapId = '000000000000000'; // Dummy map ID. // The tolerance value for floating-point comparisons in the tests. // This value was selected as the minimum possible value that the test passes. @@ -1468,7 +1468,105 @@ void main() { } }); - testWidgets('testCloudMapId', (WidgetTester tester) async { + testWidgets('advanced markers clustering', (WidgetTester tester) async { + final Key key = GlobalKey(); + const int clusterManagersAmount = 2; + const int markersPerClusterManager = 5; + final Map markers = {}; + final Set clusterManagers = {}; + + for (int i = 0; i < clusterManagersAmount; i++) { + final ClusterManagerId clusterManagerId = ClusterManagerId( + 'cluster_manager_$i', + ); + final ClusterManager clusterManager = ClusterManager( + clusterManagerId: clusterManagerId, + ); + clusterManagers.add(clusterManager); + } + + for (final ClusterManager cm in clusterManagers) { + for (int i = 0; i < markersPerClusterManager; i++) { + final MarkerId markerId = MarkerId( + '${cm.clusterManagerId.value}_marker_$i', + ); + final AdvancedMarker marker = AdvancedMarker( + markerId: markerId, + clusterManagerId: cm.clusterManagerId, + position: LatLng( + _kInitialMapCenter.latitude + i, + _kInitialMapCenter.longitude, + ), + ); + markers[markerId] = marker; + } + } + + final Completer controllerCompleter = + Completer(); + + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + clusterManagers: clusterManagers, + markerType: MarkerType.advancedMarker, + markers: Set.of(markers.values), + onMapCreated: (ExampleGoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + ), + ); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + for (final ClusterManager cm in clusterManagers) { + final List clusters = await inspector.getClusters( + mapId: controller.mapId, + clusterManagerId: cm.clusterManagerId, + ); + final int markersAmountForClusterManager = clusters + .map((Cluster cluster) => cluster.count) + .reduce((int value, int element) => value + element); + expect(markersAmountForClusterManager, markersPerClusterManager); + } + + // Remove markers from clusterManagers and test that clusterManagers are empty. + for (final MapEntry entry in markers.entries) { + markers[entry.key] = _copyAdvancedMarkerWithClusterManagerId( + entry.value, + null, + ); + } + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + clusterManagers: clusterManagers, + markers: Set.of(markers.values), + ), + ), + ); + + for (final ClusterManager cm in clusterManagers) { + final List clusters = await inspector.getClusters( + mapId: controller.mapId, + clusterManagerId: cm.clusterManagerId, + ); + expect(clusters.length, 0); + } + }); + + testWidgets('testMapId', (WidgetTester tester) async { final Completer mapIdCompleter = Completer(); final Key key = GlobalKey(); @@ -1481,7 +1579,7 @@ void main() { onMapCreated: (ExampleGoogleMapController controller) { mapIdCompleter.complete(controller.mapId); }, - cloudMapId: _kCloudMapId, + mapId: _kMapId, ), ), ); @@ -2183,6 +2281,38 @@ void main() { // https://github.com/flutter/flutter/issues/131071 skip: true, ); + + testWidgets('markerWithPinConfig', (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.pinConfig( + backgroundColor: Colors.green, + borderColor: Colors.greenAccent, + glyph: const TextGlyph(text: 'A', textColor: Colors.white), + ), + ), + }; + + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: ui.TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(10.0, 20.0), + ), + markers: markers, + markerType: MarkerType.advancedMarker, + onMapCreated: (ExampleGoogleMapController controller) => + controllerCompleter.complete(controller), + ), + ), + ); + await tester.pumpAndSettle(); + }); } class _DebugTileProvider implements TileProvider { @@ -2371,3 +2501,29 @@ Future _checkCameraUpdateByType( expect(currentPosition.zoom, wrapMatcher(equals(_kInitialZoomLevel - 1))); } } + +AdvancedMarker _copyAdvancedMarkerWithClusterManagerId( + AdvancedMarker marker, + ClusterManagerId? clusterManagerId, +) { + return AdvancedMarker( + markerId: marker.markerId, + alpha: marker.alpha, + anchor: marker.anchor, + consumeTapEvents: marker.consumeTapEvents, + draggable: marker.draggable, + flat: marker.flat, + icon: marker.icon, + infoWindow: marker.infoWindow, + position: marker.position, + rotation: marker.rotation, + visible: marker.visible, + zIndex: marker.zIndex.toInt(), + onTap: marker.onTap, + onDragStart: marker.onDragStart, + onDrag: marker.onDrag, + onDragEnd: marker.onDragEnd, + clusterManagerId: clusterManagerId, + collisionBehavior: marker.collisionBehavior, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_marker_icons.dart new file mode 100644 index 00000000000..c999b87688c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_marker_icons.dart @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; +import 'place_advanced_marker.dart'; + +/// Page that demonstrates how to use custom [AdvanceMarker] icons. +class AdvancedMarkerIconsPage extends GoogleMapExampleAppPage { + /// Default constructor. + const AdvancedMarkerIconsPage({required this.mapId, Key? key}) + : super( + key: key, + const Icon(Icons.image_outlined), + 'Advanced marker icons', + ); + + /// Map ID to use for the GoogleMap. + final String? mapId; + + @override + Widget build(BuildContext context) { + return _AdvancedMarkerIconsBody(mapId: mapId); + } +} + +const LatLng _kMapCenter = LatLng(52.4478, -3.5402); + +class _AdvancedMarkerIconsBody extends StatefulWidget { + const _AdvancedMarkerIconsBody({required this.mapId}); + + /// Map ID to use for the GoogleMap. + final String? mapId; + + @override + State<_AdvancedMarkerIconsBody> createState() => + _AdvancedMarkerIconsBodyState(); +} + +class _AdvancedMarkerIconsBodyState extends State<_AdvancedMarkerIconsBody> { + final Set _markers = {}; + + ExampleGoogleMapController? controller; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + AdvancedMarkersCapabilityStatus(controller: controller), + Expanded( + child: ExampleGoogleMap( + mapId: widget.mapId, + markerType: MarkerType.advancedMarker, + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + markers: _markers, + onMapCreated: (ExampleGoogleMapController controller) { + setState(() { + this.controller = controller; + }); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: TextButton( + onPressed: _markers.isNotEmpty + ? null + : () async { + final AssetMapBitmap asset = await BitmapDescriptor.asset( + const ImageConfiguration(size: Size(12, 12)), + 'assets/red_square.png', + ); + final AssetMapBitmap largeAsset = + await BitmapDescriptor.asset( + const ImageConfiguration(size: Size(36, 36)), + 'assets/red_square.png', + ); + + setState(() { + _markers.addAll([ + // Default icon + AdvancedMarker( + markerId: const MarkerId('1'), + position: LatLng( + _kMapCenter.latitude + 1, + _kMapCenter.longitude + 1, + ), + ), + // Custom pin colors + AdvancedMarker( + markerId: const MarkerId('2'), + position: LatLng( + _kMapCenter.latitude - 1, + _kMapCenter.longitude - 1, + ), + icon: BitmapDescriptor.pinConfig( + borderColor: Colors.red, + backgroundColor: Colors.black, + glyph: const CircleGlyph(color: Colors.red), + ), + ), + // Pin with text + AdvancedMarker( + markerId: const MarkerId('3'), + position: LatLng( + _kMapCenter.latitude - 1, + _kMapCenter.longitude + 1, + ), + icon: BitmapDescriptor.pinConfig( + borderColor: Colors.blue, + backgroundColor: Colors.white, + glyph: const TextGlyph( + text: 'Hi!', + textColor: Colors.blue, + ), + ), + ), + // Pin with bitmap + AdvancedMarker( + markerId: const MarkerId('4'), + position: LatLng( + _kMapCenter.latitude + 1, + _kMapCenter.longitude - 1, + ), + icon: BitmapDescriptor.pinConfig( + borderColor: Colors.red, + backgroundColor: Colors.white, + glyph: BitmapGlyph(bitmap: asset), + ), + ), + // Custom marker icon + AdvancedMarker( + markerId: const MarkerId('5'), + position: LatLng( + _kMapCenter.latitude, + _kMapCenter.longitude, + ), + icon: largeAsset, + ), + ]); + }); + }, + child: const Text('Add advanced markers'), + ), + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_markers_clustering.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_markers_clustering.dart new file mode 100644 index 00000000000..6ad4e91205f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_markers_clustering.dart @@ -0,0 +1,321 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'clustering.dart'; +import 'example_google_map.dart'; +import 'page.dart'; +import 'place_advanced_marker.dart'; + +/// Page for demonstrating advanced marker clustering support. +/// Same as [ClusteringPage] but works with [AdvancedMarker]. +class AdvancedMarkersClusteringPage extends GoogleMapExampleAppPage { + /// Default constructor. + const AdvancedMarkersClusteringPage({Key? key, required this.mapId}) + : super( + key: key, + const Icon(Icons.place_outlined), + 'Manage clusters of advanced markers', + ); + + /// Map ID to use for the GoogleMap. + final String? mapId; + + @override + Widget build(BuildContext context) { + return _ClusteringBody(mapId: mapId); + } +} + +/// Body of the clustering page. +class _ClusteringBody extends StatefulWidget { + /// Default Constructor. + const _ClusteringBody({required this.mapId}); + + /// Map ID to use for the GoogleMap. + final String? mapId; + + @override + State createState() => _ClusteringBodyState(); +} + +/// State of the clustering page. +class _ClusteringBodyState extends State<_ClusteringBody> { + /// Default Constructor. + _ClusteringBodyState(); + + /// Starting point from where markers are added. + static const LatLng center = LatLng(-33.86, 151.1547171); + + /// Initial camera position. + static const CameraPosition initialCameraPosition = CameraPosition( + target: LatLng(-33.852, 151.25), + zoom: 11.0, + ); + + /// Marker offset factor for randomizing marker placing. + static const double _markerOffsetFactor = 0.05; + + /// Offset for longitude when placing markers to different cluster managers. + static const double _clusterManagerLongitudeOffset = 0.1; + + /// Maximum amount of cluster managers. + static const int _clusterManagerMaxCount = 3; + + /// Amount of markers to be added to the cluster manager at once. + static const int _markersToAddToClusterManagerCount = 10; + + /// Fully visible alpha value. + static const double _fullyVisibleAlpha = 1.0; + + /// Half visible alpha value. + static const double _halfVisibleAlpha = 0.5; + + /// Google map controller. + ExampleGoogleMapController? controller; + + /// Map of clusterManagers with identifier as the key. + Map clusterManagers = + {}; + + /// Map of markers with identifier as the key. + Map markers = {}; + + /// Id of the currently selected marker. + MarkerId? selectedMarker; + + /// Counter for added cluster manager ids. + int _clusterManagerIdCounter = 1; + + /// Counter for added markers ids. + int _markerIdCounter = 1; + + /// Cluster that was tapped most recently. + Cluster? lastCluster; + + // ignore: use_setters_to_change_properties + void _onMapCreated(ExampleGoogleMapController controller) { + this.controller = controller; + } + + void _onMarkerTapped(MarkerId markerId) { + final AdvancedMarker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + final MarkerId? previousMarkerId = selectedMarker; + if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { + final AdvancedMarker resetOld = copyWithSelectedState( + markers[previousMarkerId]!, + false, + ); + markers[previousMarkerId] = resetOld; + } + selectedMarker = markerId; + final AdvancedMarker newMarker = copyWithSelectedState( + tappedMarker, + true, + ); + markers[markerId] = newMarker; + }); + } + } + + void _addClusterManager() { + if (clusterManagers.length == _clusterManagerMaxCount) { + return; + } + + final String clusterManagerIdVal = + 'cluster_manager_id_$_clusterManagerIdCounter'; + _clusterManagerIdCounter++; + final ClusterManagerId clusterManagerId = ClusterManagerId( + clusterManagerIdVal, + ); + + final ClusterManager clusterManager = ClusterManager( + clusterManagerId: clusterManagerId, + onClusterTap: (Cluster cluster) => setState(() { + lastCluster = cluster; + }), + ); + + setState(() { + clusterManagers[clusterManagerId] = clusterManager; + }); + _addMarkersToCluster(clusterManager); + } + + void _removeClusterManager(ClusterManager clusterManager) { + setState(() { + // Remove markers managed by cluster manager to be removed. + markers.removeWhere( + (MarkerId key, AdvancedMarker marker) => + marker.clusterManagerId == clusterManager.clusterManagerId, + ); + // Remove cluster manager. + clusterManagers.remove(clusterManager.clusterManagerId); + }); + } + + void _addMarkersToCluster(ClusterManager clusterManager) { + for (int i = 0; i < _markersToAddToClusterManagerCount; i++) { + final String markerIdVal = + '${clusterManager.clusterManagerId.value}_marker_id_$_markerIdCounter'; + _markerIdCounter++; + final MarkerId markerId = MarkerId(markerIdVal); + + final int clusterManagerIndex = clusterManagers.values.toList().indexOf( + clusterManager, + ); + + // Add additional offset to longitude for each cluster manager to space + // out markers in different cluster managers. + final double clusterManagerLongitudeOffset = + clusterManagerIndex * _clusterManagerLongitudeOffset; + + final AdvancedMarker marker = AdvancedMarker( + markerId: markerId, + clusterManagerId: clusterManager.clusterManagerId, + position: LatLng( + center.latitude + _getRandomOffset(), + center.longitude + _getRandomOffset() + clusterManagerLongitudeOffset, + ), + infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), + onTap: () => _onMarkerTapped(markerId), + icon: BitmapDescriptor.pinConfig( + backgroundColor: Colors.white, + borderColor: Colors.blue, + glyph: const CircleGlyph(color: Colors.blue), + ), + ); + markers[markerId] = marker; + } + setState(() {}); + } + + double _getRandomOffset() { + return (Random().nextDouble() - 0.5) * _markerOffsetFactor; + } + + void _remove(MarkerId markerId) { + setState(() { + if (markers.containsKey(markerId)) { + markers.remove(markerId); + } + }); + } + + void _changeMarkersAlpha() { + for (final MarkerId markerId in markers.keys) { + final AdvancedMarker marker = markers[markerId]!; + final double current = marker.alpha; + markers[markerId] = marker.copyWith( + alphaParam: current == _fullyVisibleAlpha + ? _halfVisibleAlpha + : _fullyVisibleAlpha, + ); + } + setState(() {}); + } + + /// Returns selected or unselected state of the given [marker]. + AdvancedMarker copyWithSelectedState(AdvancedMarker marker, bool isSelected) { + return marker.copyWith( + iconParam: isSelected + ? BitmapDescriptor.pinConfig( + backgroundColor: Colors.blue, + borderColor: Colors.white, + glyph: const CircleGlyph(color: Colors.white), + ) + : BitmapDescriptor.pinConfig( + backgroundColor: Colors.white, + borderColor: Colors.blue, + glyph: const CircleGlyph(color: Colors.blue), + ), + ); + } + + @override + Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; + final Cluster? lastCluster = this.lastCluster; + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AdvancedMarkersCapabilityStatus(controller: controller), + Expanded( + child: ExampleGoogleMap( + mapId: widget.mapId, + markerType: MarkerType.advancedMarker, + onMapCreated: _onMapCreated, + initialCameraPosition: initialCameraPosition, + markers: Set.of(markers.values), + clusterManagers: Set.of(clusterManagers.values), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: clusterManagers.length >= _clusterManagerMaxCount + ? null + : () => _addClusterManager(), + child: const Text('Add cluster manager'), + ), + TextButton( + onPressed: clusterManagers.isEmpty + ? null + : () => _removeClusterManager(clusterManagers.values.last), + child: const Text('Remove cluster manager'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + for (final MapEntry clusterEntry + in clusterManagers.entries) + TextButton( + onPressed: () => _addMarkersToCluster(clusterEntry.value), + child: Text('Add markers to ${clusterEntry.key.value}'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: selectedId == null + ? null + : () { + _remove(selectedId); + setState(() { + selectedMarker = null; + }); + }, + child: const Text('Remove selected marker'), + ), + TextButton( + onPressed: markers.isEmpty ? null : () => _changeMarkersAlpha(), + child: const Text('Change all markers alpha'), + ), + ], + ), + if (lastCluster != null) + Padding( + padding: const EdgeInsets.all(10), + child: Text( + 'Cluster with ${lastCluster.count} markers clicked at ${lastCluster.position}', + ), + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/clustering.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/clustering.dart index ca60d810373..6a405c9f0bd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/clustering.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/clustering.dart @@ -18,23 +18,23 @@ class ClusteringPage extends GoogleMapExampleAppPage { @override Widget build(BuildContext context) { - return const ClusteringBody(); + return const _ClusteringBody(); } } /// Body of the clustering page. -class ClusteringBody extends StatefulWidget { +class _ClusteringBody extends StatefulWidget { /// Default Constructor. - const ClusteringBody({super.key}); + const _ClusteringBody(); @override - State createState() => ClusteringBodyState(); + State createState() => _ClusteringBodyState(); } /// State of the clustering page. -class ClusteringBodyState extends State { +class _ClusteringBodyState extends State<_ClusteringBody> { /// Default Constructor. - ClusteringBodyState(); + _ClusteringBodyState(); /// Starting point from where markers are added. static const LatLng center = LatLng(-33.86, 151.1547171); @@ -101,17 +101,14 @@ class ClusteringBodyState extends State { setState(() { final MarkerId? previousMarkerId = selectedMarker; if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { - final Marker resetOld = markers[previousMarkerId]!.copyWith( - iconParam: BitmapDescriptor.defaultMarker, + final Marker resetOld = _copyWithSelectedState( + markers[previousMarkerId]!, + false, ); markers[previousMarkerId] = resetOld; } selectedMarker = markerId; - final Marker newMarker = tappedMarker.copyWith( - iconParam: BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueGreen, - ), - ); + final Marker newMarker = _copyWithSelectedState(tappedMarker, true); markers[markerId] = newMarker; }); } @@ -171,8 +168,8 @@ class ClusteringBodyState extends State { clusterManagerIndex * _clusterManagerLongitudeOffset; final Marker marker = Marker( - clusterManagerId: clusterManager.clusterManagerId, markerId: markerId, + clusterManagerId: clusterManager.clusterManagerId, position: LatLng( center.latitude + _getRandomOffset(), center.longitude + _getRandomOffset() + clusterManagerLongitudeOffset, @@ -210,6 +207,15 @@ class ClusteringBodyState extends State { setState(() {}); } + /// Returns selected or unselected state of the given [marker]. + Marker _copyWithSelectedState(Marker marker, bool isSelected) { + return marker.copyWith( + iconParam: isSelected + ? BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen) + : BitmapDescriptor.defaultMarker, + ); + } + @override Widget build(BuildContext context) { final MarkerId? selectedId = selectedMarker; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/collision_behavior.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/collision_behavior.dart new file mode 100644 index 00000000000..aecbd780140 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/collision_behavior.dart @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; +import 'place_advanced_marker.dart'; + +/// Page demonstrating how to use AdvancedMarker's collision behavior. +class AdvancedMarkerCollisionBehaviorPage extends GoogleMapExampleAppPage { + /// Default constructor. + const AdvancedMarkerCollisionBehaviorPage({Key? key, required this.mapId}) + : super( + const Icon(Icons.not_listed_location), + 'Advanced marker collision behavior', + key: key, + ); + + /// Map ID to use for the GoogleMap. + final String? mapId; + + @override + Widget build(BuildContext context) { + return _CollisionBehaviorPageBody(mapId: mapId); + } +} + +class _CollisionBehaviorPageBody extends StatefulWidget { + const _CollisionBehaviorPageBody({required this.mapId}); + + final String? mapId; + + @override + State<_CollisionBehaviorPageBody> createState() => + _CollisionBehaviorPageBodyState(); +} + +class _CollisionBehaviorPageBodyState + extends State<_CollisionBehaviorPageBody> { + static const LatLng center = LatLng(-33.86711, 151.1947171); + static const double zoomOutLevel = 9; + static const double zoomInLevel = 12; + + MarkerCollisionBehavior markerCollisionBehavior = + MarkerCollisionBehavior.optionalAndHidesLowerPriority; + + ExampleGoogleMapController? controller; + final List markers = []; + + void _addMarkers() { + final List newMarkers = [ + for (int i = 0; i < 12; i++) + AdvancedMarker( + markerId: MarkerId('marker_${i}_$markerCollisionBehavior'), + position: LatLng( + center.latitude + sin(i * pi / 6.0) / 20.0, + center.longitude + cos(i * pi / 6.0) / 20.0, + ), + icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), + collisionBehavior: markerCollisionBehavior, + ), + ]; + + markers.clear(); + markers.addAll(newMarkers); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + AdvancedMarkersCapabilityStatus(controller: controller), + Expanded( + child: ExampleGoogleMap( + mapId: widget.mapId, + markerType: MarkerType.advancedMarker, + initialCameraPosition: const CameraPosition( + target: center, + zoom: zoomInLevel, + ), + markers: Set.of(markers), + tiltGesturesEnabled: false, + zoomGesturesEnabled: false, + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + onMapCreated: (ExampleGoogleMapController controller) { + setState(() { + this.controller = controller; + }); + }, + ), + ), + const SizedBox(height: 12), + Text( + 'Current collision behavior: ${markerCollisionBehavior.name}', + style: Theme.of(context).textTheme.labelLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: () { + setState(() { + _addMarkers(); + }); + }, + child: const Text('Add markers'), + ), + TextButton( + onPressed: () { + controller?.animateCamera( + CameraUpdate.newCameraPosition( + const CameraPosition(target: center, zoom: zoomOutLevel), + ), + ); + }, + child: const Text('Zoom out'), + ), + TextButton( + onPressed: () { + controller?.animateCamera( + CameraUpdate.newCameraPosition( + const CameraPosition(target: center, zoom: zoomInLevel), + ), + ); + }, + child: const Text('Zoom in'), + ), + TextButton( + onPressed: () { + setState(() { + markerCollisionBehavior = + markerCollisionBehavior == + MarkerCollisionBehavior.optionalAndHidesLowerPriority + ? MarkerCollisionBehavior.requiredDisplay + : MarkerCollisionBehavior.optionalAndHidesLowerPriority; + _addMarkers(); + }); + }, + child: const Text('Toggle collision behavior'), + ), + ], + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart index bee30555b2e..4df40931fc1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart @@ -312,11 +312,12 @@ class ExampleGoogleMap extends StatefulWidget { this.onCameraMoveStarted, this.tileOverlays = const {}, this.groundOverlays = const {}, + this.markerType = MarkerType.marker, this.onCameraMove, this.onCameraIdle, this.onTap, this.onLongPress, - this.cloudMapId, + this.mapId, this.style, }); @@ -430,11 +431,14 @@ class ExampleGoogleMap extends StatefulWidget { /// /// See https://developers.google.com/maps/documentation/get-map-id /// for more details. - final String? cloudMapId; + final String? mapId; /// The locally configured style for the map. final String? style; + /// The type of marker to use (legacy or advanced). + final MarkerType markerType; + /// Creates a [State] for this [ExampleGoogleMap]. @override State createState() => _ExampleGoogleMapState(); @@ -680,7 +684,8 @@ MapConfiguration _configurationFromMapWidget(ExampleGoogleMap map) { indoorViewEnabled: map.indoorViewEnabled, trafficEnabled: map.trafficEnabled, buildingsEnabled: map.buildingsEnabled, - cloudMapId: map.cloudMapId, + markerType: map.markerType, + mapId: map.mapId, style: map.style, ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart index b980e1d2a64..e0c7c1f29d4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/main.dart @@ -8,8 +8,11 @@ import 'package:flutter/material.dart'; import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'advanced_marker_icons.dart'; +import 'advanced_markers_clustering.dart'; import 'animate_camera.dart'; import 'clustering.dart'; +import 'collision_behavior.dart'; import 'ground_overlay.dart'; import 'lite_mode.dart'; import 'map_click.dart'; @@ -20,6 +23,7 @@ import 'marker_icons.dart'; import 'move_camera.dart'; import 'padding.dart'; import 'page.dart'; +import 'place_advanced_marker.dart'; import 'place_circle.dart'; import 'place_marker.dart'; import 'place_polygon.dart'; @@ -28,6 +32,10 @@ import 'scrolling_map.dart'; import 'snapshot.dart'; import 'tile_overlay.dart'; +/// Place your map ID here. Map ID is required for pages that use advanced +/// markers. +const String? _mapId = null; + final List _allPages = [ const MapUiPage(), const MapCoordinatesPage(), @@ -35,7 +43,9 @@ final List _allPages = [ const AnimateCameraPage(), const MoveCameraPage(), const PlaceMarkerPage(), + const PlaceAdvancedMarkerPage(mapId: _mapId), const MarkerIconsPage(), + const AdvancedMarkerIconsPage(mapId: _mapId), const ScrollingMapPage(), const PlacePolylinePage(), const PlacePolygonPage(), @@ -46,7 +56,9 @@ final List _allPages = [ const TileOverlayPage(), const GroundOverlayPage(), const ClusteringPage(), + const AdvancedMarkersClusteringPage(mapId: _mapId), const MapIdPage(), + const AdvancedMarkerCollisionBehaviorPage(mapId: _mapId), ]; /// MapsDemo is the Main Application. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_map_id.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_map_id.dart index 039966595ba..d71c7a692df 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_map_id.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_map_id.dart @@ -49,7 +49,7 @@ class MapIdBodyState extends State { super.initState(); } - String _getInitializedsRendererType() { + String _getInitializedRendererType() { switch (_initializedRenderer) { case AndroidMapRenderer.latest: return 'latest'; @@ -80,7 +80,7 @@ class MapIdBodyState extends State { zoom: 7.0, ), key: _key, - cloudMapId: _mapId, + mapId: _mapId, ); final List columnChildren = [ @@ -109,7 +109,7 @@ class MapIdBodyState extends State { padding: const EdgeInsets.all(10.0), child: Text( 'On Android, Cloud-based maps styling only works with "latest" renderer.\n\n' - 'Current initialized renderer is "${_getInitializedsRendererType()}".', + 'Current initialized renderer is "${_getInitializedRendererType()}".', ), ), ]; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart index 40df30c94ac..e9be0889025 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart @@ -21,22 +21,22 @@ class MarkerIconsPage extends GoogleMapExampleAppPage { @override Widget build(BuildContext context) { - return const MarkerIconsBody(); + return const _MarkerIconsBody(); } } -class MarkerIconsBody extends StatefulWidget { - const MarkerIconsBody({super.key}); +class _MarkerIconsBody extends StatefulWidget { + const _MarkerIconsBody(); @override - State createState() => MarkerIconsBodyState(); + State createState() => _MarkerIconsBodyState(); } const LatLng _kMapCenter = LatLng(52.4478, -3.5402); enum _MarkerSizeOption { original, width30, height40, size30x60, size120x60 } -class MarkerIconsBodyState extends State { +class _MarkerIconsBodyState extends State<_MarkerIconsBody> { final Size _markerAssetImageSize = const Size(48, 48); _MarkerSizeOption _currentSizeOption = _MarkerSizeOption.original; Set _markers = {}; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_advanced_marker.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_advanced_marker.dart new file mode 100644 index 00000000000..40415c17aa0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_advanced_marker.dart @@ -0,0 +1,494 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +/// Page demonstrating how to use Advanced [Marker] class. +class PlaceAdvancedMarkerPage extends GoogleMapExampleAppPage { + /// Default constructor. + const PlaceAdvancedMarkerPage({Key? key, required this.mapId}) + : super( + const Icon(Icons.place_outlined), + 'Place advanced marker', + key: key, + ); + + /// Map ID to use for the GoogleMap. + final String? mapId; + + @override + Widget build(BuildContext context) { + return _PlaceAdvancedMarkerBody(mapId: mapId); + } +} + +class _PlaceAdvancedMarkerBody extends StatefulWidget { + const _PlaceAdvancedMarkerBody({required this.mapId}); + + final String? mapId; + + @override + State createState() => _PlaceAdvancedMarkerBodyState(); +} + +class _PlaceAdvancedMarkerBodyState extends State<_PlaceAdvancedMarkerBody> { + _PlaceAdvancedMarkerBodyState(); + static const LatLng center = LatLng(-33.86711, 151.1947171); + + ExampleGoogleMapController? controller; + Map markers = {}; + MarkerId? selectedMarker; + int _markerIdCounter = 1; + LatLng? markerPosition; + + void _onMapCreated(ExampleGoogleMapController controller) { + setState(() { + this.controller = controller; + }); + } + + void _onMarkerTapped(MarkerId markerId) { + final AdvancedMarker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + final MarkerId? previousMarkerId = selectedMarker; + if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { + final AdvancedMarker resetOld = copyWithSelectedState( + markers[previousMarkerId]!, + false, + ); + markers[previousMarkerId] = resetOld; + } + selectedMarker = markerId; + final AdvancedMarker newMarker = copyWithSelectedState( + tappedMarker, + true, + ); + markers[markerId] = newMarker; + + markerPosition = null; + }); + } + } + + Future _onMarkerDrag(MarkerId markerId, LatLng newPosition) async { + setState(() { + markerPosition = newPosition; + }); + } + + Future _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { + final AdvancedMarker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + markerPosition = null; + }); + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 66), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Old position: ${tappedMarker.position}'), + Text('New position: $newPosition'), + ], + ), + ), + ); + }, + ); + } + } + + void _add() { + final int markerCount = markers.length; + + if (markerCount == 12) { + return; + } + + final String markerIdVal = 'marker_id_$_markerIdCounter'; + _markerIdCounter++; + final MarkerId markerId = MarkerId(markerIdVal); + + final AdvancedMarker marker = AdvancedMarker( + markerId: markerId, + position: LatLng( + center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0, + center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, + ), + icon: _getMarkerBitmapDescriptor(isSelected: false), + infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), + onTap: () => _onMarkerTapped(markerId), + onDrag: (LatLng position) => _onMarkerDrag(markerId, position), + onDragEnd: (LatLng position) => _onMarkerDragEnd(markerId, position), + ); + + setState(() { + markers[markerId] = marker; + }); + } + + BitmapDescriptor _getMarkerBitmapDescriptor({required bool isSelected}) { + return BitmapDescriptor.pinConfig( + backgroundColor: isSelected ? Colors.blue : Colors.white, + borderColor: isSelected ? Colors.white : Colors.blue, + glyph: CircleGlyph(color: isSelected ? Colors.white : Colors.blue), + ); + } + + void _remove(MarkerId markerId) { + setState(() { + if (markers.containsKey(markerId)) { + markers.remove(markerId); + } + }); + } + + void _changePosition(MarkerId markerId) { + final AdvancedMarker marker = markers[markerId]!; + final LatLng current = marker.position; + final Offset offset = Offset( + center.latitude - current.latitude, + center.longitude - current.longitude, + ); + setState(() { + markers[markerId] = marker.copyWith( + positionParam: LatLng( + center.latitude + offset.dy, + center.longitude + offset.dx, + ), + ); + }); + } + + void _changeAnchor(MarkerId markerId) { + final AdvancedMarker marker = markers[markerId]!; + final Offset currentAnchor = marker.anchor; + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + setState(() { + markers[markerId] = marker.copyWith(anchorParam: newAnchor); + }); + } + + Future _changeInfoAnchor(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + final Offset currentAnchor = marker.infoWindow.anchor; + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + setState(() { + markers[markerId] = marker.copyWith( + infoWindowParam: marker.infoWindow.copyWith(anchorParam: newAnchor), + ); + }); + } + + Future _toggleDraggable(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith(draggableParam: !marker.draggable); + }); + } + + Future _toggleFlat(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith(flatParam: !marker.flat); + }); + } + + Future _changeInfo(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + final String newSnippet = '${marker.infoWindow.snippet!}*'; + setState(() { + markers[markerId] = marker.copyWith( + infoWindowParam: marker.infoWindow.copyWith(snippetParam: newSnippet), + ); + }); + } + + Future _changeAlpha(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + final double current = marker.alpha; + setState(() { + markers[markerId] = marker.copyWith( + alphaParam: current < 0.1 ? 1.0 : current * 0.75, + ); + }); + } + + Future _changeRotation(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + final double current = marker.rotation; + setState(() { + markers[markerId] = marker.copyWith( + rotationParam: current == 330.0 ? 0.0 : current + 30.0, + ); + }); + } + + Future _toggleVisible(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith(visibleParam: !marker.visible); + }); + } + + Future _changeZIndex(MarkerId markerId) async { + final AdvancedMarker marker = markers[markerId]!; + final double current = marker.zIndex; + setState(() { + markers[markerId] = marker.copyWith( + zIndexParam: current == 12.0 ? 0.0 : current + 1.0, + ); + }); + } + + void _setMarkerIcon(MarkerId markerId, BitmapDescriptor assetIcon) { + final AdvancedMarker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith(iconParam: assetIcon); + }); + } + + BitmapDescriptor _getMarkerIcon(BuildContext context) { + return BitmapDescriptor.pinConfig( + backgroundColor: Colors.red, + borderColor: Colors.red, + glyph: const TextGlyph(text: 'Hi!', textColor: Colors.white), + ); + } + + /// Performs customizations of the [marker] to mark it as selected or not. + AdvancedMarker copyWithSelectedState(AdvancedMarker marker, bool isSelected) { + return marker.copyWith( + iconParam: _getMarkerBitmapDescriptor(isSelected: isSelected), + ); + } + + @override + Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; + return Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AdvancedMarkersCapabilityStatus(controller: controller), + Expanded( + child: ExampleGoogleMap( + mapId: widget.mapId, + markerType: MarkerType.advancedMarker, + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11.0, + ), + markers: Set.of(markers.values), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton(onPressed: _add, child: const Text('Add')), + TextButton( + onPressed: selectedId == null + ? null + : () => _remove(selectedId), + child: const Text('Remove'), + ), + ], + ), + Wrap( + alignment: WrapAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: selectedId == null + ? null + : () => _changeInfo(selectedId), + child: const Text('change info'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeInfoAnchor(selectedId), + child: const Text('change info anchor'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeAlpha(selectedId), + child: const Text('change alpha'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeAnchor(selectedId), + child: const Text('change anchor'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _toggleDraggable(selectedId), + child: const Text('toggle draggable'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _toggleFlat(selectedId), + child: const Text('toggle flat'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changePosition(selectedId), + child: const Text('change position'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeRotation(selectedId), + child: const Text('change rotation'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _toggleVisible(selectedId), + child: const Text('toggle visible'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => _changeZIndex(selectedId), + child: const Text('change zIndex'), + ), + TextButton( + onPressed: selectedId == null + ? null + : () => + _setMarkerIcon(selectedId, _getMarkerIcon(context)), + child: const Text('set glyph text'), + ), + ], + ), + ], + ), + Visibility( + visible: markerPosition != null, + child: Container( + color: Colors.white70, + height: 30, + padding: const EdgeInsets.only(left: 12, right: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (markerPosition == null) + Container() + else + Expanded(child: Text('lat: ${markerPosition!.latitude}')), + if (markerPosition == null) + Container() + else + Expanded(child: Text('lng: ${markerPosition!.longitude}')), + ], + ), + ), + ), + ], + ); + } +} + +/// Widget displaying the status of advanced markers capability check. +class AdvancedMarkersCapabilityStatus extends StatefulWidget { + /// Default constructor. + const AdvancedMarkersCapabilityStatus({super.key, required this.controller}); + + /// Controller of the map to check for advanced markers capability. + final ExampleGoogleMapController? controller; + + @override + State createState() => + _AdvancedMarkersCapabilityStatusState(); +} + +class _AdvancedMarkersCapabilityStatusState + extends State { + /// Whether map supports advanced markers. + bool? _isAdvancedMarkersAvailable; + + /// Whether a capability check is in progress. + bool _isFetching = false; + + @override + void didUpdateWidget(covariant AdvancedMarkersCapabilityStatus oldWidget) { + super.didUpdateWidget(oldWidget); + _checkCapabilityIfNeeded(); + } + + @override + void initState() { + super.initState(); + _checkCapabilityIfNeeded(); + } + + void _checkCapabilityIfNeeded() { + final ExampleGoogleMapController? controller = widget.controller; + if (controller != null && + _isAdvancedMarkersAvailable == null && + !_isFetching) { + _isFetching = true; + GoogleMapsFlutterPlatform.instance + .isAdvancedMarkersAvailable(mapId: controller.mapId) + .then((bool result) { + if (!mounted) { + return; + } + setState(() { + _isAdvancedMarkersAvailable = result; + _isFetching = false; + }); + }); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Text( + switch (_isAdvancedMarkersAvailable) { + null => 'Checking map capabilities…', + true => + 'Map capabilities check result:\nthis map supports advanced markers', + false => + "Map capabilities check result:\nthis map doesn't support advanced markers. Please check that map ID is provided and correct map renderer is used", + }, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: switch (_isAdvancedMarkersAvailable) { + true => Colors.green.shade700, + false => Colors.red, + null => Colors.black, + }, + ), + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart index e32b7b27747..68deb28940a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart @@ -21,21 +21,21 @@ class PlaceMarkerPage extends GoogleMapExampleAppPage { @override Widget build(BuildContext context) { - return const PlaceMarkerBody(); + return const _PlaceMarkerBody(); } } -class PlaceMarkerBody extends StatefulWidget { - const PlaceMarkerBody({super.key}); +class _PlaceMarkerBody extends StatefulWidget { + const _PlaceMarkerBody(); @override - State createState() => PlaceMarkerBodyState(); + State createState() => _PlaceMarkerBodyState(); } typedef MarkerUpdateAction = Marker Function(Marker marker); -class PlaceMarkerBodyState extends State { - PlaceMarkerBodyState(); +class _PlaceMarkerBodyState extends State<_PlaceMarkerBody> { + _PlaceMarkerBodyState(); static const LatLng center = LatLng(-33.86711, 151.1947171); ExampleGoogleMapController? controller; @@ -44,9 +44,10 @@ class PlaceMarkerBodyState extends State { int _markerIdCounter = 1; LatLng? markerPosition; - // ignore: use_setters_to_change_properties void _onMapCreated(ExampleGoogleMapController controller) { - this.controller = controller; + setState(() { + this.controller = controller; + }); } @override @@ -60,17 +61,14 @@ class PlaceMarkerBodyState extends State { setState(() { final MarkerId? previousMarkerId = selectedMarker; if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { - final Marker resetOld = markers[previousMarkerId]!.copyWith( - iconParam: BitmapDescriptor.defaultMarker, + final Marker resetOld = _copyWithSelectedState( + markers[previousMarkerId]!, + false, ); markers[previousMarkerId] = resetOld; } selectedMarker = markerId; - final Marker newMarker = tappedMarker.copyWith( - iconParam: BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueGreen, - ), - ); + final Marker newMarker = _copyWithSelectedState(tappedMarker, true); markers[markerId] = newMarker; markerPosition = null; @@ -127,7 +125,7 @@ class PlaceMarkerBodyState extends State { _markerIdCounter++; final MarkerId markerId = MarkerId(markerIdVal); - final Marker marker = Marker( + final Marker marker = createMarker( markerId: markerId, position: LatLng( center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0, @@ -263,6 +261,34 @@ class PlaceMarkerBodyState extends State { return BytesMapBitmap(bytes.buffer.asUint8List()); } + /// Creates a marker with given parameters + Marker createMarker({ + required MarkerId markerId, + required LatLng position, + required InfoWindow infoWindow, + required VoidCallback onTap, + required ValueChanged? onDragEnd, + required ValueChanged? onDrag, + }) { + return Marker( + markerId: markerId, + position: position, + infoWindow: infoWindow, + onTap: onTap, + onDrag: onDrag, + onDragEnd: onDragEnd, + ); + } + + /// Performs customizations of the [marker] to mark it as selected or not. + Marker _copyWithSelectedState(Marker marker, bool isSelected) { + return marker.copyWith( + iconParam: isSelected + ? BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen) + : BitmapDescriptor.defaultMarker, + ); + } + @override Widget build(BuildContext context) { final MarkerId? selectedId = selectedMarker; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index e556fa6acf9..58cec3dcf1a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_maps_flutter_platform_interface: ^2.11.0 + google_maps_flutter_platform_interface: ^2.13.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index 34d05ca6f8f..a0490d0e85c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -501,6 +501,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { : _setStyleFailureMessage; } + @override + Future isAdvancedMarkersAvailable({required int mapId}) async { + return _hostApi(mapId).isAdvancedMarkersAvailable(); + } + /// Set [GoogleMapsFlutterPlatform] to use [AndroidViewSurface] to build the /// Google Maps widget. /// @@ -825,6 +830,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { zIndex: marker.zIndex, markerId: marker.markerId.value, clusterManagerId: marker.clusterManagerId?.value, + collisionBehavior: marker is AdvancedMarker + ? platformMarkerCollisionBehaviorFromMarkerCollisionBehavior( + marker.collisionBehavior, + ) + : PlatformMarkerCollisionBehavior.requiredDisplay, ); } @@ -1064,6 +1074,36 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { height: bytes.height, ), ); + case final PinConfig pinConfig: + final AdvancedMarkerGlyph? glyph = pinConfig.glyph; + Color? glyphColor; + String? glyphText; + Color? glyphTextColor; + BitmapDescriptor? glyphBitmapDescriptor; + switch (glyph) { + case final CircleGlyph circleGlyph: + glyphColor = circleGlyph.color; + case final TextGlyph textGlyph: + glyphText = textGlyph.text; + glyphTextColor = textGlyph.textColor; + case final BitmapGlyph bitmapGlyph: + glyphBitmapDescriptor = bitmapGlyph.bitmap; + case null: + break; + } + + return PlatformBitmap( + bitmap: PlatformBitmapPinConfig( + backgroundColor: pinConfig.backgroundColor?.value, + borderColor: pinConfig.borderColor?.value, + glyphColor: glyphColor?.value, + glyphText: glyphText, + glyphTextColor: glyphTextColor?.value, + glyphBitmap: glyphBitmapDescriptor != null + ? platformBitmapFromBitmapDescriptor(glyphBitmapDescriptor) + : null, + ), + ); default: throw ArgumentError( 'Unrecognized type of bitmap ${bitmap.runtimeType}', @@ -1346,6 +1386,14 @@ PlatformEdgeInsets? _platformEdgeInsetsFromEdgeInsets(EdgeInsets? insets) { ); } +PlatformMarkerType? _platformMarkerTypeFromMarkerType(MarkerType? markerType) { + return switch (markerType) { + null => null, + MarkerType.marker => PlatformMarkerType.marker, + MarkerType.advancedMarker => PlatformMarkerType.advancedMarker, + }; +} + PlatformMapConfiguration _platformMapConfigurationFromMapConfiguration( MapConfiguration config, ) { @@ -1372,7 +1420,8 @@ PlatformMapConfiguration _platformMapConfigurationFromMapConfiguration( trafficEnabled: config.trafficEnabled, buildingsEnabled: config.buildingsEnabled, liteModeEnabled: config.liteModeEnabled, - cloudMapId: config.cloudMapId, + markerType: _platformMarkerTypeFromMarkerType(config.markerType), + mapId: config.mapId, style: config.style, ); } @@ -1417,7 +1466,8 @@ PlatformMapConfiguration _platformMapConfigurationFromOptionsJson( trafficEnabled: options['trafficEnabled'] as bool?, buildingsEnabled: options['buildingsEnabled'] as bool?, liteModeEnabled: options['liteModeEnabled'] as bool?, - cloudMapId: options['cloudMapId'] as String?, + markerType: PlatformMarkerType.marker, + mapId: options['mapId'] as String?, style: options['style'] as String?, ); } @@ -1541,6 +1591,23 @@ PlatformPatternItem platformPatternItemFromPatternItem(PatternItem item) { return PlatformPatternItem(type: PlatformPatternItemType.dot); } +/// Converts a MarkerCollisionBehavior to Pigeon's +/// PlatformMarkerCollisionBehavior. +@visibleForTesting +PlatformMarkerCollisionBehavior +platformMarkerCollisionBehaviorFromMarkerCollisionBehavior( + MarkerCollisionBehavior markerCollisionBehavior, +) { + switch (markerCollisionBehavior) { + case MarkerCollisionBehavior.requiredDisplay: + return PlatformMarkerCollisionBehavior.requiredDisplay; + case MarkerCollisionBehavior.optionalAndHidesLowerPriority: + return PlatformMarkerCollisionBehavior.optionalAndHidesLowerPriority; + case MarkerCollisionBehavior.requiredAndHidesOptional: + return PlatformMarkerCollisionBehavior.requiredAndHidesOptional; + } +} + /// Update specification for a set of [TileOverlay]s. // TODO(stuartmorgan): Fix the missing export of this class in the platform // interface, and remove this copy. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart index 04d02cb3439..b75de5b3d6e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart @@ -37,6 +37,12 @@ enum PlatformMapType { none, normal, satellite, terrain, hybrid } enum PlatformRendererType { legacy, latest } +enum PlatformMarkerCollisionBehavior { + requiredDisplay, + optionalAndHidesLowerPriority, + requiredAndHidesOptional, +} + /// Join types for polyline joints. enum PlatformJointType { mitered, bevel, round } @@ -48,6 +54,8 @@ enum PlatformCapType { buttCap, roundCap, squareCap, customCap } /// Enumeration of possible types for PatternItem. enum PlatformPatternItemType { dot, dash, gap } +enum PlatformMarkerType { marker, advancedMarker } + /// Pigeon equivalent of [MapBitmapScaling]. enum PlatformMapBitmapScaling { auto, none } @@ -415,6 +423,7 @@ class PlatformMarker { this.zIndex = 0.0, required this.markerId, this.clusterManagerId, + this.collisionBehavior = PlatformMarkerCollisionBehavior.requiredDisplay, }); double alpha; @@ -443,6 +452,8 @@ class PlatformMarker { String? clusterManagerId; + PlatformMarkerCollisionBehavior collisionBehavior; + Object encode() { return [ alpha, @@ -458,6 +469,7 @@ class PlatformMarker { zIndex, markerId, clusterManagerId, + collisionBehavior, ]; } @@ -477,6 +489,7 @@ class PlatformMarker { zIndex: result[10]! as double, markerId: result[11]! as String, clusterManagerId: result[12] as String?, + collisionBehavior: result[13]! as PlatformMarkerCollisionBehavior, ); } } @@ -1041,7 +1054,8 @@ class PlatformMapConfiguration { this.trafficEnabled, this.buildingsEnabled, this.liteModeEnabled, - this.cloudMapId, + this.markerType, + this.mapId, this.style, }); @@ -1081,7 +1095,9 @@ class PlatformMapConfiguration { bool? liteModeEnabled; - String? cloudMapId; + PlatformMarkerType? markerType; + + String? mapId; String? style; @@ -1105,7 +1121,8 @@ class PlatformMapConfiguration { trafficEnabled, buildingsEnabled, liteModeEnabled, - cloudMapId, + markerType, + mapId, style, ]; } @@ -1131,8 +1148,9 @@ class PlatformMapConfiguration { trafficEnabled: result[15] as bool?, buildingsEnabled: result[16] as bool?, liteModeEnabled: result[17] as bool?, - cloudMapId: result[18] as String?, - style: result[19] as String?, + markerType: result[18] as PlatformMarkerType?, + mapId: result[19] as String?, + style: result[20] as String?, ); } } @@ -1397,6 +1415,53 @@ class PlatformBitmapBytesMap { } } +/// Pigeon equivalent of [PinConfig]. +class PlatformBitmapPinConfig { + PlatformBitmapPinConfig({ + this.backgroundColor, + this.borderColor, + this.glyphColor, + this.glyphBitmap, + this.glyphText, + this.glyphTextColor, + }); + + int? backgroundColor; + + int? borderColor; + + int? glyphColor; + + PlatformBitmap? glyphBitmap; + + String? glyphText; + + int? glyphTextColor; + + Object encode() { + return [ + backgroundColor, + borderColor, + glyphColor, + glyphBitmap, + glyphText, + glyphTextColor, + ]; + } + + static PlatformBitmapPinConfig decode(Object result) { + result as List; + return PlatformBitmapPinConfig( + backgroundColor: result[0] as int?, + borderColor: result[1] as int?, + glyphColor: result[2] as int?, + glyphBitmap: result[3] as PlatformBitmap?, + glyphText: result[4] as String?, + glyphTextColor: result[5] as int?, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -1410,137 +1475,146 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformRendererType) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is PlatformJointType) { + } else if (value is PlatformMarkerCollisionBehavior) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is PlatformCapType) { + } else if (value is PlatformJointType) { buffer.putUint8(132); writeValue(buffer, value.index); - } else if (value is PlatformPatternItemType) { + } else if (value is PlatformCapType) { buffer.putUint8(133); writeValue(buffer, value.index); - } else if (value is PlatformMapBitmapScaling) { + } else if (value is PlatformPatternItemType) { buffer.putUint8(134); writeValue(buffer, value.index); - } else if (value is PlatformCameraPosition) { + } else if (value is PlatformMarkerType) { buffer.putUint8(135); + writeValue(buffer, value.index); + } else if (value is PlatformMapBitmapScaling) { + buffer.putUint8(136); + writeValue(buffer, value.index); + } else if (value is PlatformCameraPosition) { + buffer.putUint8(137); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdate) { - buffer.putUint8(136); + buffer.putUint8(138); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewCameraPosition) { - buffer.putUint8(137); + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewLatLng) { - buffer.putUint8(138); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewLatLngBounds) { - buffer.putUint8(139); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewLatLngZoom) { - buffer.putUint8(140); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateScrollBy) { - buffer.putUint8(141); + buffer.putUint8(143); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateZoomBy) { - buffer.putUint8(142); + buffer.putUint8(144); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateZoom) { - buffer.putUint8(143); + buffer.putUint8(145); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateZoomTo) { - buffer.putUint8(144); + buffer.putUint8(146); writeValue(buffer, value.encode()); } else if (value is PlatformCircle) { - buffer.putUint8(145); + buffer.putUint8(147); writeValue(buffer, value.encode()); } else if (value is PlatformHeatmap) { - buffer.putUint8(146); + buffer.putUint8(148); writeValue(buffer, value.encode()); } else if (value is PlatformClusterManager) { - buffer.putUint8(147); + buffer.putUint8(149); writeValue(buffer, value.encode()); } else if (value is PlatformDoublePair) { - buffer.putUint8(148); + buffer.putUint8(150); writeValue(buffer, value.encode()); } else if (value is PlatformInfoWindow) { - buffer.putUint8(149); + buffer.putUint8(151); writeValue(buffer, value.encode()); } else if (value is PlatformMarker) { - buffer.putUint8(150); + buffer.putUint8(152); writeValue(buffer, value.encode()); } else if (value is PlatformPolygon) { - buffer.putUint8(151); + buffer.putUint8(153); writeValue(buffer, value.encode()); } else if (value is PlatformPolyline) { - buffer.putUint8(152); + buffer.putUint8(154); writeValue(buffer, value.encode()); } else if (value is PlatformCap) { - buffer.putUint8(153); + buffer.putUint8(155); writeValue(buffer, value.encode()); } else if (value is PlatformPatternItem) { - buffer.putUint8(154); + buffer.putUint8(156); writeValue(buffer, value.encode()); } else if (value is PlatformTile) { - buffer.putUint8(155); + buffer.putUint8(157); writeValue(buffer, value.encode()); } else if (value is PlatformTileOverlay) { - buffer.putUint8(156); + buffer.putUint8(158); writeValue(buffer, value.encode()); } else if (value is PlatformEdgeInsets) { - buffer.putUint8(157); + buffer.putUint8(159); writeValue(buffer, value.encode()); } else if (value is PlatformLatLng) { - buffer.putUint8(158); + buffer.putUint8(160); writeValue(buffer, value.encode()); } else if (value is PlatformLatLngBounds) { - buffer.putUint8(159); + buffer.putUint8(161); writeValue(buffer, value.encode()); } else if (value is PlatformCluster) { - buffer.putUint8(160); + buffer.putUint8(162); writeValue(buffer, value.encode()); } else if (value is PlatformGroundOverlay) { - buffer.putUint8(161); + buffer.putUint8(163); writeValue(buffer, value.encode()); } else if (value is PlatformCameraTargetBounds) { - buffer.putUint8(162); + buffer.putUint8(164); writeValue(buffer, value.encode()); } else if (value is PlatformMapViewCreationParams) { - buffer.putUint8(163); + buffer.putUint8(165); writeValue(buffer, value.encode()); } else if (value is PlatformMapConfiguration) { - buffer.putUint8(164); + buffer.putUint8(166); writeValue(buffer, value.encode()); } else if (value is PlatformPoint) { - buffer.putUint8(165); + buffer.putUint8(167); writeValue(buffer, value.encode()); } else if (value is PlatformTileLayer) { - buffer.putUint8(166); + buffer.putUint8(168); writeValue(buffer, value.encode()); } else if (value is PlatformZoomRange) { - buffer.putUint8(167); + buffer.putUint8(169); writeValue(buffer, value.encode()); } else if (value is PlatformBitmap) { - buffer.putUint8(168); + buffer.putUint8(170); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapDefaultMarker) { - buffer.putUint8(169); + buffer.putUint8(171); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapBytes) { - buffer.putUint8(170); + buffer.putUint8(172); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapAsset) { - buffer.putUint8(171); + buffer.putUint8(173); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapAssetImage) { - buffer.putUint8(172); + buffer.putUint8(174); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapAssetMap) { - buffer.putUint8(173); + buffer.putUint8(175); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapBytesMap) { - buffer.putUint8(174); + buffer.putUint8(176); + writeValue(buffer, value.encode()); + } else if (value is PlatformBitmapPinConfig) { + buffer.putUint8(177); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -1558,96 +1632,106 @@ class _PigeonCodec extends StandardMessageCodec { return value == null ? null : PlatformRendererType.values[value]; case 131: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformJointType.values[value]; + return value == null + ? null + : PlatformMarkerCollisionBehavior.values[value]; case 132: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformCapType.values[value]; + return value == null ? null : PlatformJointType.values[value]; case 133: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformPatternItemType.values[value]; + return value == null ? null : PlatformCapType.values[value]; case 134: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformMapBitmapScaling.values[value]; + return value == null ? null : PlatformPatternItemType.values[value]; case 135: - return PlatformCameraPosition.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformMarkerType.values[value]; case 136: - return PlatformCameraUpdate.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformMapBitmapScaling.values[value]; case 137: - return PlatformCameraUpdateNewCameraPosition.decode(readValue(buffer)!); + return PlatformCameraPosition.decode(readValue(buffer)!); case 138: - return PlatformCameraUpdateNewLatLng.decode(readValue(buffer)!); + return PlatformCameraUpdate.decode(readValue(buffer)!); case 139: - return PlatformCameraUpdateNewLatLngBounds.decode(readValue(buffer)!); + return PlatformCameraUpdateNewCameraPosition.decode(readValue(buffer)!); case 140: - return PlatformCameraUpdateNewLatLngZoom.decode(readValue(buffer)!); + return PlatformCameraUpdateNewLatLng.decode(readValue(buffer)!); case 141: - return PlatformCameraUpdateScrollBy.decode(readValue(buffer)!); + return PlatformCameraUpdateNewLatLngBounds.decode(readValue(buffer)!); case 142: - return PlatformCameraUpdateZoomBy.decode(readValue(buffer)!); + return PlatformCameraUpdateNewLatLngZoom.decode(readValue(buffer)!); case 143: - return PlatformCameraUpdateZoom.decode(readValue(buffer)!); + return PlatformCameraUpdateScrollBy.decode(readValue(buffer)!); case 144: - return PlatformCameraUpdateZoomTo.decode(readValue(buffer)!); + return PlatformCameraUpdateZoomBy.decode(readValue(buffer)!); case 145: - return PlatformCircle.decode(readValue(buffer)!); + return PlatformCameraUpdateZoom.decode(readValue(buffer)!); case 146: - return PlatformHeatmap.decode(readValue(buffer)!); + return PlatformCameraUpdateZoomTo.decode(readValue(buffer)!); case 147: - return PlatformClusterManager.decode(readValue(buffer)!); + return PlatformCircle.decode(readValue(buffer)!); case 148: - return PlatformDoublePair.decode(readValue(buffer)!); + return PlatformHeatmap.decode(readValue(buffer)!); case 149: - return PlatformInfoWindow.decode(readValue(buffer)!); + return PlatformClusterManager.decode(readValue(buffer)!); case 150: - return PlatformMarker.decode(readValue(buffer)!); + return PlatformDoublePair.decode(readValue(buffer)!); case 151: - return PlatformPolygon.decode(readValue(buffer)!); + return PlatformInfoWindow.decode(readValue(buffer)!); case 152: - return PlatformPolyline.decode(readValue(buffer)!); + return PlatformMarker.decode(readValue(buffer)!); case 153: - return PlatformCap.decode(readValue(buffer)!); + return PlatformPolygon.decode(readValue(buffer)!); case 154: - return PlatformPatternItem.decode(readValue(buffer)!); + return PlatformPolyline.decode(readValue(buffer)!); case 155: - return PlatformTile.decode(readValue(buffer)!); + return PlatformCap.decode(readValue(buffer)!); case 156: - return PlatformTileOverlay.decode(readValue(buffer)!); + return PlatformPatternItem.decode(readValue(buffer)!); case 157: - return PlatformEdgeInsets.decode(readValue(buffer)!); + return PlatformTile.decode(readValue(buffer)!); case 158: - return PlatformLatLng.decode(readValue(buffer)!); + return PlatformTileOverlay.decode(readValue(buffer)!); case 159: - return PlatformLatLngBounds.decode(readValue(buffer)!); + return PlatformEdgeInsets.decode(readValue(buffer)!); case 160: - return PlatformCluster.decode(readValue(buffer)!); + return PlatformLatLng.decode(readValue(buffer)!); case 161: - return PlatformGroundOverlay.decode(readValue(buffer)!); + return PlatformLatLngBounds.decode(readValue(buffer)!); case 162: - return PlatformCameraTargetBounds.decode(readValue(buffer)!); + return PlatformCluster.decode(readValue(buffer)!); case 163: - return PlatformMapViewCreationParams.decode(readValue(buffer)!); + return PlatformGroundOverlay.decode(readValue(buffer)!); case 164: - return PlatformMapConfiguration.decode(readValue(buffer)!); + return PlatformCameraTargetBounds.decode(readValue(buffer)!); case 165: - return PlatformPoint.decode(readValue(buffer)!); + return PlatformMapViewCreationParams.decode(readValue(buffer)!); case 166: - return PlatformTileLayer.decode(readValue(buffer)!); + return PlatformMapConfiguration.decode(readValue(buffer)!); case 167: - return PlatformZoomRange.decode(readValue(buffer)!); + return PlatformPoint.decode(readValue(buffer)!); case 168: - return PlatformBitmap.decode(readValue(buffer)!); + return PlatformTileLayer.decode(readValue(buffer)!); case 169: - return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); + return PlatformZoomRange.decode(readValue(buffer)!); case 170: - return PlatformBitmapBytes.decode(readValue(buffer)!); + return PlatformBitmap.decode(readValue(buffer)!); case 171: - return PlatformBitmapAsset.decode(readValue(buffer)!); + return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); case 172: - return PlatformBitmapAssetImage.decode(readValue(buffer)!); + return PlatformBitmapBytes.decode(readValue(buffer)!); case 173: - return PlatformBitmapAssetMap.decode(readValue(buffer)!); + return PlatformBitmapAsset.decode(readValue(buffer)!); case 174: + return PlatformBitmapAssetImage.decode(readValue(buffer)!); + case 175: + return PlatformBitmapAssetMap.decode(readValue(buffer)!); + case 176: return PlatformBitmapBytesMap.decode(readValue(buffer)!); + case 177: + return PlatformBitmapPinConfig.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -2296,6 +2380,39 @@ class MapsApi { } } + /// Returns true if this map supports advanced markers. + /// + /// This allows checking if the map supports advanced markers before + /// attempting to use them. + Future isAdvancedMarkersAvailable() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.MapsApi.isAdvancedMarkersAvailable$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + /// Clears the cache of tiles previously requseted from the tile provider. Future clearTileCache(String tileOverlayId) async { final String pigeonVar_channelName = diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart index bcb3e418c02..1802934c2ab 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart @@ -176,6 +176,7 @@ class PlatformMarker { this.visible = true, this.zIndex = 0.0, this.clusterManagerId, + this.collisionBehavior = PlatformMarkerCollisionBehavior.requiredDisplay, }); final double alpha; @@ -192,6 +193,14 @@ class PlatformMarker { final double zIndex; final String markerId; final String? clusterManagerId; + + final PlatformMarkerCollisionBehavior collisionBehavior; +} + +enum PlatformMarkerCollisionBehavior { + requiredDisplay, + optionalAndHidesLowerPriority, + requiredAndHidesOptional, } /// Pigeon equivalent of the Polygon class. @@ -432,6 +441,8 @@ class PlatformMapViewCreationParams { final List initialGroundOverlays; } +enum PlatformMarkerType { marker, advancedMarker } + /// Pigeon equivalent of MapConfiguration. class PlatformMapConfiguration { PlatformMapConfiguration({ @@ -453,7 +464,8 @@ class PlatformMapConfiguration { required this.trafficEnabled, required this.buildingsEnabled, required this.liteModeEnabled, - required this.cloudMapId, + required this.markerType, + required this.mapId, required this.style, }); @@ -475,7 +487,8 @@ class PlatformMapConfiguration { final bool? trafficEnabled; final bool? buildingsEnabled; final bool? liteModeEnabled; - final String? cloudMapId; + final PlatformMarkerType? markerType; + final String? mapId; final String? style; } @@ -602,6 +615,26 @@ class PlatformBitmapBytesMap { final double? height; } +/// Pigeon equivalent of [PinConfig]. +class PlatformBitmapPinConfig { + PlatformBitmapPinConfig({ + required this.backgroundColor, + required this.borderColor, + required this.glyphColor, + required this.glyphBitmap, + required this.glyphText, + required this.glyphTextColor, + }); + + final int? backgroundColor; + final int? borderColor; + final int? glyphColor; + final PlatformBitmap? glyphBitmap; + + final String? glyphText; + final int? glyphTextColor; +} + /// Interface for non-test interactions with the native SDK. /// /// For test-only state queries, see [MapsInspectorApi]. @@ -719,6 +752,12 @@ abstract class MapsApi { /// is no way to return failures from map initialization. bool didLastStyleSucceed(); + /// Returns true if this map supports advanced markers. + /// + /// This allows checking if the map supports advanced markers before + /// attempting to use them. + bool isAdvancedMarkersAvailable(); + /// Clears the cache of tiles previously requseted from the tile provider. void clearTileCache(String tileOverlayId); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index 982c86fb901..27d65a36e06 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.18.4 +version: 2.19.0 environment: sdk: ^3.9.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.11.0 + google_maps_flutter_platform_interface: ^2.13.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart index 3569b0098c3..9b0bf327aea 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart @@ -323,6 +323,19 @@ void main() { verify(api.clearTileCache(tileOverlayId)); }); + test('isAdvancedMarkersAvailable calls through', () async { + const int mapId = 1; + final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = setUpMockMap( + mapId: mapId, + ); + when(api.isAdvancedMarkersAvailable()).thenAnswer((_) async => true); + + await maps.isAdvancedMarkersAvailable(mapId: mapId); + final bool isAdvancedMarkersAvailable = await api + .isAdvancedMarkersAvailable(); + expect(isAdvancedMarkersAvailable, isTrue); + }); + test('updateMapConfiguration passes expected arguments', () async { const int mapId = 1; final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = setUpMockMap( @@ -609,6 +622,116 @@ void main() { } }); + test('updateMarkers passes expected arguments (AdvancedMarkers)', () async { + const int mapId = 1; + final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = setUpMockMap( + mapId: mapId, + ); + + final AdvancedMarker object1 = AdvancedMarker( + markerId: const MarkerId('1'), + ); + final AdvancedMarker object2old = AdvancedMarker( + markerId: const MarkerId('2'), + ); + final AdvancedMarker object2new = object2old.copyWith( + rotationParam: 42, + collisionBehaviorParam: + MarkerCollisionBehavior.optionalAndHidesLowerPriority, + ); + final AdvancedMarker object3 = AdvancedMarker( + markerId: const MarkerId('3'), + collisionBehavior: MarkerCollisionBehavior.requiredAndHidesOptional, + ); + await maps.updateMarkers( + MarkerUpdates.from( + {object1, object2old}, + {object2new, object3}, + ), + mapId: mapId, + ); + + final VerificationResult verification = verify( + api.updateMarkers(captureAny, captureAny, captureAny), + ); + final List toAdd = + verification.captured[0] as List; + final List toChange = + verification.captured[1] as List; + final List toRemove = verification.captured[2] as List; + // Object one should be removed. + expect(toRemove.length, 1); + expect(toRemove.first, object1.markerId.value); + // Object two should be changed. + { + expect(toChange.length, 1); + final PlatformMarker firstChanged = toChange.first; + expect(firstChanged.alpha, object2new.alpha); + expect(firstChanged.anchor.x, object2new.anchor.dx); + expect(firstChanged.anchor.y, object2new.anchor.dy); + expect(firstChanged.consumeTapEvents, object2new.consumeTapEvents); + expect(firstChanged.draggable, object2new.draggable); + expect(firstChanged.flat, object2new.flat); + expect( + firstChanged.icon.bitmap.runtimeType, + GoogleMapsFlutterAndroid.platformBitmapFromBitmapDescriptor( + object2new.icon, + ).bitmap.runtimeType, + ); + expect(firstChanged.infoWindow.title, object2new.infoWindow.title); + expect(firstChanged.infoWindow.snippet, object2new.infoWindow.snippet); + expect(firstChanged.infoWindow.anchor.x, object2new.infoWindow.anchor.dx); + expect(firstChanged.infoWindow.anchor.y, object2new.infoWindow.anchor.dy); + expect(firstChanged.position.latitude, object2new.position.latitude); + expect(firstChanged.position.longitude, object2new.position.longitude); + expect(firstChanged.rotation, object2new.rotation); + expect(firstChanged.visible, object2new.visible); + expect(firstChanged.zIndex, object2new.zIndex); + expect(firstChanged.markerId, object2new.markerId.value); + expect(firstChanged.clusterManagerId, object2new.clusterManagerId?.value); + expect( + firstChanged.collisionBehavior, + platformMarkerCollisionBehaviorFromMarkerCollisionBehavior( + object2new.collisionBehavior, + ), + ); + } + // Object 3 should be added. + { + expect(toAdd.length, 1); + final PlatformMarker firstAdded = toAdd.first; + expect(firstAdded.alpha, object3.alpha); + expect(firstAdded.anchor.x, object3.anchor.dx); + expect(firstAdded.anchor.y, object3.anchor.dy); + expect(firstAdded.consumeTapEvents, object3.consumeTapEvents); + expect(firstAdded.draggable, object3.draggable); + expect(firstAdded.flat, object3.flat); + expect( + firstAdded.icon.bitmap.runtimeType, + GoogleMapsFlutterAndroid.platformBitmapFromBitmapDescriptor( + object3.icon, + ).bitmap.runtimeType, + ); + expect(firstAdded.infoWindow.title, object3.infoWindow.title); + expect(firstAdded.infoWindow.snippet, object3.infoWindow.snippet); + expect(firstAdded.infoWindow.anchor.x, object3.infoWindow.anchor.dx); + expect(firstAdded.infoWindow.anchor.y, object3.infoWindow.anchor.dy); + expect(firstAdded.position.latitude, object3.position.latitude); + expect(firstAdded.position.longitude, object3.position.longitude); + expect(firstAdded.rotation, object3.rotation); + expect(firstAdded.visible, object3.visible); + expect(firstAdded.zIndex, object3.zIndex); + expect(firstAdded.markerId, object3.markerId.value); + expect(firstAdded.clusterManagerId, object3.clusterManagerId?.value); + expect( + firstAdded.collisionBehavior, + platformMarkerCollisionBehaviorFromMarkerCollisionBehavior( + object3.collisionBehavior, + ), + ); + } + }); + test('updatePolygons passes expected arguments', () async { const int mapId = 1; final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = setUpMockMap( @@ -1517,7 +1640,7 @@ void main() { as PlatformMapViewCreationParams?; if (creationParams != null) { final String? passedMapId = - creationParams.mapConfiguration.cloudMapId; + creationParams.mapConfiguration.mapId; if (passedMapId != null) { passedCloudMapIdCompleter.complete(passedMapId); } @@ -1537,6 +1660,8 @@ void main() { initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), textDirection: TextDirection.ltr, ), + // Deprecated cloudMapId is used to test that creation params have + // the correct mapId. mapConfiguration: const MapConfiguration(cloudMapId: cloudMapId), ), ); @@ -1547,4 +1672,97 @@ void main() { reason: 'Should pass cloudMapId on PlatformView creation message', ); }); + + testWidgets('mapId is passed', (WidgetTester tester) async { + const String mapId = '000000000000000'; // Dummy map ID. + final Completer passedMapIdCompleter = Completer(); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform_views, ( + MethodCall methodCall, + ) async { + if (methodCall.method == 'create') { + final Map args = Map.from( + methodCall.arguments as Map, + ); + if (args.containsKey('params')) { + final Uint8List paramsUint8List = args['params'] as Uint8List; + final ByteData byteData = ByteData.sublistView(paramsUint8List); + final PlatformMapViewCreationParams? creationParams = + MapsApi.pigeonChannelCodec.decodeMessage(byteData) + as PlatformMapViewCreationParams?; + if (creationParams != null) { + final String? passedMapId = + creationParams.mapConfiguration.mapId; + if (passedMapId != null) { + passedMapIdCompleter.complete(passedMapId); + } + } + } + } + return 0; + }); + + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + + await tester.pumpWidget( + maps.buildViewWithConfiguration( + 1, + (int id) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + ), + mapConfiguration: const MapConfiguration(mapId: mapId), + ), + ); + + expect( + await passedMapIdCompleter.future, + mapId, + reason: 'Should pass mapId on PlatformView creation message', + ); + }); + + test('Correct marker type is passed to platform view', () async { + final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); + final Widget widget = maps.buildViewWithConfiguration( + 1, + (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + ), + mapConfiguration: const MapConfiguration( + markerType: MarkerType.advancedMarker, + ), + ); + + expect(widget, isA()); + final dynamic creationParams = (widget as AndroidView).creationParams; + expect(creationParams, isA()); + expect( + (creationParams as PlatformMapViewCreationParams) + .mapConfiguration + .markerType, + PlatformMarkerType.advancedMarker, + ); + + final Widget widget2 = maps.buildViewWithConfiguration( + 1, + (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + ), + mapConfiguration: const MapConfiguration(markerType: MarkerType.marker), + ); + expect(widget2, isA()); + expect( + ((widget2 as AndroidView).creationParams as PlatformMapViewCreationParams) + .mapConfiguration + .markerType, + PlatformMarkerType.marker, + ); + }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart index cfa6b0c5514..e0f05001711 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart @@ -331,6 +331,15 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { ) as _i4.Future); + @override + _i4.Future isAdvancedMarkersAvailable() => + (super.noSuchMethod( + Invocation.method(#isAdvancedMarkersAvailable, []), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) + as _i4.Future); + @override _i4.Future clearTileCache(String? tileOverlayId) => (super.noSuchMethod(