From 525c59fff4cb9a2af796a266ab496b3f750e1d96 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Mon, 23 Feb 2026 12:48:05 +0200 Subject: [PATCH] [google_maps_flutter] Add Advanced Markers support --- .../google_maps_flutter/README.md | 22 + .../integration_test/src/maps_controller.dart | 4 +- .../example/integration_test/src/shared.dart | 2 +- .../example/lib/advanced_marker_icons.dart | 156 ++++++ .../lib/advanced_markers_clustering.dart | 327 ++++++++++++ .../example/lib/clustering.dart | 34 +- .../example/lib/collision_behavior.dart | 154 ++++++ .../google_maps_flutter/example/lib/main.dart | 12 + .../example/lib/map_map_id.dart | 2 +- .../example/lib/marker_icons.dart | 8 +- .../example/lib/place_advanced_marker.dart | 472 ++++++++++++++++++ .../example/lib/place_marker.dart | 46 +- .../lib/readme_sample_advanced_markers.dart | 46 ++ .../google_maps_flutter/example/pubspec.yaml | 2 +- .../example/web/index.html | 2 +- .../lib/src/google_map.dart | 47 +- .../google_maps_flutter/pubspec.yaml | 4 +- .../test/google_map_test.dart | 173 +++++++ 18 files changed, 1460 insertions(+), 53 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_marker_icons.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_markers_clustering.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/collision_behavior.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/place_advanced_marker.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/readme_sample_advanced_markers.dart diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 22ac5fb2f2db..4dcc3400876b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -127,6 +127,28 @@ the `GoogleMap`'s `onMapCreated` callback. The `GoogleMap` widget should be used within a widget with a bounded size. Using it in an unbounded widget will cause the application to throw a Flutter exception. +### Advanced Markers + +[Advanced Markers](https://developers.google.com/maps/documentation/javascript/advanced-markers/overview) +are map markers that offer extra customization options. +[Map ID](https://developers.google.com/maps/documentation/get-map-id) is +required in order to use Advanced Markers: + + +```dart +body: GoogleMap( + // Set your Map ID. + mapId: 'my-map-id', + // Enable support for Advanced Markers. + markerType: GoogleMapMarkerType.advancedMarker, + initialCameraPosition: _kGooglePlex, +), +``` + +**WARNING:** On iOS, using a PinConfig may result in the marker not showing. For details and updates, see +[this issue](https://issuetracker.google.com/issues/370536110). If this issue has not been fixed in the version of the +Google Maps SDK you are using, consider using an asset or bitmap for customization on iOS. + ### Sample Usage diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart index 9f60a28874f7..a114cbe04631 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/maps_controller.dart @@ -567,7 +567,7 @@ void runTests() { skip: isAndroid || isWeb || isIOS, ); - testWidgets('testCloudMapId', (WidgetTester tester) async { + testWidgets('testMapId', (WidgetTester tester) async { final mapIdCompleter = Completer(); final Key key = GlobalKey(); @@ -579,7 +579,7 @@ void runTests() { onMapCreated: (GoogleMapController controller) { mapIdCompleter.complete(controller.mapId); }, - cloudMapId: kCloudMapId, + mapId: kMapId, ), ); await tester.pumpAndSettle(); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart index 627e22b34702..3af595236c25 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/src/shared.dart @@ -22,7 +22,7 @@ const CameraPosition kInitialCameraPosition = CameraPosition( ); // Dummy map ID -const String kCloudMapId = '000000000000000'; // Dummy map ID. +const String kMapId = '000000000000000'; // Dummy map ID. /// True if the test is running in an iOS device final bool isIOS = defaultTargetPlatform == TargetPlatform.iOS; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_marker_icons.dart new file mode 100644 index 000000000000..5848bbfc6a91 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_marker_icons.dart @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.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({Key? key, required this.mapId}) + : 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 = {}; + + GoogleMapController? controller; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + AdvancedMarkersCapabilityStatus(controller: controller), + Expanded( + child: GoogleMap( + mapId: widget.mapId, + markerType: GoogleMapMarkerType.advancedMarker, + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + markers: _markers, + onMapCreated: (GoogleMapController 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/example/lib/advanced_markers_clustering.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_markers_clustering.dart new file mode 100644 index 000000000000..68996fd2491e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_markers_clustering.dart @@ -0,0 +1,327 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'clustering.dart'; +import 'page.dart'; +import 'place_advanced_marker.dart'; + +/// Page for demonstrating advanced marker clustering support. +/// Same as [ClusteringPage] but works with [AdvancedMarker]. +class AdvancedMarkersClustering extends GoogleMapExampleAppPage { + /// Default constructor. + const AdvancedMarkersClustering({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); + + /// 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. + GoogleMapController? 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; + + void _onMapCreated(GoogleMapController controllerParam) { + setState(() { + controller = controllerParam; + }); + } + + /// 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), + ), + ); + } + + 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, Marker 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(() {}); + } + + @override + Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; + final Cluster? lastCluster = this.lastCluster; + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + AdvancedMarkersCapabilityStatus(controller: controller), + SizedBox( + height: 300.0, + child: GoogleMap( + mapId: widget.mapId, + markerType: GoogleMapMarkerType.advancedMarker, + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.25), + zoom: 11.0, + ), + markers: Set.of(markers.values), + clusterManagers: Set.of(clusterManagers.values), + ), + ), + Column( + children: [ + 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/example/lib/clustering.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/clustering.dart index d5fe7eb43019..e6630ee80cd0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/clustering.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/clustering.dart @@ -17,23 +17,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); @@ -89,23 +89,29 @@ class ClusteringBodyState extends State { super.dispose(); } + /// 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, + ); + } + void _onMarkerTapped(MarkerId markerId) { final Marker? tappedMarker = markers[markerId]; if (tappedMarker != null) { 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; }); } @@ -162,8 +168,8 @@ class ClusteringBodyState extends State { clusterManagerIndex * _clusterManagerLongitudeOffset; final marker = Marker( - clusterManagerId: clusterManager.clusterManagerId, markerId: markerId, + clusterManagerId: clusterManager.clusterManagerId, position: LatLng( center.latitude + _getRandomOffset(), center.longitude + _getRandomOffset() + clusterManagerLongitudeOffset, diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/collision_behavior.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/collision_behavior.dart new file mode 100644 index 000000000000..225c44e6ea32 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/collision_behavior.dart @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.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; + + GoogleMapController? 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: GoogleMap( + mapId: widget.mapId, + markerType: GoogleMapMarkerType.advancedMarker, + initialCameraPosition: const CameraPosition( + target: center, + zoom: zoomInLevel, + ), + markers: Set.of(markers), + tiltGesturesEnabled: false, + zoomGesturesEnabled: false, + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + onMapCreated: (GoogleMapController 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/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 2cee263eeba3..227672f319a8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/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 'heatmap.dart'; import 'lite_mode.dart'; @@ -21,6 +24,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'; @@ -29,6 +33,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(), @@ -36,7 +44,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(), @@ -47,8 +57,10 @@ final List _allPages = [ const TileOverlayPage(), const GroundOverlayPage(), const ClusteringPage(), + const AdvancedMarkersClustering(mapId: _mapId), const MapIdPage(), const HeatmapPage(), + const AdvancedMarkerCollisionBehaviorPage(mapId: _mapId), ]; /// MapsDemo is the Main Application. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_map_id.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_map_id.dart index 8ac61c1f1c8f..e5836f05e079 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_map_id.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_map_id.dart @@ -82,7 +82,7 @@ class MapIdBodyState extends State { zoom: 7.0, ), key: _key, - cloudMapId: _mapId, + mapId: _mapId, ); final columnChildren = [ diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart index 2e239cc99032..616d1525db32 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart @@ -20,12 +20,12 @@ 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(); @@ -35,7 +35,7 @@ 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/example/lib/place_advanced_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_advanced_marker.dart new file mode 100644 index 000000000000..77f99aa53831 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_advanced_marker.dart @@ -0,0 +1,472 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.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); + + GoogleMapController? controller; + Map markers = {}; + MarkerId? selectedMarker; + int _markerIdCounter = 1; + LatLng? markerPosition; + + void _onMapCreated(GoogleMapController 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: GoogleMap( + mapId: widget.mapId, + markerType: GoogleMapMarkerType.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 GoogleMapController? controller; + + @override + State createState() => + _AdvancedMarkersCapabilityStatusState(); +} + +class _AdvancedMarkersCapabilityStatusState + extends State { + /// Whether map supports advanced markers. Null indicates capability check + /// is in progress. + bool? _isAdvancedMarkersAvailable; + + @override + Widget build(BuildContext context) { + if (widget.controller != null) { + GoogleMapsFlutterPlatform.instance + .isAdvancedMarkersAvailable(mapId: widget.controller!.mapId) + .then((bool result) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _isAdvancedMarkersAvailable = result; + }); + }); + }); + } + + 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/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 4c4b71ca1750..c3f90629dfdd 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -20,21 +20,19 @@ 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); GoogleMapController? controller; @@ -43,14 +41,10 @@ class PlaceMarkerBodyState extends State { int _markerIdCounter = 1; LatLng? markerPosition; - // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { - this.controller = controller; - } - - @override - void dispose() { - super.dispose(); + setState(() { + this.controller = controller; + }); } void _onMarkerTapped(MarkerId markerId) { @@ -59,17 +53,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; @@ -134,8 +125,8 @@ class PlaceMarkerBodyState extends State { ), infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), onTap: () => _onMarkerTapped(markerId), - onDragEnd: (LatLng position) => _onMarkerDragEnd(markerId, position), onDrag: (LatLng position) => _onMarkerDrag(markerId, position), + onDragEnd: (LatLng position) => _onMarkerDragEnd(markerId, position), ); setState(() { @@ -262,6 +253,15 @@ class PlaceMarkerBodyState extends State { return BytesMapBitmap(bytes.buffer.asUint8List()); } + /// 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/example/lib/readme_sample_advanced_markers.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/readme_sample_advanced_markers.dart new file mode 100644 index 000000000000..a58270fcfe99 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/readme_sample_advanced_markers.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Flutter Google Maps Demo', + home: AdvancedMarkersSample(), + ); + } +} + +class AdvancedMarkersSample extends StatelessWidget { + const AdvancedMarkersSample({super.key}); + + static const CameraPosition _kGooglePlex = CameraPosition( + target: LatLng(37.42796133580664, -122.085749655962), + zoom: 14.4746, + ); + + @override + Widget build(BuildContext context) { + return const Scaffold( + // #docregion AdvancedMarkersSample + body: GoogleMap( + // Set your Map ID. + mapId: 'my-map-id', + // Enable support for Advanced Markers. + markerType: GoogleMapMarkerType.advancedMarker, + initialCameraPosition: _kGooglePlex, + ), + // #enddocregion AdvancedMarkersSample + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 8e4ac761e988..340877e237ab 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/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_android: ^2.16.1 + google_maps_flutter_android: ^2.19.1 google_maps_flutter_platform_interface: ^2.14.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter/example/web/index.html index a3d504b043b2..b2e157a0a178 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/web/index.html +++ b/packages/google_maps_flutter/google_maps_flutter/example/web/index.html @@ -36,7 +36,7 @@ - + diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index 627efae290de..fb34dd2de667 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -87,6 +87,16 @@ class AndroidGoogleMapsFlutter { } } +/// Indicates the type of marker that the map should use. +enum GoogleMapMarkerType { + /// Represents the default marker type, [Marker]. This marker type is + /// deprecated on the web. + marker, + + /// Represents the advanced marker type, [AdvancedMarker]. + advancedMarker, +} + /// A widget which displays a map with data obtained from the Google Maps service. class GoogleMap extends StatefulWidget { /// Creates a widget displaying data from Google Maps services. @@ -133,8 +143,15 @@ class GoogleMap extends StatefulWidget { this.onCameraIdle, this.onTap, this.onLongPress, - this.cloudMapId, - }); + this.markerType = GoogleMapMarkerType.marker, + String? mapId, + @Deprecated('cloudMapId is deprecated. Use mapId instead.') + String? cloudMapId, + }) : assert( + mapId == null || cloudMapId == null, + '''A value may be provided for either mapId or cloudMapId, or neither, but not for both.''', + ), + mapId = mapId ?? cloudMapId; /// Callback method for when the map is ready to be used. /// @@ -371,7 +388,21 @@ class GoogleMap extends StatefulWidget { /// /// See https://developers.google.com/maps/documentation/get-map-id /// for more details. - final String? cloudMapId; + final String? mapId; + + /// Indicates whether map uses [AdvancedMarker]s or [Marker]s. + /// + /// [AdvancedMarker] and [Marker]s classes might not be related to each other + /// in the platform implementation. It's important to set the correct + /// [MarkerType] so that the platform implementation can handle the markers: + /// * If [MarkerType.advancedMarker] is used, all markers must be of type + /// [AdvancedMarker]. + /// * If [MarkerType.marker] is used, markers cannot be of type + /// [AdvancedMarker]. + /// + /// While some features work with either type, using the incorrect type + /// may result in unexpected behavior. + final GoogleMapMarkerType markerType; /// Creates a [State] for this [GoogleMap]. @override @@ -700,6 +731,11 @@ class _GoogleMapState extends State { /// Builds a [MapConfiguration] from the given [map]. MapConfiguration _configurationFromMapWidget(GoogleMap map) { + final MarkerType mapConfigurationMarkerType = switch (map.markerType) { + GoogleMapMarkerType.marker => MarkerType.marker, + GoogleMapMarkerType.advancedMarker => MarkerType.advancedMarker, + }; + return MapConfiguration( webCameraControlPosition: map.webCameraControlPosition, webCameraControlEnabled: map.webCameraControlEnabled, @@ -723,7 +759,10 @@ MapConfiguration _configurationFromMapWidget(GoogleMap map) { indoorViewEnabled: map.indoorViewEnabled, trafficEnabled: map.trafficEnabled, buildingsEnabled: map.buildingsEnabled, - mapId: map.cloudMapId, + markerType: mapConfigurationMarkerType, + // A null mapId in the widget means no map ID, which is expressed as '' in + // the configuration to distinguish from no change (null). + mapId: map.mapId ?? '', // A null style in the widget means no style, which is expressed as '' in // the configuration to distinguish from no change (null). style: map.style ?? '', diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 5e62c4d58054..53cf08453c8d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -21,10 +21,10 @@ flutter: dependencies: flutter: sdk: flutter - google_maps_flutter_android: ^2.16.1 + google_maps_flutter_android: ^2.19.1 google_maps_flutter_ios: ^2.15.4 google_maps_flutter_platform_interface: ^2.14.0 - google_maps_flutter_web: ^0.5.14 + google_maps_flutter_web: ^0.6.1 dev_dependencies: flutter_test: diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 4aedfdbb78f8..cf0b7f5033ad 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -659,4 +659,177 @@ void main() { expect(map.tileOverlaySets.length, 1); }); + + testWidgets('Default markerType is "marker"', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.mapConfiguration.markerType, MarkerType.marker); + }); + + testWidgets('Can update markerType', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + // ignore: avoid_redundant_argument_values + markerType: GoogleMapMarkerType.marker, + ), + ), + ); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.mapConfiguration.markerType, MarkerType.marker); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + markerType: GoogleMapMarkerType.advancedMarker, + ), + ), + ); + expect(map.mapConfiguration.markerType, MarkerType.advancedMarker); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + expect(map.mapConfiguration.markerType, MarkerType.marker); + }); + + testWidgets('Can update mapId', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + mapId: 'myMapId', + ), + ), + ); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.mapConfiguration.mapId, 'myMapId'); + expect(map.mapConfiguration.cloudMapId, 'myMapId'); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + mapId: 'myNewMapId', + ), + ), + ); + expect(map.mapConfiguration.mapId, 'myNewMapId'); + expect(map.mapConfiguration.cloudMapId, 'myNewMapId'); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + expect(map.mapConfiguration.mapId, ''); + expect(map.mapConfiguration.cloudMapId, ''); + }); + + testWidgets('Can update cloudMapId', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + cloudMapId: 'myCloudMapId', + ), + ), + ); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.mapConfiguration.cloudMapId, 'myCloudMapId'); + expect(map.mapConfiguration.mapId, 'myCloudMapId'); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + cloudMapId: 'myNewCloudMapId', + ), + ), + ); + expect(map.mapConfiguration.cloudMapId, 'myNewCloudMapId'); + expect(map.mapConfiguration.mapId, 'myNewCloudMapId'); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + expect(map.mapConfiguration.cloudMapId, ''); + expect(map.mapConfiguration.mapId, ''); + }); + + testWidgets('Providing both mapId and cloudMapId throws an exception', ( + WidgetTester tester, + ) async { + expect(() { + GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + mapId: 'mapId', + cloudMapId: 'cloudMapId', + ); + }, throwsAssertionError); + }); + + testWidgets("Providing mapId doesn't thrown an exception", ( + WidgetTester tester, + ) async { + expect(() { + const GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + mapId: 'mapId', + ); + }, returnsNormally); + }); + + testWidgets("Providing cloudMapid doesn't thrown an exception", ( + WidgetTester tester, + ) async { + expect(() { + const GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + cloudMapId: 'cloudMapId', + ); + }, returnsNormally); + }); + + testWidgets("Not setting cloudMapid and mapId doesn't thrown an exception", ( + WidgetTester tester, + ) async { + expect(() { + const GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ); + }, returnsNormally); + }); }