Skip to content

Commit 3cfb6f4

Browse files
committed
feat: add advanced marker support implementation for Android
1 parent 7af370d commit 3cfb6f4

34 files changed

+2776
-288
lines changed

packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.19.0
2+
3+
* Adds support for advanced markers.
4+
15
## 2.18.6
26

37
* Bumps com.android.tools.build:gradle from 8.12.1 to 8.13.1.

packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ android {
3838
dependencies {
3939
implementation("androidx.annotation:annotation:1.9.1")
4040
implementation("com.google.android.gms:play-services-maps:19.2.0")
41-
implementation("com.google.maps.android:android-maps-utils:3.6.0")
41+
implementation("com.google.maps.android:android-maps-utils:3.7.0")
4242
androidTestImplementation("androidx.test:runner:1.7.0")
4343
androidTestImplementation("androidx.test:rules:1.7.0")
4444
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ClusterManagersController.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
import android.content.Context;
88
import androidx.annotation.NonNull;
99
import androidx.annotation.Nullable;
10+
import androidx.annotation.VisibleForTesting;
1011
import com.google.android.gms.maps.GoogleMap;
12+
import com.google.android.gms.maps.model.AdvancedMarkerOptions;
1113
import com.google.android.gms.maps.model.Marker;
1214
import com.google.android.gms.maps.model.MarkerOptions;
1315
import com.google.maps.android.clustering.Cluster;
1416
import com.google.maps.android.clustering.ClusterItem;
1517
import com.google.maps.android.clustering.ClusterManager;
18+
import com.google.maps.android.clustering.view.ClusterRenderer;
19+
import com.google.maps.android.clustering.view.DefaultAdvancedMarkersClusterRenderer;
1620
import com.google.maps.android.clustering.view.DefaultClusterRenderer;
1721
import com.google.maps.android.collections.MarkerManager;
1822
import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi;
23+
import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType;
1924
import java.util.HashMap;
2025
import java.util.List;
2126
import java.util.Map;
@@ -29,10 +34,14 @@ class ClusterManagersController
2934
implements GoogleMap.OnCameraIdleListener,
3035
ClusterManager.OnClusterClickListener<MarkerBuilder> {
3136
@NonNull private final Context context;
32-
@NonNull private final HashMap<String, ClusterManager<MarkerBuilder>> clusterManagerIdToManager;
37+
38+
@VisibleForTesting @NonNull
39+
protected final HashMap<String, ClusterManager<MarkerBuilder>> clusterManagerIdToManager;
40+
3341
@NonNull private final MapsCallbackApi flutterApi;
3442
@Nullable private MarkerManager markerManager;
3543
@Nullable private GoogleMap googleMap;
44+
@NonNull private PlatformMarkerType markerType;
3645

3746
@Nullable
3847
private ClusterManager.OnClusterItemClickListener<MarkerBuilder> clusterItemClickListener;
@@ -41,10 +50,14 @@ class ClusterManagersController
4150
private ClusterManagersController.OnClusterItemRendered<MarkerBuilder>
4251
clusterItemRenderedListener;
4352

44-
ClusterManagersController(@NonNull MapsCallbackApi flutterApi, Context context) {
53+
ClusterManagersController(
54+
@NonNull MapsCallbackApi flutterApi,
55+
@NonNull Context context,
56+
@NonNull PlatformMarkerType markerType) {
4557
this.clusterManagerIdToManager = new HashMap<>();
4658
this.context = context;
4759
this.flutterApi = flutterApi;
60+
this.markerType = markerType;
4861
}
4962

5063
void init(GoogleMap googleMap, MarkerManager markerManager) {
@@ -89,11 +102,21 @@ void addClusterManagers(@NonNull List<Messages.PlatformClusterManager> clusterMa
89102
void addClusterManager(String clusterManagerId) {
90103
ClusterManager<MarkerBuilder> clusterManager =
91104
new ClusterManager<MarkerBuilder>(context, googleMap, markerManager);
92-
ClusterRenderer<MarkerBuilder> clusterRenderer =
93-
new ClusterRenderer<MarkerBuilder>(context, googleMap, clusterManager, this);
105+
initializeRenderer(clusterManager);
106+
clusterManagerIdToManager.put(clusterManagerId, clusterManager);
107+
}
108+
109+
/**
110+
* Initializes cluster renderer based on marker type. AdvancedMarkerCluster renderer is used for
111+
* advanced markers and MarkerClusterRenderer is used for default markers.
112+
*/
113+
private void initializeRenderer(ClusterManager<MarkerBuilder> clusterManager) {
114+
final ClusterRenderer<MarkerBuilder> clusterRenderer =
115+
markerType == PlatformMarkerType.ADVANCED_MARKER
116+
? new AdvancedMarkerClusterRenderer<>(context, googleMap, clusterManager, this)
117+
: new MarkerClusterRenderer<>(context, googleMap, clusterManager, this);
94118
clusterManager.setRenderer(clusterRenderer);
95119
initListenersForClusterManager(clusterManager, this, clusterItemClickListener);
96-
clusterManagerIdToManager.put(clusterManagerId, clusterManager);
97120
}
98121

99122
/** Removes ClusterManagers by given cluster manager IDs from the controller. */
@@ -195,13 +218,14 @@ public boolean onClusterClick(Cluster<MarkerBuilder> cluster) {
195218
}
196219

197220
/**
198-
* ClusterRenderer builds marker options for new markers to be rendered to the map. After cluster
199-
* item (marker) is rendered, it is sent to the listeners for control.
221+
* MarkerClusterRenderer builds marker options for new markers to be rendered to the map. After
222+
* cluster item (marker) is rendered, it is sent to the listeners for control.
200223
*/
201-
private static class ClusterRenderer<T extends MarkerBuilder> extends DefaultClusterRenderer<T> {
224+
@VisibleForTesting
225+
static class MarkerClusterRenderer<T extends MarkerBuilder> extends DefaultClusterRenderer<T> {
202226
private final ClusterManagersController clusterManagersController;
203227

204-
public ClusterRenderer(
228+
public MarkerClusterRenderer(
205229
Context context,
206230
GoogleMap map,
207231
ClusterManager<T> clusterManager,
@@ -225,6 +249,35 @@ protected void onClusterItemRendered(@NonNull T item, @NonNull Marker marker) {
225249
}
226250
}
227251

252+
/** AdvancedMarkerClusterRenderer is a ClusterRenderer that supports AdvancedMarkers. */
253+
@VisibleForTesting
254+
static class AdvancedMarkerClusterRenderer<T extends MarkerBuilder>
255+
extends DefaultAdvancedMarkersClusterRenderer<T> {
256+
257+
private final ClusterManagersController clusterManagersController;
258+
259+
public AdvancedMarkerClusterRenderer(
260+
Context context,
261+
GoogleMap map,
262+
ClusterManager<T> clusterManager,
263+
ClusterManagersController clusterManagersController) {
264+
super(context, map, clusterManager);
265+
this.clusterManagersController = clusterManagersController;
266+
}
267+
268+
@Override
269+
protected void onBeforeClusterItemRendered(
270+
@NonNull T item, @NonNull AdvancedMarkerOptions markerOptions) {
271+
item.update(markerOptions);
272+
}
273+
274+
@Override
275+
protected void onClusterItemRendered(@NonNull T item, @NonNull Marker marker) {
276+
super.onClusterItemRendered(item, marker);
277+
clusterManagersController.onClusterItemRendered(item, marker);
278+
}
279+
}
280+
228281
/** Interface for handling situations where clusterManager adds new visible marker to the map. */
229282
public interface OnClusterItemRendered<T extends ClusterItem> {
230283
void onClusterItemRendered(@NonNull T item, @NonNull Marker marker);

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.android.gms.maps.CameraUpdate;
2121
import com.google.android.gms.maps.CameraUpdateFactory;
2222
import com.google.android.gms.maps.MapsInitializer;
23+
import com.google.android.gms.maps.model.AdvancedMarkerOptions;
2324
import com.google.android.gms.maps.model.BitmapDescriptor;
2425
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
2526
import com.google.android.gms.maps.model.ButtCap;
@@ -34,6 +35,7 @@
3435
import com.google.android.gms.maps.model.LatLng;
3536
import com.google.android.gms.maps.model.LatLngBounds;
3637
import com.google.android.gms.maps.model.PatternItem;
38+
import com.google.android.gms.maps.model.PinConfig;
3739
import com.google.android.gms.maps.model.RoundCap;
3840
import com.google.android.gms.maps.model.SquareCap;
3941
import com.google.android.gms.maps.model.Tile;
@@ -117,6 +119,10 @@ private static BitmapDescriptor toBitmapDescriptor(
117119
Messages.PlatformBitmapBytesMap typedBitmap = (Messages.PlatformBitmapBytesMap) bitmap;
118120
return getBitmapFromBytes(typedBitmap, density, wrapper);
119121
}
122+
if (bitmap instanceof Messages.PlatformBitmapPinConfig) {
123+
Messages.PlatformBitmapPinConfig pinConfigBitmap = (Messages.PlatformBitmapPinConfig) bitmap;
124+
return getBitmapFromPinConfigBuilder(pinConfigBitmap, assetManager, density, wrapper);
125+
}
120126
throw new IllegalArgumentException("PlatformBitmap did not contain a supported subtype.");
121127
}
122128

@@ -187,6 +193,75 @@ public static BitmapDescriptor getBitmapFromBytes(
187193
}
188194
}
189195

196+
public static BitmapDescriptor getBitmapFromPinConfigBuilder(
197+
Messages.PlatformBitmapPinConfig pinConfigBitmap,
198+
AssetManager assetManager,
199+
float density,
200+
BitmapDescriptorFactoryWrapper bitmapDescriptorFactory) {
201+
try {
202+
final PinConfig pinConfig =
203+
getPinConfigFromPlatformPinConfig(
204+
pinConfigBitmap, assetManager, density, bitmapDescriptorFactory);
205+
return bitmapDescriptorFactory.fromPinConfig(pinConfig);
206+
} catch (Exception e) {
207+
throw new IllegalArgumentException("Unable to interpret pin config as a valid image.", e);
208+
}
209+
}
210+
211+
@VisibleForTesting
212+
public static PinConfig getPinConfigFromPlatformPinConfig(
213+
Messages.PlatformBitmapPinConfig pinConfigBitmap,
214+
AssetManager assetManager,
215+
float density,
216+
BitmapDescriptorFactoryWrapper bitmapDescriptorFactory) {
217+
final Integer backgroundColor =
218+
pinConfigBitmap.getBackgroundColor() != null
219+
? toInt(pinConfigBitmap.getBackgroundColor())
220+
: null;
221+
final Integer borderColor =
222+
pinConfigBitmap.getBorderColor() != null ? toInt(pinConfigBitmap.getBorderColor()) : null;
223+
final String glyphText =
224+
pinConfigBitmap.getGlyphText() != null ? pinConfigBitmap.getGlyphText() : null;
225+
final Integer glyphTextColor =
226+
pinConfigBitmap.getGlyphTextColor() != null
227+
? toInt(pinConfigBitmap.getGlyphTextColor())
228+
: null;
229+
final Integer glyphColor =
230+
pinConfigBitmap.getGlyphColor() != null ? toInt(pinConfigBitmap.getGlyphColor()) : null;
231+
final BitmapDescriptor glyphBitmapDescriptor =
232+
pinConfigBitmap.getGlyphBitmap() != null
233+
? toBitmapDescriptor(
234+
pinConfigBitmap.getGlyphBitmap(), assetManager, density, bitmapDescriptorFactory)
235+
: null;
236+
237+
final PinConfig.Builder pinConfigBuilder = PinConfig.builder();
238+
if (backgroundColor != null) {
239+
pinConfigBuilder.setBackgroundColor(backgroundColor);
240+
}
241+
242+
if (borderColor != null) {
243+
pinConfigBuilder.setBorderColor(borderColor);
244+
}
245+
246+
PinConfig.Glyph glyph = null;
247+
if (glyphText != null) {
248+
glyph =
249+
glyphTextColor != null
250+
? new PinConfig.Glyph(glyphText, glyphTextColor)
251+
: new PinConfig.Glyph(glyphText);
252+
} else if (glyphBitmapDescriptor != null) {
253+
glyph = new PinConfig.Glyph(glyphBitmapDescriptor);
254+
} else if (glyphColor != null) {
255+
glyph = new PinConfig.Glyph(glyphColor);
256+
}
257+
258+
if (glyph != null) {
259+
pinConfigBuilder.setGlyph(glyph);
260+
}
261+
262+
return pinConfigBuilder.build();
263+
}
264+
190265
/**
191266
* Creates a BitmapDescriptor object from asset, using given details and density.
192267
*
@@ -610,6 +685,7 @@ static void interpretMarkerOptions(
610685
sink.setRotation(marker.getRotation().floatValue());
611686
sink.setVisible(marker.getVisible());
612687
sink.setZIndex(marker.getZIndex().floatValue());
688+
sink.setCollisionBehavior(collisionBehaviorFromPigeon(marker.getCollisionBehavior()));
613689
}
614690

615691
private static void interpretInfoWindowOptions(
@@ -648,6 +724,20 @@ static int jointTypeFromPigeon(Messages.PlatformJointType jointType) {
648724
return JointType.DEFAULT;
649725
}
650726

727+
static int collisionBehaviorFromPigeon(
728+
Messages.PlatformMarkerCollisionBehavior collisionBehavior) {
729+
switch (collisionBehavior) {
730+
case REQUIRED_DISPLAY:
731+
return AdvancedMarkerOptions.CollisionBehavior.REQUIRED;
732+
case OPTIONAL_AND_HIDES_LOWER_PRIORITY:
733+
return AdvancedMarkerOptions.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY;
734+
case REQUIRED_AND_HIDES_OPTIONAL:
735+
return AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL;
736+
default:
737+
return AdvancedMarkerOptions.CollisionBehavior.REQUIRED;
738+
}
739+
}
740+
651741
static String interpretPolylineOptions(
652742
Messages.PlatformPolyline polyline,
653743
PolylineOptionsSink sink,
@@ -1029,6 +1119,11 @@ public BitmapDescriptor fromAsset(String assetKey) {
10291119
public BitmapDescriptor fromBitmap(Bitmap bitmap) {
10301120
return BitmapDescriptorFactory.fromBitmap(bitmap);
10311121
}
1122+
1123+
@VisibleForTesting
1124+
public BitmapDescriptor fromPinConfig(PinConfig pinConfig) {
1125+
return BitmapDescriptorFactory.fromPinConfig(pinConfig);
1126+
}
10321127
}
10331128

10341129
@VisibleForTesting

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.google.android.gms.maps.model.CameraPosition;
1313
import com.google.android.gms.maps.model.LatLngBounds;
1414
import io.flutter.plugin.common.BinaryMessenger;
15+
import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType;
1516
import java.util.List;
1617

1718
class GoogleMapBuilder implements GoogleMapOptionsSink {
@@ -37,9 +38,11 @@ GoogleMapController build(
3738
int id,
3839
Context context,
3940
BinaryMessenger binaryMessenger,
40-
LifecycleProvider lifecycleProvider) {
41+
LifecycleProvider lifecycleProvider,
42+
PlatformMarkerType markerType) {
4143
final GoogleMapController controller =
42-
new GoogleMapController(id, context, binaryMessenger, lifecycleProvider, options);
44+
new GoogleMapController(
45+
id, context, binaryMessenger, lifecycleProvider, options, markerType);
4346
controller.init();
4447
controller.setMyLocationEnabled(myLocationEnabled);
4548
controller.setMyLocationButtonEnabled(myLocationButtonEnabled);

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.google.android.gms.maps.model.GroundOverlay;
3636
import com.google.android.gms.maps.model.LatLng;
3737
import com.google.android.gms.maps.model.LatLngBounds;
38+
import com.google.android.gms.maps.model.MapCapabilities;
3839
import com.google.android.gms.maps.model.MapStyleOptions;
3940
import com.google.android.gms.maps.model.Marker;
4041
import com.google.android.gms.maps.model.Polygon;
@@ -50,6 +51,7 @@
5051
import io.flutter.plugins.googlemaps.Messages.MapsApi;
5152
import io.flutter.plugins.googlemaps.Messages.MapsCallbackApi;
5253
import io.flutter.plugins.googlemaps.Messages.MapsInspectorApi;
54+
import io.flutter.plugins.googlemaps.Messages.PlatformMarkerType;
5355
import java.io.ByteArrayOutputStream;
5456
import java.util.ArrayList;
5557
import java.util.List;
@@ -116,7 +118,8 @@ class GoogleMapController
116118
Context context,
117119
BinaryMessenger binaryMessenger,
118120
LifecycleProvider lifecycleProvider,
119-
GoogleMapOptions options) {
121+
GoogleMapOptions options,
122+
PlatformMarkerType markerType) {
120123
this.id = id;
121124
this.context = context;
122125
this.options = options;
@@ -128,14 +131,15 @@ class GoogleMapController
128131
MapsInspectorApi.setUp(binaryMessenger, Integer.toString(id), this);
129132
AssetManager assetManager = context.getAssets();
130133
this.lifecycleProvider = lifecycleProvider;
131-
this.clusterManagersController = new ClusterManagersController(flutterApi, context);
134+
this.clusterManagersController = new ClusterManagersController(flutterApi, context, markerType);
132135
this.markersController =
133136
new MarkersController(
134137
flutterApi,
135138
clusterManagersController,
136139
assetManager,
137140
density,
138-
new Convert.BitmapDescriptorFactoryWrapper());
141+
new Convert.BitmapDescriptorFactoryWrapper(),
142+
markerType);
139143
this.polygonsController = new PolygonsController(flutterApi, density);
140144
this.polylinesController = new PolylinesController(flutterApi, assetManager, density);
141145
this.circlesController = new CirclesController(flutterApi, density);
@@ -1026,6 +1030,18 @@ public Boolean isInfoWindowShown(@NonNull String markerId) {
10261030
return lastSetStyleSucceeded;
10271031
}
10281032

1033+
@Override
1034+
public @NonNull Boolean isAdvancedMarkersAvailable() {
1035+
if (googleMap == null) {
1036+
throw new FlutterError(
1037+
"GoogleMap uninitialized",
1038+
"getMapCapabilities() called prior to map initialization",
1039+
null);
1040+
}
1041+
final MapCapabilities mapCapabilities = googleMap.getMapCapabilities();
1042+
return mapCapabilities.isAdvancedMarkersAvailable();
1043+
}
1044+
10291045
@Override
10301046
public void clearTileCache(@NonNull String tileOverlayId) {
10311047
tileOverlaysController.clearTileCache(tileOverlayId);

packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar
4848
builder.setInitialTileOverlays(params.getInitialTileOverlays());
4949
builder.setInitialGroundOverlays(params.getInitialGroundOverlays());
5050

51-
final String cloudMapId = mapConfig.getCloudMapId();
52-
if (cloudMapId != null) {
53-
builder.setMapId(cloudMapId);
51+
final String mapId = mapConfig.getMapId();
52+
if (mapId != null && !mapId.isEmpty()) {
53+
builder.setMapId(mapId);
5454
}
5555

56-
return builder.build(id, context, binaryMessenger, lifecycleProvider);
56+
return builder.build(
57+
id, context, binaryMessenger, lifecycleProvider, mapConfig.getMarkerType());
5758
}
5859
}

0 commit comments

Comments
 (0)