Skip to content

Commit bf3a29c

Browse files
authored
[google_maps_flutter_ios] Add Advanced marker support (#10508)
This PR adds Advanced markers support to the iOS implementation of `google_maps_flutter`. Approved combined PR: #7882 Approved and merged platform interface PR: #9737 Issue: flutter/flutter#155526 Blocker: GMSAdvancedMarker is not rendered when the marker icon is a GMSPinImage instance. Tracking issue: https://issuetracker.google.com/issues/370536110 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 8d5c5cd commit bf3a29c

117 files changed

Lines changed: 10395 additions & 1571 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md

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

37
* Adds UIScene compatibility.

packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const CameraPosition _kInitialCameraPosition = CameraPosition(
2121
target: _kInitialMapCenter,
2222
zoom: _kInitialZoomLevel,
2323
);
24-
const String _kCloudMapId = '000000000000000'; // Dummy map ID.
24+
const String _kMapId = '000000000000000'; // Dummy map ID.
2525

2626
// The tolerance value for floating-point comparisons in the tests.
2727
// This value was selected as the minimum possible value that the test passes.
@@ -1366,16 +1366,107 @@ void main() {
13661366
}
13671367
});
13681368

1369-
testWidgets('testSetStyleMapId', (WidgetTester tester) async {
1369+
testWidgets('advanced markers clustering', (WidgetTester tester) async {
13701370
final Key key = GlobalKey();
1371+
const clusterManagersAmount = 2;
1372+
const markersPerClusterManager = 5;
1373+
final markers = <MarkerId, AdvancedMarker>{};
1374+
final clusterManagers = <ClusterManager>{};
1375+
1376+
for (var i = 0; i < clusterManagersAmount; i++) {
1377+
final clusterManagerId = ClusterManagerId('cluster_manager_$i');
1378+
final clusterManager = ClusterManager(clusterManagerId: clusterManagerId);
1379+
clusterManagers.add(clusterManager);
1380+
}
1381+
1382+
for (final cm in clusterManagers) {
1383+
for (var i = 0; i < markersPerClusterManager; i++) {
1384+
final markerId = MarkerId('${cm.clusterManagerId.value}_marker_$i');
1385+
final marker = AdvancedMarker(
1386+
markerId: markerId,
1387+
clusterManagerId: cm.clusterManagerId,
1388+
position: LatLng(
1389+
_kInitialMapCenter.latitude + i,
1390+
_kInitialMapCenter.longitude,
1391+
),
1392+
);
1393+
markers[markerId] = marker;
1394+
}
1395+
}
1396+
1397+
final controllerCompleter = Completer<ExampleGoogleMapController>();
1398+
1399+
final GoogleMapsInspectorPlatform inspector =
1400+
GoogleMapsInspectorPlatform.instance!;
1401+
1402+
await tester.pumpWidget(
1403+
Directionality(
1404+
textDirection: TextDirection.ltr,
1405+
child: ExampleGoogleMap(
1406+
key: key,
1407+
initialCameraPosition: _kInitialCameraPosition,
1408+
clusterManagers: clusterManagers,
1409+
markerType: MarkerType.advancedMarker,
1410+
markers: Set<Marker>.of(markers.values),
1411+
onMapCreated: (ExampleGoogleMapController googleMapController) {
1412+
controllerCompleter.complete(googleMapController);
1413+
},
1414+
),
1415+
),
1416+
);
1417+
1418+
final ExampleGoogleMapController controller =
1419+
await controllerCompleter.future;
1420+
1421+
for (final cm in clusterManagers) {
1422+
final List<Cluster> clusters = await inspector.getClusters(
1423+
mapId: controller.mapId,
1424+
clusterManagerId: cm.clusterManagerId,
1425+
);
1426+
final int markersAmountForClusterManager = clusters
1427+
.map<int>((Cluster cluster) => cluster.count)
1428+
.reduce((int value, int element) => value + element);
1429+
expect(markersAmountForClusterManager, markersPerClusterManager);
1430+
}
13711431

1432+
// Remove markers from clusterManagers and test that clusterManagers are empty.
1433+
for (final MapEntry<MarkerId, AdvancedMarker> entry in markers.entries) {
1434+
markers[entry.key] = _copyAdvancedMarkerWithClusterManagerId(
1435+
entry.value,
1436+
null,
1437+
);
1438+
}
13721439
await tester.pumpWidget(
13731440
Directionality(
13741441
textDirection: TextDirection.ltr,
13751442
child: ExampleGoogleMap(
13761443
key: key,
13771444
initialCameraPosition: _kInitialCameraPosition,
1378-
mapId: _kCloudMapId,
1445+
clusterManagers: clusterManagers,
1446+
markers: Set<Marker>.of(markers.values),
1447+
),
1448+
),
1449+
);
1450+
1451+
for (final cm in clusterManagers) {
1452+
final List<Cluster> clusters = await inspector.getClusters(
1453+
mapId: controller.mapId,
1454+
clusterManagerId: cm.clusterManagerId,
1455+
);
1456+
expect(clusters.length, 0);
1457+
}
1458+
});
1459+
1460+
testWidgets('testMapId', (WidgetTester tester) async {
1461+
final Key key = GlobalKey();
1462+
1463+
await tester.pumpWidget(
1464+
Directionality(
1465+
textDirection: TextDirection.ltr,
1466+
child: ExampleGoogleMap(
1467+
key: key,
1468+
initialCameraPosition: _kInitialCameraPosition,
1469+
mapId: _kMapId,
13791470
),
13801471
),
13811472
);
@@ -2069,6 +2160,38 @@ void main() {
20692160
// Hanging in CI, https://github.com/flutter/flutter/issues/166139
20702161
skip: true,
20712162
);
2163+
2164+
testWidgets('markerWithPinConfig', (WidgetTester tester) async {
2165+
final markers = <AdvancedMarker>{
2166+
AdvancedMarker(
2167+
markerId: const MarkerId('1'),
2168+
icon: BitmapDescriptor.pinConfig(
2169+
backgroundColor: Colors.green,
2170+
borderColor: Colors.greenAccent,
2171+
glyph: const TextGlyph(text: 'A', textColor: Colors.white),
2172+
),
2173+
),
2174+
};
2175+
2176+
final controllerCompleter = Completer<ExampleGoogleMapController>();
2177+
2178+
await tester.pumpWidget(
2179+
Directionality(
2180+
textDirection: ui.TextDirection.ltr,
2181+
child: ExampleGoogleMap(
2182+
initialCameraPosition: const CameraPosition(
2183+
target: LatLng(10.0, 20.0),
2184+
),
2185+
markers: markers,
2186+
markerType: MarkerType.advancedMarker,
2187+
onMapCreated: (ExampleGoogleMapController controller) =>
2188+
controllerCompleter.complete(controller),
2189+
),
2190+
),
2191+
);
2192+
await tester.pumpAndSettle();
2193+
await controllerCompleter.future;
2194+
});
20722195
}
20732196

20742197
class _DebugTileProvider implements TileProvider {
@@ -2135,6 +2258,32 @@ Marker _copyMarkerWithClusterManagerId(
21352258
);
21362259
}
21372260

2261+
AdvancedMarker _copyAdvancedMarkerWithClusterManagerId(
2262+
AdvancedMarker marker,
2263+
ClusterManagerId? clusterManagerId,
2264+
) {
2265+
return AdvancedMarker(
2266+
markerId: marker.markerId,
2267+
alpha: marker.alpha,
2268+
anchor: marker.anchor,
2269+
consumeTapEvents: marker.consumeTapEvents,
2270+
draggable: marker.draggable,
2271+
flat: marker.flat,
2272+
icon: marker.icon,
2273+
infoWindow: marker.infoWindow,
2274+
position: marker.position,
2275+
rotation: marker.rotation,
2276+
visible: marker.visible,
2277+
zIndex: marker.zIndex.toInt(),
2278+
onTap: marker.onTap,
2279+
onDragStart: marker.onDragStart,
2280+
onDrag: marker.onDrag,
2281+
onDragEnd: marker.onDragEnd,
2282+
clusterManagerId: clusterManagerId,
2283+
collisionBehavior: marker.collisionBehavior,
2284+
);
2285+
}
2286+
21382287
CameraUpdate _getCameraUpdateForType(CameraUpdateType type) {
21392288
return switch (type) {
21402289
CameraUpdateType.newCameraPosition => CameraUpdate.newCameraPosition(

packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/ClusterManagersControllerTests.m

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ - (void)testClustering {
3333
[[FGMMarkersController alloc] initWithMapView:mapView
3434
eventDelegate:eventHandler
3535
clusterManagersController:clusterManagersController
36-
assetProvider:[[TestAssetProvider alloc] init]];
36+
assetProvider:[[TestAssetProvider alloc] init]
37+
markerType:FGMPlatformMarkerTypeMarker];
3738

3839
// Add cluster managers.
3940
NSString *clusterManagerId = @"cm";
@@ -69,7 +70,8 @@ - (void)testClustering {
6970
visible:YES
7071
zIndex:1
7172
markerId:markerId1
72-
clusterManagerId:clusterManagerId];
73+
clusterManagerId:clusterManagerId
74+
collisionBehavior:nil];
7375
FGMPlatformMarker *marker2 = [FGMPlatformMarker makeWithAlpha:1
7476
anchor:zeroPoint
7577
consumeTapEvents:NO
@@ -82,7 +84,8 @@ - (void)testClustering {
8284
visible:YES
8385
zIndex:1
8486
markerId:markerId2
85-
clusterManagerId:clusterManagerId];
87+
clusterManagerId:clusterManagerId
88+
collisionBehavior:nil];
8689

8790
[markersController addMarkers:@[ marker1, marker2 ]];
8891

packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/ExtractIconFromDataTests.m

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,109 @@ - (void)testExtractIconFromDataBytesNoScaling {
277277
XCTAssertEqual(resultImage.size.height, 1.0);
278278
}
279279

280+
/// Tests for PinConfig (GMSPinImageOptions) - requires iOS 16.0+ and Google Maps SDK 9.0+.
281+
/// On earlier versions, FGMIconFromBitmap returns nil for PinConfig, which is expected behavior.
282+
- (void)testExtractIconFromPinConfigWithGlyphColor {
283+
TestAssetProvider *assetProvider = [[TestAssetProvider alloc] init];
284+
285+
FGMPlatformColor *backgroundColor = [FGMPlatformColor makeWithRed:0.0
286+
green:1.0
287+
blue:1.0
288+
alpha:1.0];
289+
FGMPlatformColor *borderColor = [FGMPlatformColor makeWithRed:1.0 green:0.0 blue:1.0 alpha:1.0];
290+
FGMPlatformColor *glyphColor = [FGMPlatformColor makeWithRed:0.1 green:0.2 blue:0.3 alpha:1.0];
291+
292+
FGMPlatformBitmapPinConfig *pinConfig =
293+
[FGMPlatformBitmapPinConfig makeWithBackgroundColor:backgroundColor
294+
borderColor:borderColor
295+
glyphColor:glyphColor
296+
glyphTextColor:nil
297+
glyphText:nil
298+
glyphBitmap:nil];
299+
300+
CGFloat screenScale = 3.0;
301+
302+
UIImage *resultImage =
303+
FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:pinConfig], assetProvider, screenScale);
304+
305+
// PinConfig may return nil on old Google Maps SDK versions (<=8.4.0).
306+
// Also, due to a Google Maps SDK issue (https://issuetracker.google.com/issues/370536110),
307+
// GMSPinImage can return a zero-sized image.
308+
XCTAssertTrue(resultImage == nil || resultImage.size.width >= 0);
309+
XCTAssertTrue(resultImage == nil || resultImage.size.height >= 0);
310+
}
311+
312+
- (void)testExtractIconFromPinConfigWithGlyphText {
313+
TestAssetProvider *assetProvider = [[TestAssetProvider alloc] init];
314+
315+
FGMPlatformColor *glyphTextColor = [FGMPlatformColor makeWithRed:1.0
316+
green:1.0
317+
blue:1.0
318+
alpha:1.0];
319+
320+
FGMPlatformBitmapPinConfig *pinConfig =
321+
[FGMPlatformBitmapPinConfig makeWithBackgroundColor:nil
322+
borderColor:nil
323+
glyphColor:nil
324+
glyphTextColor:glyphTextColor
325+
glyphText:@"Hi"
326+
glyphBitmap:nil];
327+
328+
CGFloat screenScale = 3.0;
329+
330+
UIImage *resultImage =
331+
FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:pinConfig], assetProvider, screenScale);
332+
333+
// PinConfig returns nil on iOS versions without GMSPinImageOptions support (< iOS 16.0).
334+
// On simulators, GMSPinImage may also return a zero-dimension image. Both cases are acceptable
335+
// in test environment - the important thing is that the call doesn't crash.
336+
// When the image is valid, it should have positive dimensions.
337+
XCTAssertTrue(resultImage == nil || resultImage.size.width >= 0);
338+
XCTAssertTrue(resultImage == nil || resultImage.size.height >= 0);
339+
}
340+
341+
- (void)testExtractIconFromPinConfigWithGlyphBitmap {
342+
UIImage *testImage = [self createOnePixelImage];
343+
NSString *assetName = @"fakeImageNameKey";
344+
TestAssetProvider *assetProvider = [[TestAssetProvider alloc] initWithImage:testImage
345+
forAssetName:assetName
346+
package:nil];
347+
348+
FGMPlatformBitmapAssetMap *assetBitmap =
349+
[FGMPlatformBitmapAssetMap makeWithAssetName:assetName
350+
bitmapScaling:FGMPlatformMapBitmapScalingAuto
351+
imagePixelRatio:1
352+
width:nil
353+
height:nil];
354+
FGMPlatformBitmap *glyphBitmap = [FGMPlatformBitmap makeWithBitmap:assetBitmap];
355+
356+
FGMPlatformColor *backgroundColor = [FGMPlatformColor makeWithRed:1.0
357+
green:1.0
358+
blue:1.0
359+
alpha:1.0];
360+
FGMPlatformColor *borderColor = [FGMPlatformColor makeWithRed:0.0 green:0.0 blue:0.0 alpha:1.0];
361+
362+
FGMPlatformBitmapPinConfig *pinConfig =
363+
[FGMPlatformBitmapPinConfig makeWithBackgroundColor:backgroundColor
364+
borderColor:borderColor
365+
glyphColor:nil
366+
glyphTextColor:nil
367+
glyphText:nil
368+
glyphBitmap:glyphBitmap];
369+
370+
CGFloat screenScale = 3.0;
371+
372+
UIImage *resultImage =
373+
FGMIconFromBitmap([FGMPlatformBitmap makeWithBitmap:pinConfig], assetProvider, screenScale);
374+
375+
// PinConfig returns nil on iOS versions without GMSPinImageOptions support (< iOS 16.0).
376+
// On simulators, GMSPinImage may also return a zero-dimension image. Both cases are acceptable
377+
// in test environment - the important thing is that the call doesn't crash.
378+
// When the image is valid, it should have positive dimensions.
379+
XCTAssertTrue(resultImage == nil || resultImage.size.width >= 0);
380+
XCTAssertTrue(resultImage == nil || resultImage.size.height >= 0);
381+
}
382+
280383
- (void)testIsScalableWithScaleFactorFromSize100x100to10x100 {
281384
CGSize originalSize = CGSizeMake(100.0, 100.0);
282385
CGSize targetSize = CGSizeMake(10.0, 100.0);

packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/GoogleMapsTests.m

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,25 @@ - (FGMPlatformMapViewCreationParams *)emptyCreationParameters {
232232
longitude:0.0]
233233
tilt:0.0
234234
zoom:0.0]
235-
mapConfiguration:[[FGMPlatformMapConfiguration alloc] init]
235+
mapConfiguration:[FGMPlatformMapConfiguration
236+
makeWithCompassEnabled:nil
237+
cameraTargetBounds:nil
238+
mapType:nil
239+
minMaxZoomPreference:nil
240+
rotateGesturesEnabled:nil
241+
scrollGesturesEnabled:nil
242+
tiltGesturesEnabled:nil
243+
trackCameraPosition:nil
244+
zoomGesturesEnabled:nil
245+
myLocationEnabled:nil
246+
myLocationButtonEnabled:nil
247+
padding:nil
248+
indoorViewEnabled:nil
249+
trafficEnabled:nil
250+
buildingsEnabled:nil
251+
markerType:FGMPlatformMarkerTypeMarker
252+
mapId:nil
253+
style:nil]
236254
initialCircles:@[]
237255
initialMarkers:@[]
238256
initialPolygons:@[]

0 commit comments

Comments
 (0)