diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 53b2cceeb2d..1066f3c5d9c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -116,6 +116,24 @@ 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, +), +``` + ### 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 d44aa4d341e..3b1a226120b 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 @@ -562,7 +562,7 @@ void runTests() { onMapCreated: (GoogleMapController controller) { mapIdCompleter.complete(controller.mapId); }, - cloudMapId: kCloudMapId, + cloudMapId: 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 126733f678e..9db6aa293cb 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 @@ -20,7 +20,7 @@ const CameraPosition kInitialCameraPosition = CameraPosition(target: kInitialMapCenter, zoom: kInitialZoomLevel); // 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 00000000000..08d502161ab --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_marker_icons.dart @@ -0,0 +1,162 @@ +// 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 00000000000..18efca7b644 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/advanced_markers_clustering.dart @@ -0,0 +1,315 @@ +// 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 5c06a61b2a1..ea4957c7cbb 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,22 +89,27 @@ 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; }); } @@ -160,8 +165,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, 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 00000000000..6597d0b5b9a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/collision_behavior.dart @@ -0,0 +1,159 @@ +// 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 db7f38f9f8a..c142953a8e1 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 6b90d777da0..bddb7cf3bb4 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 @@ -80,7 +80,7 @@ class MapIdBodyState extends State { zoom: 7.0, ), key: _key, - cloudMapId: _mapId); + mapId: _mapId); final List columnChildren = [ Padding( 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 d2010b65b13..a9f80769e30 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(); @@ -41,7 +41,7 @@ enum _MarkerSizeOption { 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 00000000000..c351a504905 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_advanced_marker.dart @@ -0,0 +1,487 @@ +// 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 1534f3b64ba..6e91e9acd5b 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,16 +53,12 @@ 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; @@ -130,8 +120,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(() { @@ -272,6 +262,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 00000000000..c67473ae5b6 --- /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 16df1fb3d99..698193561bb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -33,3 +33,8 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter: {path: ../../../google_maps_flutter/google_maps_flutter}, google_maps_flutter_android: {path: ../../../google_maps_flutter/google_maps_flutter_android}, google_maps_flutter_ios: {path: ../../../google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../google_maps_flutter/google_maps_flutter_platform_interface}, google_maps_flutter_web: {path: ../../../google_maps_flutter/google_maps_flutter_web}} 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 1d99e9742c8..49d01b4e669 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 76bcad6e26c..0aaece1c735 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 @@ -83,6 +83,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. @@ -127,8 +137,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. /// @@ -353,7 +370,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 @@ -651,6 +682,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( webGestureHandling: map.webGestureHandling, compassEnabled: map.compassEnabled, @@ -672,7 +708,10 @@ MapConfiguration _configurationFromMapWidget(GoogleMap map) { indoorViewEnabled: map.indoorViewEnabled, trafficEnabled: map.trafficEnabled, buildingsEnabled: map.buildingsEnabled, - cloudMapId: 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 974bbaa2101..f7a3df5a7ac 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -41,3 +41,8 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_android: {path: ../../google_maps_flutter/google_maps_flutter_android}, google_maps_flutter_ios: {path: ../../google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../google_maps_flutter/google_maps_flutter_platform_interface}, google_maps_flutter_web: {path: ../../google_maps_flutter/google_maps_flutter_web}} 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 9276a7dbda3..cb2e9f69201 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 @@ -579,4 +579,201 @@ void main() { expect(map.mapConfiguration.style, ''); }); + + 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, + ); + }, + ); } 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 260a842616c..ef94bf3af24 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:18.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.6.2' androidTestImplementation 'androidx.test:rules:1.6.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 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 cd81eba7940..ac63306979c 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,22 @@ 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 renderer = clusterManager.getRenderer(); + 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 +219,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 +250,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 c8109ebd4f8..65574043423 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 getBitmapFromPinConfig(pinConfigBitmap, assetManager, density, wrapper); + } throw new IllegalArgumentException("PlatformBitmap did not contain a supported subtype."); } @@ -187,6 +193,70 @@ public static BitmapDescriptor getBitmapFromBytes( } } + public static BitmapDescriptor getBitmapFromPinConfig( + Messages.PlatformBitmapPinConfig pinConfigBitmap, + AssetManager assetManager, + float density, + BitmapDescriptorFactoryWrapper bitmapDescriptorFactory) { + try { + final String backgroundColorKey = "backgroundColor"; + final String borderColorKey = "borderColor"; + final String glyphTextKey = "glyphText"; + final String glyphTextColorKey = "glyphTextColor"; + final String glyphColorKey = "glyphColor"; + final String glyphBitmapDescriptorKey = "glyphBitmapDescriptor"; + + 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) + : 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); + } + + final PinConfig pinConfig = pinConfigBuilder.build(); + return bitmapDescriptorFactory.fromPinConfig(pinConfig); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to interpret pin config as a valid image.", e); + } + } + /** * Creates a BitmapDescriptor object from asset, using given details and density. * @@ -602,6 +672,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( @@ -640,6 +711,19 @@ 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; + } + return AdvancedMarkerOptions.CollisionBehavior.REQUIRED; + } + static String interpretPolylineOptions( Messages.PlatformPolyline polyline, PolylineOptionsSink sink, @@ -1021,6 +1105,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 75897778f8d..678475558ac 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 012f0689d7c..80e5969c077 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 c4208f003c6..ab4d4013cb7 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) { + 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 fe99cb48ada..7e0cdd5fc17 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 353ec2dfadb..c86264b7bf8 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 88c970c1f14..e88d3cd6e38 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 24326fe5305..b19cc422eea 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 85e0b7926a5..697e367d7c7 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 @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.googlemaps; @@ -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 a63efc17df1..7fd5e8c8c14 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 acb5fb85a9f..68e4ea819e3 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 @@ -37,6 +37,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; @@ -97,11 +98,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 +328,20 @@ public void GetBitmapFromBytesThrowsErrorIfInvalidImageData() { fail("Expected an IllegalArgumentException to be thrown"); } + @Test + public void GetBitmapFromPinConfig() { + Messages.PlatformBitmapPinConfig bitmap = + new Messages.PlatformBitmapPinConfig.Builder() + .setBackgroundColor(0xFFFFFFL) + .setBorderColor(0xFFFFFFL) + .build(); + + when(bitmapDescriptorFactoryWrapper.fromPinConfig(any())).thenReturn(mockBitmapDescriptor); + BitmapDescriptor result = + Convert.getBitmapFromPinConfig(bitmap, assetManager, 1f, bitmapDescriptorFactoryWrapper); + Assert.assertEquals(mockBitmapDescriptor, result); + } + @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 a5d23705990..c2d05cd1a39 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 @@ -28,6 +28,7 @@ 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; @@ -71,7 +72,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 +218,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 +227,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()); 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 1a98c1141b1..b1cf75394b0 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 @@ -23,6 +23,8 @@ 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; @@ -81,7 +83,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 +93,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(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart index 952c381df53..198798f0f85 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart @@ -21,7 +21,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. @@ -1279,8 +1279,99 @@ void googleMapsTests() { } }); + 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( - 'testCloudMapId', + 'testMapId', (WidgetTester tester) async { final Completer mapIdCompleter = Completer(); final Key key = GlobalKey(); @@ -1294,7 +1385,7 @@ void googleMapsTests() { onMapCreated: (ExampleGoogleMapController controller) { mapIdCompleter.complete(controller.mapId); }, - cloudMapId: _kCloudMapId, + mapId: _kMapId, ), ), ); @@ -1949,6 +2040,33 @@ void googleMapsTests() { // 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), + ), + ), + }; + + await tester.pumpWidget( + Directionality( + textDirection: ui.TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(10.0, 20.0), + ), + markers: markers, + markerType: MarkerType.advancedMarker, + ), + ), + ); + await tester.pumpAndSettle(); + }); } class _DebugTileProvider implements TileProvider { @@ -1996,7 +2114,9 @@ class _DebugTileProvider implements TileProvider { } Marker _copyMarkerWithClusterManagerId( - Marker marker, ClusterManagerId? clusterManagerId) { + Marker marker, + ClusterManagerId? clusterManagerId, +) { return Marker( markerId: marker.markerId, alpha: marker.alpha, @@ -2103,3 +2223,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..07efdec1230 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_marker_icons.dart @@ -0,0 +1,162 @@ +// 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_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..6a483e272f6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/advanced_markers_clustering.dart @@ -0,0 +1,315 @@ +// 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_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 1cd95118eb1..9182d8b4210 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,16 +101,12 @@ 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; }); } @@ -166,8 +162,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, @@ -205,6 +201,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..09ee5370871 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/collision_behavior.dart @@ -0,0 +1,159 @@ +// 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_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 e712208421c..d06479433ba 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 @@ -262,11 +262,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, }); @@ -380,11 +381,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(); @@ -604,7 +608,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 5261d84beac..e7f6fa65cbf 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 984e32a8ea5..6054257ed65 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 @@ -48,7 +48,7 @@ class MapIdBodyState extends State { super.initState(); } - String _getInitializedsRendererType() { + String _getInitializedRendererType() { switch (_initializedRenderer) { case AndroidMapRenderer.latest: return 'latest'; @@ -79,7 +79,7 @@ class MapIdBodyState extends State { zoom: 7.0, ), key: _key, - cloudMapId: _mapId, + mapId: _mapId, ); final List columnChildren = [ @@ -114,7 +114,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 df4f79205e8..2c1a8d2a64d 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,15 +21,15 @@ 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); @@ -42,7 +42,7 @@ enum _MarkerSizeOption { 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..43073aefe73 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_advanced_marker.dart @@ -0,0 +1,484 @@ +// 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_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. 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_android/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart index d475787c92f..dc9c4c000f0 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,16 +61,12 @@ 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; @@ -123,7 +120,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, @@ -273,6 +270,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 46b41e3f9f4..5d7f952b501 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 @@ -33,3 +33,8 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_android: {path: ../../../google_maps_flutter/google_maps_flutter_android}, google_maps_flutter_platform_interface: {path: ../../../google_maps_flutter/google_maps_flutter_platform_interface}} 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 ef713e6fd0a..45bbb7755ee 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 @@ -497,6 +497,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. /// @@ -662,8 +667,9 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { creationId, onPlatformViewCreated, widgetConfiguration: MapWidgetConfiguration( - initialCameraPosition: initialCameraPosition, - textDirection: textDirection), + initialCameraPosition: initialCameraPosition, + textDirection: textDirection, + ), mapObjects: MapObjects( markers: markers, polygons: polygons, @@ -795,6 +801,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, ); } @@ -996,6 +1007,36 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { imagePixelRatio: bytes.imagePixelRatio, width: bytes.width, 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}', 'bitmap'); @@ -1243,6 +1284,14 @@ PlatformEdgeInsets? _platformEdgeInsetsFromEdgeInsets(EdgeInsets? insets) { right: insets.right); } +PlatformMarkerType? _platformMarkerTypeFromMarkerType(MarkerType? markerType) { + return switch (markerType) { + null => null, + MarkerType.marker => PlatformMarkerType.marker, + MarkerType.advancedMarker => PlatformMarkerType.advancedMarker, + }; +} + PlatformMapConfiguration _platformMapConfigurationFromMapConfiguration( MapConfiguration config) { return PlatformMapConfiguration( @@ -1266,7 +1315,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, ); } @@ -1307,7 +1357,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?, ); } @@ -1420,6 +1471,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 d77cb4b83ea..93eb69c60e1 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 @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -43,6 +43,12 @@ enum PlatformRendererType { latest, } +enum PlatformMarkerCollisionBehavior { + requiredDisplay, + optionalAndHidesLowerPriority, + requiredAndHidesOptional, +} + /// Join types for polyline joints. enum PlatformJointType { mitered, @@ -67,6 +73,11 @@ enum PlatformPatternItemType { gap, } +enum PlatformMarkerType { + marker, + advancedMarker, +} + /// Pigeon equivalent of [MapBitmapScaling]. enum PlatformMapBitmapScaling { auto, @@ -518,6 +529,7 @@ class PlatformMarker { this.zIndex = 0.0, required this.markerId, this.clusterManagerId, + this.collisionBehavior = PlatformMarkerCollisionBehavior.requiredDisplay, }); double alpha; @@ -546,6 +558,8 @@ class PlatformMarker { String? clusterManagerId; + PlatformMarkerCollisionBehavior collisionBehavior; + Object encode() { return [ alpha, @@ -561,6 +575,7 @@ class PlatformMarker { zIndex, markerId, clusterManagerId, + collisionBehavior, ]; } @@ -580,6 +595,7 @@ class PlatformMarker { zIndex: result[10]! as double, markerId: result[11]! as String, clusterManagerId: result[12] as String?, + collisionBehavior: result[13]! as PlatformMarkerCollisionBehavior, ); } } @@ -1192,7 +1208,8 @@ class PlatformMapConfiguration { this.trafficEnabled, this.buildingsEnabled, this.liteModeEnabled, - this.cloudMapId, + this.markerType, + this.mapId, this.style, }); @@ -1232,7 +1249,9 @@ class PlatformMapConfiguration { bool? liteModeEnabled; - String? cloudMapId; + PlatformMarkerType? markerType; + + String? mapId; String? style; @@ -1256,7 +1275,8 @@ class PlatformMapConfiguration { trafficEnabled, buildingsEnabled, liteModeEnabled, - cloudMapId, + markerType, + mapId, style, ]; } @@ -1282,8 +1302,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?, ); } } @@ -1608,6 +1629,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 @@ -1621,137 +1689,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); @@ -1769,96 +1846,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); } @@ -2466,6 +2553,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 04989b20e00..10f1c6df96a 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 @@ -184,6 +184,7 @@ class PlatformMarker { this.visible = true, this.zIndex = 0.0, this.clusterManagerId, + this.collisionBehavior = PlatformMarkerCollisionBehavior.requiredDisplay, }); final double alpha; @@ -200,6 +201,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. @@ -453,6 +462,11 @@ class PlatformMapViewCreationParams { final List initialGroundOverlays; } +enum PlatformMarkerType { + marker, + advancedMarker, +} + /// Pigeon equivalent of MapConfiguration. class PlatformMapConfiguration { PlatformMapConfiguration({ @@ -474,7 +488,8 @@ class PlatformMapConfiguration { required this.trafficEnabled, required this.buildingsEnabled, required this.liteModeEnabled, - required this.cloudMapId, + required this.markerType, + required this.mapId, required this.style, }); @@ -496,7 +511,8 @@ class PlatformMapConfiguration { final bool? trafficEnabled; final bool? buildingsEnabled; final bool? liteModeEnabled; - final String? cloudMapId; + final PlatformMarkerType? markerType; + final String? mapId; final String? style; } @@ -621,6 +637,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]. @@ -713,6 +749,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 84779722eb1..cd33828209a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -37,3 +37,8 @@ topics: - google-maps - google-maps-flutter - map + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_platform_interface: {path: ../../google_maps_flutter/google_maps_flutter_platform_interface}} 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 210be3c3e01..a35c8d3b855 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 @@ -239,6 +239,18 @@ 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) = @@ -478,6 +490,104 @@ 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) = @@ -967,11 +1077,14 @@ void main() { () async { final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); maps.useAndroidViewSurface = false; - final Widget widget = maps.buildViewWithConfiguration(1, (int _) {}, - widgetConfiguration: const MapWidgetConfiguration( - initialCameraPosition: - CameraPosition(target: LatLng(0, 0), zoom: 1), - textDirection: TextDirection.ltr)); + final Widget widget = maps.buildViewWithConfiguration( + 1, + (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + ), + ); expect(widget, isA()); }, @@ -1211,9 +1324,9 @@ void main() { final Widget widget = maps.buildViewWithConfiguration(1, (int _) {}, widgetConfiguration: const MapWidgetConfiguration( - initialCameraPosition: - CameraPosition(target: LatLng(0, 0), zoom: 1), - textDirection: TextDirection.ltr)); + initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + )); expect(widget, isA()); }); @@ -1221,11 +1334,14 @@ void main() { testWidgets('Defaults to AndroidView', (WidgetTester tester) 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)); + final Widget widget = maps.buildViewWithConfiguration( + 1, + (int _) {}, + widgetConfiguration: const MapWidgetConfiguration( + initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + ), + ); expect(widget, isA()); }); @@ -1248,8 +1364,7 @@ void main() { MapsApi.pigeonChannelCodec.decodeMessage(byteData) as PlatformMapViewCreationParams?; if (creationParams != null) { - final String? passedMapId = - creationParams.mapConfiguration.cloudMapId; + final String? passedMapId = creationParams.mapConfiguration.mapId; if (passedMapId != null) { passedCloudMapIdCompleter.complete(passedMapId); } @@ -1264,9 +1379,11 @@ void main() { await tester.pumpWidget(maps.buildViewWithConfiguration(1, (int id) {}, widgetConfiguration: const MapWidgetConfiguration( - initialCameraPosition: - CameraPosition(target: LatLng(0, 0), zoom: 1), - textDirection: TextDirection.ltr), + initialCameraPosition: CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + ), + // Here deprecated cloudMapId is used to test that creation params have + // the correct mapId. mapConfiguration: const MapConfiguration(cloudMapId: cloudMapId))); expect( @@ -1275,4 +1392,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 463abd22234..3391dd85df3 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 @@ -25,20 +25,35 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: subtype_of_sealed_class class _FakePlatformPoint_0 extends _i1.SmartFake implements _i2.PlatformPoint { - _FakePlatformPoint_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakePlatformPoint_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakePlatformLatLng_1 extends _i1.SmartFake implements _i2.PlatformLatLng { - _FakePlatformLatLng_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakePlatformLatLng_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakePlatformLatLngBounds_2 extends _i1.SmartFake implements _i2.PlatformLatLngBounds { - _FakePlatformLatLngBounds_2(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakePlatformLatLngBounds_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } /// A class which mocks [MapsApi]. @@ -60,17 +75,22 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { @override _i4.Future waitForMap() => (super.noSuchMethod( - Invocation.method(#waitForMap, []), + Invocation.method( + #waitForMap, + [], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future updateMapConfiguration( - _i2.PlatformMapConfiguration? configuration, - ) => + _i2.PlatformMapConfiguration? configuration) => (super.noSuchMethod( - Invocation.method(#updateMapConfiguration, [configuration]), + Invocation.method( + #updateMapConfiguration, + [configuration], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -82,7 +102,14 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updateCircles, [toAdd, toChange, idsToRemove]), + Invocation.method( + #updateCircles, + [ + toAdd, + toChange, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -94,7 +121,14 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updateHeatmaps, [toAdd, toChange, idsToRemove]), + Invocation.method( + #updateHeatmaps, + [ + toAdd, + toChange, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -105,7 +139,13 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updateClusterManagers, [toAdd, idsToRemove]), + Invocation.method( + #updateClusterManagers, + [ + toAdd, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -117,7 +157,14 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updateMarkers, [toAdd, toChange, idsToRemove]), + Invocation.method( + #updateMarkers, + [ + toAdd, + toChange, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -129,7 +176,14 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updatePolygons, [toAdd, toChange, idsToRemove]), + Invocation.method( + #updatePolygons, + [ + toAdd, + toChange, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -141,7 +195,14 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updatePolylines, [toAdd, toChange, idsToRemove]), + Invocation.method( + #updatePolylines, + [ + toAdd, + toChange, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -153,11 +214,14 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updateTileOverlays, [ - toAdd, - toChange, - idsToRemove, - ]), + Invocation.method( + #updateTileOverlays, + [ + toAdd, + toChange, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -169,77 +233,100 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method(#updateGroundOverlays, [ - toAdd, - toChange, - idsToRemove, - ]), + Invocation.method( + #updateGroundOverlays, + [ + toAdd, + toChange, + idsToRemove, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future<_i2.PlatformPoint> getScreenCoordinate( - _i2.PlatformLatLng? latLng, - ) => + _i2.PlatformLatLng? latLng) => (super.noSuchMethod( - Invocation.method(#getScreenCoordinate, [latLng]), - returnValue: _i4.Future<_i2.PlatformPoint>.value( - _FakePlatformPoint_0( - this, - Invocation.method(#getScreenCoordinate, [latLng]), - ), + Invocation.method( + #getScreenCoordinate, + [latLng], ), - returnValueForMissingStub: _i4.Future<_i2.PlatformPoint>.value( - _FakePlatformPoint_0( - this, - Invocation.method(#getScreenCoordinate, [latLng]), + returnValue: _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( + this, + Invocation.method( + #getScreenCoordinate, + [latLng], ), - ), + )), + returnValueForMissingStub: + _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( + this, + Invocation.method( + #getScreenCoordinate, + [latLng], + ), + )), ) as _i4.Future<_i2.PlatformPoint>); @override _i4.Future<_i2.PlatformLatLng> getLatLng( - _i2.PlatformPoint? screenCoordinate, - ) => + _i2.PlatformPoint? screenCoordinate) => (super.noSuchMethod( - Invocation.method(#getLatLng, [screenCoordinate]), - returnValue: _i4.Future<_i2.PlatformLatLng>.value( - _FakePlatformLatLng_1( - this, - Invocation.method(#getLatLng, [screenCoordinate]), - ), + Invocation.method( + #getLatLng, + [screenCoordinate], ), - returnValueForMissingStub: _i4.Future<_i2.PlatformLatLng>.value( - _FakePlatformLatLng_1( - this, - Invocation.method(#getLatLng, [screenCoordinate]), + returnValue: _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( + this, + Invocation.method( + #getLatLng, + [screenCoordinate], ), - ), + )), + returnValueForMissingStub: + _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( + this, + Invocation.method( + #getLatLng, + [screenCoordinate], + ), + )), ) as _i4.Future<_i2.PlatformLatLng>); @override _i4.Future<_i2.PlatformLatLngBounds> getVisibleRegion() => (super.noSuchMethod( - Invocation.method(#getVisibleRegion, []), + Invocation.method( + #getVisibleRegion, + [], + ), returnValue: _i4.Future<_i2.PlatformLatLngBounds>.value( - _FakePlatformLatLngBounds_2( - this, - Invocation.method(#getVisibleRegion, []), + _FakePlatformLatLngBounds_2( + this, + Invocation.method( + #getVisibleRegion, + [], ), - ), + )), returnValueForMissingStub: _i4.Future<_i2.PlatformLatLngBounds>.value( - _FakePlatformLatLngBounds_2( - this, - Invocation.method(#getVisibleRegion, []), + _FakePlatformLatLngBounds_2( + this, + Invocation.method( + #getVisibleRegion, + [], ), - ), + )), ) as _i4.Future<_i2.PlatformLatLngBounds>); @override _i4.Future moveCamera(_i2.PlatformCameraUpdate? cameraUpdate) => (super.noSuchMethod( - Invocation.method(#moveCamera, [cameraUpdate]), + Invocation.method( + #moveCamera, + [cameraUpdate], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -250,69 +337,105 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { int? durationMilliseconds, ) => (super.noSuchMethod( - Invocation.method(#animateCamera, [ - cameraUpdate, - durationMilliseconds, - ]), + Invocation.method( + #animateCamera, + [ + cameraUpdate, + durationMilliseconds, + ], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future getZoomLevel() => (super.noSuchMethod( - Invocation.method(#getZoomLevel, []), + Invocation.method( + #getZoomLevel, + [], + ), returnValue: _i4.Future.value(0.0), returnValueForMissingStub: _i4.Future.value(0.0), ) as _i4.Future); @override _i4.Future showInfoWindow(String? markerId) => (super.noSuchMethod( - Invocation.method(#showInfoWindow, [markerId]), + Invocation.method( + #showInfoWindow, + [markerId], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future hideInfoWindow(String? markerId) => (super.noSuchMethod( - Invocation.method(#hideInfoWindow, [markerId]), + Invocation.method( + #hideInfoWindow, + [markerId], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future isInfoWindowShown(String? markerId) => (super.noSuchMethod( - Invocation.method(#isInfoWindowShown, [markerId]), + Invocation.method( + #isInfoWindowShown, + [markerId], + ), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.Future); @override _i4.Future setStyle(String? style) => (super.noSuchMethod( - Invocation.method(#setStyle, [style]), + Invocation.method( + #setStyle, + [style], + ), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.Future); @override _i4.Future didLastStyleSucceed() => (super.noSuchMethod( - Invocation.method(#didLastStyleSucceed, []), + Invocation.method( + #didLastStyleSucceed, + [], + ), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) 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( - Invocation.method(#clearTileCache, [tileOverlayId]), + Invocation.method( + #clearTileCache, + [tileOverlayId], + ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future<_i5.Uint8List> takeSnapshot() => (super.noSuchMethod( - Invocation.method(#takeSnapshot, []), - returnValue: _i4.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - returnValueForMissingStub: _i4.Future<_i5.Uint8List>.value( - _i5.Uint8List(0), + Invocation.method( + #takeSnapshot, + [], ), + returnValue: _i4.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), + returnValueForMissingStub: + _i4.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), ) as _i4.Future<_i5.Uint8List>); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart index e106dbb2be8..51656e3fb72 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart @@ -21,7 +21,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. @@ -1229,6 +1229,97 @@ void main() { } }); + 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('testSetStyleMapId', (WidgetTester tester) async { final Key key = GlobalKey(); @@ -1237,7 +1328,7 @@ void main() { child: ExampleGoogleMap( key: key, initialCameraPosition: _kInitialCameraPosition, - cloudMapId: _kCloudMapId, + mapId: _kMapId, ), )); }); @@ -1890,6 +1981,39 @@ void main() { // Hanging in CI, https://github.com/flutter/flutter/issues/166139 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(); + await controllerCompleter.future; + }); } class _DebugTileProvider implements TileProvider { @@ -1937,7 +2061,9 @@ class _DebugTileProvider implements TileProvider { } Marker _copyMarkerWithClusterManagerId( - Marker marker, ClusterManagerId? clusterManagerId) { + Marker marker, + ClusterManagerId? clusterManagerId, +) { return Marker( markerId: marker.markerId, alpha: marker.alpha, @@ -2044,3 +2170,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_ios/example/ios14/ios/RunnerTests/FGMClusterManagersControllerTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/FGMClusterManagersControllerTests.m index 8d435eb4941..78a29e58c54 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/FGMClusterManagersControllerTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/FGMClusterManagersControllerTests.m @@ -35,7 +35,8 @@ - (void)testClustering { [[FLTMarkersController alloc] initWithMapView:mapView callbackHandler:handler clusterManagersController:clusterManagersController - registrar:registrar]; + registrar:registrar + markerType:FGMPlatformMarkerTypeMarker]; // Add cluster managers. NSString *clusterManagerId = @"cm"; @@ -71,7 +72,8 @@ - (void)testClustering { visible:YES zIndex:1 markerId:markerId1 - clusterManagerId:clusterManagerId]; + clusterManagerId:clusterManagerId + collisionBehavior:NULL]; FGMPlatformMarker *marker2 = [FGMPlatformMarker makeWithAlpha:1 anchor:zeroPoint consumeTapEvents:NO @@ -84,7 +86,8 @@ - (void)testClustering { visible:YES zIndex:1 markerId:markerId2 - clusterManagerId:clusterManagerId]; + clusterManagerId:clusterManagerId + collisionBehavior:NULL]; [markersController addMarkers:@[ marker1, marker2 ]]; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsMarkerControllerTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsMarkerControllerTests.m index 5b5329e9880..f42330b527b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsMarkerControllerTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsMarkerControllerTests.m @@ -33,7 +33,8 @@ - (FLTMarkersController *)markersControllerWithMapView:(GMSMapView *)mapView { return [[FLTMarkersController alloc] initWithMapView:mapView callbackHandler:[[FGMMapsCallbackApi alloc] init] clusterManagersController:nil - registrar:mockRegistrar]; + registrar:mockRegistrar + markerType:FGMPlatformMarkerTypeMarker]; } - (FGMPlatformBitmap *)placeholderBitmap { @@ -53,23 +54,24 @@ - (void)testSetsMarkerNumericProperties { double latitutde = 10.0; double longitude = 20.0; [controller addMarkers:@[ [FGMPlatformMarker - makeWithAlpha:alpha - anchor:[FGMPlatformPoint makeWithX:anchorX y:anchorY] - consumeTapEvents:YES - draggable:YES - flat:YES - icon:[self placeholderBitmap] - infoWindow:[FGMPlatformInfoWindow - makeWithTitle:@"info title" - snippet:@"info snippet" - anchor:[FGMPlatformPoint makeWithX:0 y:0]] - position:[FGMPlatformLatLng makeWithLatitude:latitutde - longitude:longitude] - rotation:rotation - visible:YES - zIndex:zIndex - markerId:markerIdentifier - clusterManagerId:nil] ]]; + makeWithAlpha:alpha + anchor:[FGMPlatformPoint makeWithX:anchorX y:anchorY] + consumeTapEvents:YES + draggable:YES + flat:YES + icon:[self placeholderBitmap] + infoWindow:[FGMPlatformInfoWindow + makeWithTitle:@"info title" + snippet:@"info snippet" + anchor:[FGMPlatformPoint makeWithX:0 y:0]] + position:[FGMPlatformLatLng makeWithLatitude:latitutde + longitude:longitude] + rotation:rotation + visible:YES + zIndex:zIndex + markerId:markerIdentifier + clusterManagerId:nil + collisionBehavior:nil] ]]; FLTGoogleMapMarkerController *markerController = controller.markerIdentifierToController[markerIdentifier]; @@ -93,22 +95,24 @@ - (void)testSetsDraggable { NSString *markerIdentifier = @"marker"; [controller addMarkers:@[ [FGMPlatformMarker - makeWithAlpha:1.0 - anchor:[FGMPlatformPoint makeWithX:0 y:0] - consumeTapEvents:NO - draggable:YES - flat:NO - icon:[self placeholderBitmap] - infoWindow:[FGMPlatformInfoWindow - makeWithTitle:@"info title" - snippet:@"info snippet" - anchor:[FGMPlatformPoint makeWithX:0 y:0]] - position:[FGMPlatformLatLng makeWithLatitude:0.0 longitude:0.0] - rotation:0 - visible:NO - zIndex:0 - markerId:markerIdentifier - clusterManagerId:nil] ]]; + makeWithAlpha:1.0 + anchor:[FGMPlatformPoint makeWithX:0 y:0] + consumeTapEvents:NO + draggable:YES + flat:NO + icon:[self placeholderBitmap] + infoWindow:[FGMPlatformInfoWindow + makeWithTitle:@"info title" + snippet:@"info snippet" + anchor:[FGMPlatformPoint makeWithX:0 y:0]] + position:[FGMPlatformLatLng makeWithLatitude:0.0 + longitude:0.0] + rotation:0 + visible:NO + zIndex:0 + markerId:markerIdentifier + clusterManagerId:nil + collisionBehavior:nil] ]]; FLTGoogleMapMarkerController *markerController = controller.markerIdentifierToController[markerIdentifier]; @@ -125,22 +129,24 @@ - (void)testSetsFlat { NSString *markerIdentifier = @"marker"; [controller addMarkers:@[ [FGMPlatformMarker - makeWithAlpha:1.0 - anchor:[FGMPlatformPoint makeWithX:0 y:0] - consumeTapEvents:NO - draggable:NO - flat:YES - icon:[self placeholderBitmap] - infoWindow:[FGMPlatformInfoWindow - makeWithTitle:@"info title" - snippet:@"info snippet" - anchor:[FGMPlatformPoint makeWithX:0 y:0]] - position:[FGMPlatformLatLng makeWithLatitude:0.0 longitude:0.0] - rotation:0 - visible:NO - zIndex:0 - markerId:markerIdentifier - clusterManagerId:nil] ]]; + makeWithAlpha:1.0 + anchor:[FGMPlatformPoint makeWithX:0 y:0] + consumeTapEvents:NO + draggable:NO + flat:YES + icon:[self placeholderBitmap] + infoWindow:[FGMPlatformInfoWindow + makeWithTitle:@"info title" + snippet:@"info snippet" + anchor:[FGMPlatformPoint makeWithX:0 y:0]] + position:[FGMPlatformLatLng makeWithLatitude:0.0 + longitude:0.0] + rotation:0 + visible:NO + zIndex:0 + markerId:markerIdentifier + clusterManagerId:nil + collisionBehavior:nil] ]]; FLTGoogleMapMarkerController *markerController = controller.markerIdentifierToController[markerIdentifier]; @@ -157,22 +163,24 @@ - (void)testSetsVisible { NSString *markerIdentifier = @"marker"; [controller addMarkers:@[ [FGMPlatformMarker - makeWithAlpha:1.0 - anchor:[FGMPlatformPoint makeWithX:0 y:0] - consumeTapEvents:NO - draggable:NO - flat:NO - icon:[self placeholderBitmap] - infoWindow:[FGMPlatformInfoWindow - makeWithTitle:@"info title" - snippet:@"info snippet" - anchor:[FGMPlatformPoint makeWithX:0 y:0]] - position:[FGMPlatformLatLng makeWithLatitude:0.0 longitude:0.0] - rotation:0 - visible:YES - zIndex:0 - markerId:markerIdentifier - clusterManagerId:nil] ]]; + makeWithAlpha:1.0 + anchor:[FGMPlatformPoint makeWithX:0 y:0] + consumeTapEvents:NO + draggable:NO + flat:NO + icon:[self placeholderBitmap] + infoWindow:[FGMPlatformInfoWindow + makeWithTitle:@"info title" + snippet:@"info snippet" + anchor:[FGMPlatformPoint makeWithX:0 y:0]] + position:[FGMPlatformLatLng makeWithLatitude:0.0 + longitude:0.0] + rotation:0 + visible:YES + zIndex:0 + markerId:markerIdentifier + clusterManagerId:nil + collisionBehavior:nil] ]]; FLTGoogleMapMarkerController *markerController = controller.markerIdentifierToController[markerIdentifier]; @@ -193,23 +201,24 @@ - (void)testSetsMarkerInfoWindowProperties { double anchorY = 2.718; [controller addMarkers:@[ [FGMPlatformMarker - makeWithAlpha:1.0 - anchor:[FGMPlatformPoint makeWithX:0 y:0] - consumeTapEvents:YES - draggable:YES - flat:YES - icon:[self placeholderBitmap] - infoWindow:[FGMPlatformInfoWindow - makeWithTitle:title - snippet:snippet - anchor:[FGMPlatformPoint makeWithX:anchorX - y:anchorY]] - position:[FGMPlatformLatLng makeWithLatitude:0 longitude:0] - rotation:0 - visible:YES - zIndex:0 - markerId:markerIdentifier - clusterManagerId:nil] ]]; + makeWithAlpha:1.0 + anchor:[FGMPlatformPoint makeWithX:0 y:0] + consumeTapEvents:YES + draggable:YES + flat:YES + icon:[self placeholderBitmap] + infoWindow:[FGMPlatformInfoWindow + makeWithTitle:title + snippet:snippet + anchor:[FGMPlatformPoint makeWithX:anchorX + y:anchorY]] + position:[FGMPlatformLatLng makeWithLatitude:0 longitude:0] + rotation:0 + visible:YES + zIndex:0 + markerId:markerIdentifier + clusterManagerId:nil + collisionBehavior:nil] ]]; FLTGoogleMapMarkerController *markerController = controller.markerIdentifierToController[markerIdentifier]; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart index d1d736a659a..bfde579e695 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/lib/main.dart @@ -3,8 +3,11 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:maps_example_dart/advanced_marker_icons.dart'; +import 'package:maps_example_dart/advanced_markers_clustering.dart'; import 'package:maps_example_dart/animate_camera.dart'; import 'package:maps_example_dart/clustering.dart'; +import 'package:maps_example_dart/collision_behavior.dart'; import 'package:maps_example_dart/ground_overlay.dart'; import 'package:maps_example_dart/lite_mode.dart'; import 'package:maps_example_dart/map_click.dart'; @@ -16,6 +19,7 @@ import 'package:maps_example_dart/marker_icons.dart'; import 'package:maps_example_dart/move_camera.dart'; import 'package:maps_example_dart/padding.dart'; import 'package:maps_example_dart/page.dart'; +import 'package:maps_example_dart/place_advanced_marker.dart'; import 'package:maps_example_dart/place_circle.dart'; import 'package:maps_example_dart/place_marker.dart'; import 'package:maps_example_dart/place_polygon.dart'; @@ -24,6 +28,10 @@ import 'package:maps_example_dart/scrolling_map.dart'; import 'package:maps_example_dart/snapshot.dart'; import 'package:maps_example_dart/tile_overlay.dart'; +/// Place your map ID here. Map ID is required for pages that use advanced +/// markers. +const String? _mapId = null; + void main() { runApp(const MaterialApp( home: MapsDemo([ @@ -33,7 +41,9 @@ void main() { AnimateCameraPage(), MoveCameraPage(), PlaceMarkerPage(), + PlaceAdvancedMarkerPage(mapId: _mapId), MarkerIconsPage(), + AdvancedMarkerIconsPage(mapId: _mapId), ScrollingMapPage(), PlacePolylinePage(), PlacePolygonPage(), @@ -44,6 +54,8 @@ void main() { TileOverlayPage(), GroundOverlayPage(), ClusteringPage(), + AdvancedMarkersClusteringPage(mapId: _mapId), MapIdPage(), + AdvancedMarkerCollisionBehaviorPage(mapId: _mapId), ]))); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml index 55e93119742..36f6d0ca311 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml @@ -32,3 +32,8 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_ios: {path: ../../../../google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../../google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart index d1d736a659a..bfde579e695 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/lib/main.dart @@ -3,8 +3,11 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:maps_example_dart/advanced_marker_icons.dart'; +import 'package:maps_example_dart/advanced_markers_clustering.dart'; import 'package:maps_example_dart/animate_camera.dart'; import 'package:maps_example_dart/clustering.dart'; +import 'package:maps_example_dart/collision_behavior.dart'; import 'package:maps_example_dart/ground_overlay.dart'; import 'package:maps_example_dart/lite_mode.dart'; import 'package:maps_example_dart/map_click.dart'; @@ -16,6 +19,7 @@ import 'package:maps_example_dart/marker_icons.dart'; import 'package:maps_example_dart/move_camera.dart'; import 'package:maps_example_dart/padding.dart'; import 'package:maps_example_dart/page.dart'; +import 'package:maps_example_dart/place_advanced_marker.dart'; import 'package:maps_example_dart/place_circle.dart'; import 'package:maps_example_dart/place_marker.dart'; import 'package:maps_example_dart/place_polygon.dart'; @@ -24,6 +28,10 @@ import 'package:maps_example_dart/scrolling_map.dart'; import 'package:maps_example_dart/snapshot.dart'; import 'package:maps_example_dart/tile_overlay.dart'; +/// Place your map ID here. Map ID is required for pages that use advanced +/// markers. +const String? _mapId = null; + void main() { runApp(const MaterialApp( home: MapsDemo([ @@ -33,7 +41,9 @@ void main() { AnimateCameraPage(), MoveCameraPage(), PlaceMarkerPage(), + PlaceAdvancedMarkerPage(mapId: _mapId), MarkerIconsPage(), + AdvancedMarkerIconsPage(mapId: _mapId), ScrollingMapPage(), PlacePolylinePage(), PlacePolygonPage(), @@ -44,6 +54,8 @@ void main() { TileOverlayPage(), GroundOverlayPage(), ClusteringPage(), + AdvancedMarkersClusteringPage(mapId: _mapId), MapIdPage(), + AdvancedMarkerCollisionBehaviorPage(mapId: _mapId), ]))); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml index 55e93119742..36f6d0ca311 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml @@ -32,3 +32,8 @@ flutter: uses-material-design: true assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_ios: {path: ../../../../google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../../google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/advanced_marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/advanced_marker_icons.dart new file mode 100644 index 00000000000..f7cc9a82d56 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/advanced_marker_icons.dart @@ -0,0 +1,160 @@ +// 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_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; +import 'place_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 = {}; + + 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_ios/example/shared/maps_example_dart/lib/advanced_markers_clustering.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/advanced_markers_clustering.dart new file mode 100644 index 00000000000..542cee61b94 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/advanced_markers_clustering.dart @@ -0,0 +1,311 @@ +// 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_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'clustering.dart'; +import 'example_google_map.dart'; +import 'page.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: [ + 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_ios/example/shared/maps_example_dart/lib/clustering.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/clustering.dart index f6860f7bf42..ddc9a209de5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/clustering.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/clustering.dart @@ -18,21 +18,30 @@ 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(); + + /// 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, + ); + } } /// State of the clustering page. -class ClusteringBodyState extends State { +class ClusteringBodyState extends State<_ClusteringBody> { /// Default Constructor. ClusteringBodyState(); @@ -95,16 +104,13 @@ 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 = + widget.copyWithSelectedState(markers[previousMarkerId]!, false); markers[previousMarkerId] = resetOld; } selectedMarker = markerId; - final Marker newMarker = tappedMarker.copyWith( - iconParam: BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueGreen, - ), - ); + final Marker newMarker = + widget.copyWithSelectedState(tappedMarker, true); markers[markerId] = newMarker; }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/collision_behavior.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/collision_behavior.dart new file mode 100644 index 00000000000..5363688ab23 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/collision_behavior.dart @@ -0,0 +1,159 @@ +// 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_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; +import 'place_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_ios/example/shared/maps_example_dart/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart index 351e1c117c4..b9fc81c46ad 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart @@ -16,10 +16,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf /// Controller for a single ExampleGoogleMap instance running on the host platform. class ExampleGoogleMapController { - ExampleGoogleMapController._( - this._googleMapState, { - required this.mapId, - }) { + ExampleGoogleMapController._(this._googleMapState, {required this.mapId}) { _connectStreams(mapId); } @@ -36,10 +33,7 @@ class ExampleGoogleMapController { _ExampleGoogleMapState googleMapState, ) async { await GoogleMapsFlutterPlatform.instance.init(id); - return ExampleGoogleMapController._( - googleMapState, - mapId: id, - ); + return ExampleGoogleMapController._(googleMapState, mapId: id); } final _ExampleGoogleMapState _googleMapState; @@ -52,7 +46,9 @@ class ExampleGoogleMapController { } if (_googleMapState.widget.onCameraMove != null) { GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen( - (CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value)); + (CameraMoveEvent e) => + _googleMapState.widget.onCameraMove!(e.value), + ); } if (_googleMapState.widget.onCameraIdle != null) { GoogleMapsFlutterPlatform.instance @@ -63,16 +59,20 @@ class ExampleGoogleMapController { .onMarkerTap(mapId: mapId) .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)); GoogleMapsFlutterPlatform.instance.onMarkerDragStart(mapId: mapId).listen( - (MarkerDragStartEvent e) => - _googleMapState.onMarkerDragStart(e.value, e.position)); + (MarkerDragStartEvent e) => + _googleMapState.onMarkerDragStart(e.value, e.position), + ); GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen( - (MarkerDragEvent e) => - _googleMapState.onMarkerDrag(e.value, e.position)); + (MarkerDragEvent e) => + _googleMapState.onMarkerDrag(e.value, e.position), + ); GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( - (MarkerDragEndEvent e) => - _googleMapState.onMarkerDragEnd(e.value, e.position)); + (MarkerDragEndEvent e) => + _googleMapState.onMarkerDragEnd(e.value, e.position), + ); GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen( - (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value)); + (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value), + ); GoogleMapsFlutterPlatform.instance .onPolylineTap(mapId: mapId) .listen((PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value)); @@ -83,13 +83,15 @@ class ExampleGoogleMapController { .onCircleTap(mapId: mapId) .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)); GoogleMapsFlutterPlatform.instance.onGroundOverlayTap(mapId: mapId).listen( - (GroundOverlayTapEvent e) => - _googleMapState.onGroundOverlayTap(e.value)); + (GroundOverlayTapEvent e) => + _googleMapState.onGroundOverlayTap(e.value), + ); GoogleMapsFlutterPlatform.instance .onTap(mapId: mapId) .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( - (MapLongPressEvent e) => _googleMapState.onLongPress(e.position)); + (MapLongPressEvent e) => _googleMapState.onLongPress(e.position), + ); GoogleMapsFlutterPlatform.instance .onClusterTap(mapId: mapId) .listen((ClusterTapEvent e) => _googleMapState.onClusterTap(e.value)); @@ -97,72 +99,96 @@ class ExampleGoogleMapController { /// Updates configuration options of the map user interface. Future _updateMapConfiguration(MapConfiguration update) { - return GoogleMapsFlutterPlatform.instance - .updateMapConfiguration(update, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.updateMapConfiguration( + update, + mapId: mapId, + ); } /// Updates marker configuration. Future _updateMarkers(MarkerUpdates markerUpdates) { - return GoogleMapsFlutterPlatform.instance - .updateMarkers(markerUpdates, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.updateMarkers( + markerUpdates, + mapId: mapId, + ); } /// Updates cluster manager configuration. Future _updateClusterManagers( - ClusterManagerUpdates clusterManagerUpdates) { - return GoogleMapsFlutterPlatform.instance - .updateClusterManagers(clusterManagerUpdates, mapId: mapId); + ClusterManagerUpdates clusterManagerUpdates, + ) { + return GoogleMapsFlutterPlatform.instance.updateClusterManagers( + clusterManagerUpdates, + mapId: mapId, + ); } /// Updates ground overlay configuration. Future _updateGroundOverlays( - GroundOverlayUpdates groundOverlayUpdates) { - return GoogleMapsFlutterPlatform.instance - .updateGroundOverlays(groundOverlayUpdates, mapId: mapId); + GroundOverlayUpdates groundOverlayUpdates, + ) { + return GoogleMapsFlutterPlatform.instance.updateGroundOverlays( + groundOverlayUpdates, + mapId: mapId, + ); } /// Updates polygon configuration. Future _updatePolygons(PolygonUpdates polygonUpdates) { - return GoogleMapsFlutterPlatform.instance - .updatePolygons(polygonUpdates, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.updatePolygons( + polygonUpdates, + mapId: mapId, + ); } /// Updates polyline configuration. Future _updatePolylines(PolylineUpdates polylineUpdates) { - return GoogleMapsFlutterPlatform.instance - .updatePolylines(polylineUpdates, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.updatePolylines( + polylineUpdates, + mapId: mapId, + ); } /// Updates circle configuration. Future _updateCircles(CircleUpdates circleUpdates) { - return GoogleMapsFlutterPlatform.instance - .updateCircles(circleUpdates, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.updateCircles( + circleUpdates, + mapId: mapId, + ); } /// Updates tile overlays configuration. Future _updateTileOverlays(Set newTileOverlays) { - return GoogleMapsFlutterPlatform.instance - .updateTileOverlays(newTileOverlays: newTileOverlays, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.updateTileOverlays( + newTileOverlays: newTileOverlays, + mapId: mapId, + ); } /// Clears the tile cache so that all tiles will be requested again from the /// [TileProvider]. Future clearTileCache(TileOverlayId tileOverlayId) async { - return GoogleMapsFlutterPlatform.instance - .clearTileCache(tileOverlayId, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.clearTileCache( + tileOverlayId, + mapId: mapId, + ); } /// Starts an animated change of the map camera position. Future animateCamera(CameraUpdate cameraUpdate, {Duration? duration}) { return GoogleMapsFlutterPlatform.instance.animateCameraWithConfiguration( - cameraUpdate, CameraUpdateAnimationConfiguration(duration: duration), - mapId: mapId); + cameraUpdate, + CameraUpdateAnimationConfiguration(duration: duration), + mapId: mapId, + ); } /// Changes the map camera position. Future moveCamera(CameraUpdate cameraUpdate) { - return GoogleMapsFlutterPlatform.instance - .moveCamera(cameraUpdate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.moveCamera( + cameraUpdate, + mapId: mapId, + ); } /// Return [LatLngBounds] defining the region that is visible in a map. @@ -172,32 +198,42 @@ class ExampleGoogleMapController { /// Return [ScreenCoordinate] of the [LatLng] in the current map view. Future getScreenCoordinate(LatLng latLng) { - return GoogleMapsFlutterPlatform.instance - .getScreenCoordinate(latLng, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.getScreenCoordinate( + latLng, + mapId: mapId, + ); } /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. Future getLatLng(ScreenCoordinate screenCoordinate) { - return GoogleMapsFlutterPlatform.instance - .getLatLng(screenCoordinate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.getLatLng( + screenCoordinate, + mapId: mapId, + ); } /// Programmatically show the Info Window for a [Marker]. Future showMarkerInfoWindow(MarkerId markerId) { - return GoogleMapsFlutterPlatform.instance - .showMarkerInfoWindow(markerId, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.showMarkerInfoWindow( + markerId, + mapId: mapId, + ); } /// Programmatically hide the Info Window for a [Marker]. Future hideMarkerInfoWindow(MarkerId markerId) { - return GoogleMapsFlutterPlatform.instance - .hideMarkerInfoWindow(markerId, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.hideMarkerInfoWindow( + markerId, + mapId: mapId, + ); } /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. Future isMarkerInfoWindowShown(MarkerId markerId) { - return GoogleMapsFlutterPlatform.instance - .isMarkerInfoWindowShown(markerId, mapId: mapId); + return GoogleMapsFlutterPlatform.instance.isMarkerInfoWindowShown( + markerId, + mapId: mapId, + ); } /// Returns the current zoom level of the map @@ -215,6 +251,13 @@ class ExampleGoogleMapController { return GoogleMapsFlutterPlatform.instance.getStyleError(mapId: mapId); } + /// Returns true if [AdvancedMarker]s can be used with this map. + Future isAdvancedMarkersAvailable() { + return GoogleMapsFlutterPlatform.instance.isAdvancedMarkersAvailable( + mapId: mapId, + ); + } + /// Disposes of the platform resources void dispose() { GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId); @@ -260,11 +303,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, }); @@ -372,11 +416,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(); @@ -436,8 +483,9 @@ class _ExampleGoogleMapState extends State { @override void dispose() { - _controller.future - .then((ExampleGoogleMapController controller) => controller.dispose()); + _controller.future.then( + (ExampleGoogleMapController controller) => controller.dispose(), + ); super.dispose(); } @@ -467,43 +515,67 @@ class _ExampleGoogleMapState extends State { Future _updateMarkers() async { final ExampleGoogleMapController controller = await _controller.future; - unawaited(controller._updateMarkers( - MarkerUpdates.from(_markers.values.toSet(), widget.markers))); + unawaited( + controller._updateMarkers( + MarkerUpdates.from(_markers.values.toSet(), widget.markers), + ), + ); _markers = keyByMarkerId(widget.markers); } Future _updateClusterManagers() async { final ExampleGoogleMapController controller = await _controller.future; - unawaited(controller._updateClusterManagers(ClusterManagerUpdates.from( - _clusterManagers.values.toSet(), widget.clusterManagers))); + unawaited( + controller._updateClusterManagers( + ClusterManagerUpdates.from( + _clusterManagers.values.toSet(), + widget.clusterManagers, + ), + ), + ); _clusterManagers = keyByClusterManagerId(widget.clusterManagers); } Future _updateGroundOverlays() async { final ExampleGoogleMapController controller = await _controller.future; - unawaited(controller._updateGroundOverlays(GroundOverlayUpdates.from( - _groundOverlays.values.toSet(), widget.groundOverlays))); + unawaited( + controller._updateGroundOverlays( + GroundOverlayUpdates.from( + _groundOverlays.values.toSet(), + widget.groundOverlays, + ), + ), + ); _groundOverlays = keyByGroundOverlayId(widget.groundOverlays); } Future _updatePolygons() async { final ExampleGoogleMapController controller = await _controller.future; - unawaited(controller._updatePolygons( - PolygonUpdates.from(_polygons.values.toSet(), widget.polygons))); + unawaited( + controller._updatePolygons( + PolygonUpdates.from(_polygons.values.toSet(), widget.polygons), + ), + ); _polygons = keyByPolygonId(widget.polygons); } Future _updatePolylines() async { final ExampleGoogleMapController controller = await _controller.future; - unawaited(controller._updatePolylines( - PolylineUpdates.from(_polylines.values.toSet(), widget.polylines))); + unawaited( + controller._updatePolylines( + PolylineUpdates.from(_polylines.values.toSet(), widget.polylines), + ), + ); _polylines = keyByPolylineId(widget.polylines); } Future _updateCircles() async { final ExampleGoogleMapController controller = await _controller.future; - unawaited(controller._updateCircles( - CircleUpdates.from(_circles.values.toSet(), widget.circles))); + unawaited( + controller._updateCircles( + CircleUpdates.from(_circles.values.toSet(), widget.circles), + ), + ); _circles = keyByCircleId(widget.circles); } @@ -594,7 +666,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_ios/example/shared/maps_example_dart/lib/map_map_id.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_map_id.dart index 037bafd8a35..fd46cf500c3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_map_id.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_map_id.dart @@ -54,7 +54,7 @@ class MapIdBodyState extends State { zoom: 7.0, ), key: _key, - cloudMapId: _mapId, + mapId: _mapId, ); final List columnChildren = [ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart index df4f79205e8..2c1a8d2a64d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart @@ -21,15 +21,15 @@ 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); @@ -42,7 +42,7 @@ enum _MarkerSizeOption { 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_ios/example/shared/maps_example_dart/lib/place_advanced_marker.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_advanced_marker.dart new file mode 100644 index 00000000000..1200f68ae33 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_advanced_marker.dart @@ -0,0 +1,458 @@ +// 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_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; + + /// Whether map supports advanced markers. Null indicates capability check + /// is in progress. + bool? _isAdvancedMarkersAvailable; + + void _onMapCreated(ExampleGoogleMapController controller) { + setState(() { + this.controller = controller; + }); + + GoogleMapsFlutterPlatform.instance + .isAdvancedMarkersAvailable(mapId: controller.mapId) + .then((bool result) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _isAdvancedMarkersAvailable = result; + }); + }); + }); + } + + 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: [ + 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, + }, + ), + ), + ), + 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}')), + ], + ), + ), + ), + ], + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart index d282602d8dc..861979493fd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart @@ -21,21 +21,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); ExampleGoogleMapController? controller; @@ -46,9 +44,10 @@ class PlaceMarkerBodyState extends State { // A helper text for Xcode UITests. String _onDragXcodeUITestHelperText = ''; - // ignore: use_setters_to_change_properties void _onMapCreated(ExampleGoogleMapController controller) { - this.controller = controller; + setState(() { + this.controller = controller; + }); } @override @@ -62,16 +61,12 @@ 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; @@ -292,6 +287,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; @@ -422,3 +426,62 @@ class PlaceMarkerBodyState extends State { ]); } } + +/// 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. 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_ios/example/shared/maps_example_dart/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml index ccfab1b92ff..d84810480bc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml @@ -27,3 +27,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_ios: {path: ../../../../../google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../../../google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h index 8aa1bcb6793..82562870b74 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.h @@ -4,6 +4,7 @@ #import #import +#import "FLTGoogleMapJSONConversions.h" #import "messages.g.h" NS_ASSUME_NONNULL_BEGIN diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m index 3830ea28f60..c2383b4c449 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageUtils.m @@ -125,6 +125,43 @@ reason:@"Unable to interpret bytes as a valid image." userInfo:nil]; } + } else if ([bitmap isKindOfClass:[FGMPlatformBitmapPinConfig class]]) { + FGMPlatformBitmapPinConfig *pinConfig = bitmap; + + GMSPinImageOptions *options = [[GMSPinImageOptions alloc] init]; + NSNumber *backgroundColor = pinConfig.backgroundColor; + if (backgroundColor) { + options.backgroundColor = FGMGetColorForRGBA([backgroundColor integerValue]); + } + + NSNumber *borderColor = pinConfig.borderColor; + if (borderColor) { + options.borderColor = FGMGetColorForRGBA([borderColor integerValue]); + } + + GMSPinImageGlyph *glyph; + NSString *glyphText = pinConfig.glyphText; + NSNumber *glyphColor = pinConfig.glyphColor; + FGMPlatformBitmap *glyphBitmap = pinConfig.glyphBitmap; + if (glyphText) { + NSNumber *glyphTextColorInt = pinConfig.glyphTextColor; + UIColor *glyphTextColor = glyphTextColorInt + ? FGMGetColorForRGBA([glyphTextColorInt integerValue]) + : [UIColor blackColor]; + glyph = [[GMSPinImageGlyph alloc] initWithText:glyphText textColor:glyphTextColor]; + } else if (glyphColor) { + UIColor *color = FGMGetColorForRGBA([glyphColor integerValue]); + glyph = [[GMSPinImageGlyph alloc] initWithGlyphColor:color]; + } else if (glyphBitmap) { + UIImage *glyphImage = FGMIconFromBitmap(glyphBitmap, registrar, screenScale); + glyph = [[GMSPinImageGlyph alloc] initWithImage:glyphImage]; + } + + if (glyph) { + options.glyph = glyph; + } + + image = [GMSPinImage pinImageWithOptions:options]; } return image; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m index 6844a67fb9b..ee32149a8bb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m @@ -135,9 +135,9 @@ - (instancetype)initWithFrame:(CGRect)frame GMSMapViewOptions *options = [[GMSMapViewOptions alloc] init]; options.frame = frame; options.camera = camera; - NSString *cloudMapId = creationParameters.mapConfiguration.cloudMapId; - if (cloudMapId) { - options.mapID = [GMSMapID mapIDWithIdentifier:cloudMapId]; + NSString *mapId = creationParameters.mapConfiguration.mapId; + if (mapId) { + options.mapID = [GMSMapID mapIDWithIdentifier:mapId]; } GMSMapView *mapView = [[GMSMapView alloc] initWithOptions:options]; @@ -162,6 +162,7 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView NSString *pigeonSuffix = [NSString stringWithFormat:@"%lld", viewId]; _dartCallbackHandler = [[FGMMapsCallbackApi alloc] initWithBinaryMessenger:registrar.messenger messageChannelSuffix:pigeonSuffix]; + FGMPlatformMarkerTypeBox *markerType = creationParameters.mapConfiguration.markerType; _mapView.delegate = self; _mapView.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorNever; _registrar = registrar; @@ -171,7 +172,8 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView _markersController = [[FLTMarkersController alloc] initWithMapView:_mapView callbackHandler:_dartCallbackHandler clusterManagersController:_clusterManagersController - registrar:registrar]; + registrar:registrar + markerType:markerType.value]; _polygonsController = [[FLTPolygonsController alloc] initWithMapView:_mapView callbackHandler:_dartCallbackHandler registrar:registrar]; @@ -731,6 +733,13 @@ - (nullable FlutterStandardTypedData *)takeSnapshotWithError: return imageData ? [FlutterStandardTypedData typedDataWithBytes:imageData] : nil; } +- (nullable NSNumber *)isAdvancedMarkersAvailable: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSUInteger advancedMarkerFlag = + self.controller.mapView.mapCapabilities & GMSMapCapabilityFlagsAdvancedMarkers; + return [NSNumber numberWithBool:(advancedMarkerFlag != 0)]; +} + @end #pragma mark - diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.h index f10f2af2188..af3ce87f0cd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.h @@ -27,7 +27,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithMapView:(GMSMapView *)mapView callbackHandler:(FGMMapsCallbackApi *)callbackHandler clusterManagersController:(nullable FGMClusterManagersController *)clusterManagersController - registrar:(NSObject *)registrar; + registrar:(NSObject *)registrar + markerType:(FGMPlatformMarkerType)markerType; - (void)addMarkers:(NSArray *)markersToAdd; - (void)changeMarkers:(NSArray *)markersToChange; - (void)removeMarkersWithIdentifiers:(NSArray *)identifiers; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m index f86cb3620c2..0ca06ebbed3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController.m @@ -108,6 +108,13 @@ - (void)setZIndex:(int)zIndex { self.marker.zIndex = zIndex; } +- (void)setCollisionBehavior:(FGMPlatformMarkerCollisionBehaviorBox *)collisionBehavior { + if ([self.marker isKindOfClass:[GMSAdvancedMarker class]]) { + GMSCollisionBehavior collitionBehaviorValue = (GMSCollisionBehavior)collisionBehavior.value; + [(GMSAdvancedMarker *)self.marker setCollisionBehavior:(collitionBehaviorValue)]; + } +} + - (void)updateFromPlatformMarker:(FGMPlatformMarker *)platformMarker registrar:(NSObject *)registrar screenScale:(CGFloat)screenScale { @@ -122,6 +129,7 @@ - (void)updateFromPlatformMarker:(FGMPlatformMarker *)platformMarker [self setPosition:FGMGetCoordinateForPigeonLatLng(platformMarker.position)]; [self setRotation:platformMarker.rotation]; [self setZIndex:platformMarker.zIndex]; + [self setCollisionBehavior:platformMarker.collisionBehavior]; FGMPlatformInfoWindow *infoWindow = platformMarker.infoWindow; [self setInfoWindowAnchor:FGMGetCGPointForPigeonPoint(infoWindow.anchor)]; if (infoWindow.title) { @@ -147,6 +155,7 @@ @interface FLTMarkersController () @property(weak, nonatomic, nullable) FGMClusterManagersController *clusterManagersController; @property(weak, nonatomic) NSObject *registrar; @property(weak, nonatomic) GMSMapView *mapView; +@property(nonatomic) FGMPlatformMarkerType markerType; @end @@ -155,7 +164,8 @@ @implementation FLTMarkersController - (instancetype)initWithMapView:(GMSMapView *)mapView callbackHandler:(FGMMapsCallbackApi *)callbackHandler clusterManagersController:(nullable FGMClusterManagersController *)clusterManagersController - registrar:(NSObject *)registrar { + registrar:(NSObject *)registrar + markerType:(FGMPlatformMarkerType)markerType { self = [super init]; if (self) { _callbackHandler = callbackHandler; @@ -163,6 +173,7 @@ - (instancetype)initWithMapView:(GMSMapView *)mapView _clusterManagersController = clusterManagersController; _markerIdentifierToController = [[NSMutableDictionary alloc] init]; _registrar = registrar; + _markerType = markerType; } return self; } @@ -177,7 +188,10 @@ - (void)addMarker:(FGMPlatformMarker *)markerToAdd { CLLocationCoordinate2D position = FGMGetCoordinateForPigeonLatLng(markerToAdd.position); NSString *markerIdentifier = markerToAdd.markerId; NSString *clusterManagerIdentifier = markerToAdd.clusterManagerId; - GMSMarker *marker = [GMSMarker markerWithPosition:position]; + GMSMarker *marker = (self.markerType == FGMPlatformMarkerTypeAdvancedMarker) + ? [GMSAdvancedMarker markerWithPosition:position] + : [GMSMarker markerWithPosition:position]; + FLTGoogleMapMarkerController *controller = [[FLTGoogleMapMarkerController alloc] initWithMarker:marker markerIdentifier:markerIdentifier diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h index 0d1c3935e8a..c6b3901392f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -28,6 +28,18 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapType) { - (instancetype)initWithValue:(FGMPlatformMapType)value; @end +typedef NS_ENUM(NSUInteger, FGMPlatformMarkerCollisionBehavior) { + FGMPlatformMarkerCollisionBehaviorRequiredDisplay = 0, + FGMPlatformMarkerCollisionBehaviorOptionalAndHidesLowerPriority = 1, + FGMPlatformMarkerCollisionBehaviorRequiredAndHidesOptional = 2, +}; + +/// Wrapper for FGMPlatformMarkerCollisionBehavior to allow for nullability. +@interface FGMPlatformMarkerCollisionBehaviorBox : NSObject +@property(nonatomic, assign) FGMPlatformMarkerCollisionBehavior value; +- (instancetype)initWithValue:(FGMPlatformMarkerCollisionBehavior)value; +@end + /// Join types for polyline joints. typedef NS_ENUM(NSUInteger, FGMPlatformJointType) { FGMPlatformJointTypeMitered = 0, @@ -54,6 +66,17 @@ typedef NS_ENUM(NSUInteger, FGMPlatformPatternItemType) { - (instancetype)initWithValue:(FGMPlatformPatternItemType)value; @end +typedef NS_ENUM(NSUInteger, FGMPlatformMarkerType) { + FGMPlatformMarkerTypeMarker = 0, + FGMPlatformMarkerTypeAdvancedMarker = 1, +}; + +/// Wrapper for FGMPlatformMarkerType to allow for nullability. +@interface FGMPlatformMarkerTypeBox : NSObject +@property(nonatomic, assign) FGMPlatformMarkerType value; +- (instancetype)initWithValue:(FGMPlatformMarkerType)value; +@end + /// Pigeon equivalent of [MapBitmapScaling]. typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { FGMPlatformMapBitmapScalingAuto = 0, @@ -105,6 +128,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @class FGMPlatformBitmapAssetImage; @class FGMPlatformBitmapAssetMap; @class FGMPlatformBitmapBytesMap; +@class FGMPlatformBitmapPinConfig; /// Pigeon representatation of a CameraPosition. @interface FGMPlatformCameraPosition : NSObject @@ -285,7 +309,8 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { visible:(BOOL)visible zIndex:(double)zIndex markerId:(NSString *)markerId - clusterManagerId:(nullable NSString *)clusterManagerId; + clusterManagerId:(nullable NSString *)clusterManagerId + collisionBehavior:(nullable FGMPlatformMarkerCollisionBehaviorBox *)collisionBehavior; @property(nonatomic, assign) double alpha; @property(nonatomic, strong) FGMPlatformPoint *anchor; @property(nonatomic, assign) BOOL consumeTapEvents; @@ -299,6 +324,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, assign) double zIndex; @property(nonatomic, copy) NSString *markerId; @property(nonatomic, copy, nullable) NSString *clusterManagerId; +@property(nonatomic, strong, nullable) FGMPlatformMarkerCollisionBehaviorBox *collisionBehavior; @end /// Pigeon equivalent of the Polygon class. @@ -505,7 +531,8 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { indoorViewEnabled:(nullable NSNumber *)indoorViewEnabled trafficEnabled:(nullable NSNumber *)trafficEnabled buildingsEnabled:(nullable NSNumber *)buildingsEnabled - cloudMapId:(nullable NSString *)cloudMapId + markerType:(nullable FGMPlatformMarkerTypeBox *)markerType + mapId:(nullable NSString *)mapId style:(nullable NSString *)style; @property(nonatomic, strong, nullable) NSNumber *compassEnabled; @property(nonatomic, strong, nullable) FGMPlatformCameraTargetBounds *cameraTargetBounds; @@ -522,7 +549,8 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) NSNumber *indoorViewEnabled; @property(nonatomic, strong, nullable) NSNumber *trafficEnabled; @property(nonatomic, strong, nullable) NSNumber *buildingsEnabled; -@property(nonatomic, copy, nullable) NSString *cloudMapId; +@property(nonatomic, strong, nullable) FGMPlatformMarkerTypeBox *markerType; +@property(nonatomic, copy, nullable) NSString *mapId; @property(nonatomic, copy, nullable) NSString *style; @end @@ -651,6 +679,22 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) NSNumber *height; @end +/// Pigeon equivalent of [PinConfig]. +@interface FGMPlatformBitmapPinConfig : NSObject ++ (instancetype)makeWithBackgroundColor:(nullable NSNumber *)backgroundColor + borderColor:(nullable NSNumber *)borderColor + glyphColor:(nullable NSNumber *)glyphColor + glyphTextColor:(nullable NSNumber *)glyphTextColor + glyphText:(nullable NSString *)glyphText + glyphBitmap:(nullable FGMPlatformBitmap *)glyphBitmap; +@property(nonatomic, strong, nullable) NSNumber *backgroundColor; +@property(nonatomic, strong, nullable) NSNumber *borderColor; +@property(nonatomic, strong, nullable) NSNumber *glyphColor; +@property(nonatomic, strong, nullable) NSNumber *glyphTextColor; +@property(nonatomic, copy, nullable) NSString *glyphText; +@property(nonatomic, strong, nullable) FGMPlatformBitmap *glyphBitmap; +@end + /// The codec used by all APIs. NSObject *FGMGetMessagesCodec(void); @@ -763,6 +807,10 @@ NSObject *FGMGetMessagesCodec(void); /// Takes a snapshot of the map and returns its image data. - (nullable FlutterStandardTypedData *)takeSnapshotWithError: (FlutterError *_Nullable *_Nonnull)error; +/// Returns true if the map supports advanced markers. +/// +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isAdvancedMarkersAvailable:(FlutterError *_Nullable *_Nonnull)error; @end extern void SetUpFGMMapsApi(id binaryMessenger, diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m index ccf0735ec94..4555717e082 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" @@ -50,6 +50,16 @@ - (instancetype)initWithValue:(FGMPlatformMapType)value { } @end +@implementation FGMPlatformMarkerCollisionBehaviorBox +- (instancetype)initWithValue:(FGMPlatformMarkerCollisionBehavior)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + /// Join types for polyline joints. @implementation FGMPlatformJointTypeBox - (instancetype)initWithValue:(FGMPlatformJointType)value { @@ -72,6 +82,16 @@ - (instancetype)initWithValue:(FGMPlatformPatternItemType)value { } @end +@implementation FGMPlatformMarkerTypeBox +- (instancetype)initWithValue:(FGMPlatformMarkerType)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + /// Pigeon equivalent of [MapBitmapScaling]. @implementation FGMPlatformMapBitmapScalingBox - (instancetype)initWithValue:(FGMPlatformMapBitmapScaling)value { @@ -317,6 +337,12 @@ + (nullable FGMPlatformBitmapBytesMap *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FGMPlatformBitmapPinConfig () ++ (FGMPlatformBitmapPinConfig *)fromList:(NSArray *)list; ++ (nullable FGMPlatformBitmapPinConfig *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FGMPlatformCameraPosition + (instancetype)makeWithBearing:(double)bearing target:(FGMPlatformLatLng *)target @@ -727,7 +753,8 @@ + (instancetype)makeWithAlpha:(double)alpha visible:(BOOL)visible zIndex:(double)zIndex markerId:(NSString *)markerId - clusterManagerId:(nullable NSString *)clusterManagerId { + clusterManagerId:(nullable NSString *)clusterManagerId + collisionBehavior:(nullable FGMPlatformMarkerCollisionBehaviorBox *)collisionBehavior { FGMPlatformMarker *pigeonResult = [[FGMPlatformMarker alloc] init]; pigeonResult.alpha = alpha; pigeonResult.anchor = anchor; @@ -742,6 +769,7 @@ + (instancetype)makeWithAlpha:(double)alpha pigeonResult.zIndex = zIndex; pigeonResult.markerId = markerId; pigeonResult.clusterManagerId = clusterManagerId; + pigeonResult.collisionBehavior = collisionBehavior; return pigeonResult; } + (FGMPlatformMarker *)fromList:(NSArray *)list { @@ -759,6 +787,7 @@ + (FGMPlatformMarker *)fromList:(NSArray *)list { pigeonResult.zIndex = [GetNullableObjectAtIndex(list, 10) doubleValue]; pigeonResult.markerId = GetNullableObjectAtIndex(list, 11); pigeonResult.clusterManagerId = GetNullableObjectAtIndex(list, 12); + pigeonResult.collisionBehavior = GetNullableObjectAtIndex(list, 13); return pigeonResult; } + (nullable FGMPlatformMarker *)nullableFromList:(NSArray *)list { @@ -779,6 +808,7 @@ + (nullable FGMPlatformMarker *)nullableFromList:(NSArray *)list { @(self.zIndex), self.markerId ?: [NSNull null], self.clusterManagerId ?: [NSNull null], + self.collisionBehavior ?: [NSNull null], ]; } @end @@ -1232,7 +1262,8 @@ + (instancetype)makeWithCompassEnabled:(nullable NSNumber *)compassEnabled indoorViewEnabled:(nullable NSNumber *)indoorViewEnabled trafficEnabled:(nullable NSNumber *)trafficEnabled buildingsEnabled:(nullable NSNumber *)buildingsEnabled - cloudMapId:(nullable NSString *)cloudMapId + markerType:(nullable FGMPlatformMarkerTypeBox *)markerType + mapId:(nullable NSString *)mapId style:(nullable NSString *)style { FGMPlatformMapConfiguration *pigeonResult = [[FGMPlatformMapConfiguration alloc] init]; pigeonResult.compassEnabled = compassEnabled; @@ -1250,7 +1281,8 @@ + (instancetype)makeWithCompassEnabled:(nullable NSNumber *)compassEnabled pigeonResult.indoorViewEnabled = indoorViewEnabled; pigeonResult.trafficEnabled = trafficEnabled; pigeonResult.buildingsEnabled = buildingsEnabled; - pigeonResult.cloudMapId = cloudMapId; + pigeonResult.markerType = markerType; + pigeonResult.mapId = mapId; pigeonResult.style = style; return pigeonResult; } @@ -1271,8 +1303,9 @@ + (FGMPlatformMapConfiguration *)fromList:(NSArray *)list { pigeonResult.indoorViewEnabled = GetNullableObjectAtIndex(list, 12); pigeonResult.trafficEnabled = GetNullableObjectAtIndex(list, 13); pigeonResult.buildingsEnabled = GetNullableObjectAtIndex(list, 14); - pigeonResult.cloudMapId = GetNullableObjectAtIndex(list, 15); - pigeonResult.style = GetNullableObjectAtIndex(list, 16); + pigeonResult.markerType = GetNullableObjectAtIndex(list, 15); + pigeonResult.mapId = GetNullableObjectAtIndex(list, 16); + pigeonResult.style = GetNullableObjectAtIndex(list, 17); return pigeonResult; } + (nullable FGMPlatformMapConfiguration *)nullableFromList:(NSArray *)list { @@ -1295,7 +1328,8 @@ + (nullable FGMPlatformMapConfiguration *)nullableFromList:(NSArray *)list { self.indoorViewEnabled ?: [NSNull null], self.trafficEnabled ?: [NSNull null], self.buildingsEnabled ?: [NSNull null], - self.cloudMapId ?: [NSNull null], + self.markerType ?: [NSNull null], + self.mapId ?: [NSNull null], self.style ?: [NSNull null], ]; } @@ -1604,6 +1638,47 @@ + (nullable FGMPlatformBitmapBytesMap *)nullableFromList:(NSArray *)list { } @end +@implementation FGMPlatformBitmapPinConfig ++ (instancetype)makeWithBackgroundColor:(nullable NSNumber *)backgroundColor + borderColor:(nullable NSNumber *)borderColor + glyphColor:(nullable NSNumber *)glyphColor + glyphTextColor:(nullable NSNumber *)glyphTextColor + glyphText:(nullable NSString *)glyphText + glyphBitmap:(nullable FGMPlatformBitmap *)glyphBitmap { + FGMPlatformBitmapPinConfig *pigeonResult = [[FGMPlatformBitmapPinConfig alloc] init]; + pigeonResult.backgroundColor = backgroundColor; + pigeonResult.borderColor = borderColor; + pigeonResult.glyphColor = glyphColor; + pigeonResult.glyphTextColor = glyphTextColor; + pigeonResult.glyphText = glyphText; + pigeonResult.glyphBitmap = glyphBitmap; + return pigeonResult; +} ++ (FGMPlatformBitmapPinConfig *)fromList:(NSArray *)list { + FGMPlatformBitmapPinConfig *pigeonResult = [[FGMPlatformBitmapPinConfig alloc] init]; + pigeonResult.backgroundColor = GetNullableObjectAtIndex(list, 0); + pigeonResult.borderColor = GetNullableObjectAtIndex(list, 1); + pigeonResult.glyphColor = GetNullableObjectAtIndex(list, 2); + pigeonResult.glyphTextColor = GetNullableObjectAtIndex(list, 3); + pigeonResult.glyphText = GetNullableObjectAtIndex(list, 4); + pigeonResult.glyphBitmap = GetNullableObjectAtIndex(list, 5); + return pigeonResult; +} ++ (nullable FGMPlatformBitmapPinConfig *)nullableFromList:(NSArray *)list { + return (list) ? [FGMPlatformBitmapPinConfig fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.backgroundColor ?: [NSNull null], + self.borderColor ?: [NSNull null], + self.glyphColor ?: [NSNull null], + self.glyphTextColor ?: [NSNull null], + self.glyphText ?: [NSNull null], + self.glyphBitmap ?: [NSNull null], + ]; +} +@end + @interface FGMMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FGMMessagesPigeonCodecReader @@ -1616,101 +1691,115 @@ - (nullable id)readValueOfType:(UInt8)type { : [[FGMPlatformMapTypeBox alloc] initWithValue:[enumAsNumber integerValue]]; } case 130: { + NSNumber *enumAsNumber = [self readValue]; + return enumAsNumber == nil ? nil + : [[FGMPlatformMarkerCollisionBehaviorBox alloc] + initWithValue:[enumAsNumber integerValue]]; + } + case 131: { NSNumber *enumAsNumber = [self readValue]; return enumAsNumber == nil ? nil : [[FGMPlatformJointTypeBox alloc] initWithValue:[enumAsNumber integerValue]]; } - case 131: { + case 132: { NSNumber *enumAsNumber = [self readValue]; return enumAsNumber == nil ? nil : [[FGMPlatformPatternItemTypeBox alloc] initWithValue:[enumAsNumber integerValue]]; } - case 132: { + case 133: { + NSNumber *enumAsNumber = [self readValue]; + return enumAsNumber == nil + ? nil + : [[FGMPlatformMarkerTypeBox alloc] initWithValue:[enumAsNumber integerValue]]; + } + case 134: { NSNumber *enumAsNumber = [self readValue]; return enumAsNumber == nil ? nil : [[FGMPlatformMapBitmapScalingBox alloc] initWithValue:[enumAsNumber integerValue]]; } - case 133: + case 135: return [FGMPlatformCameraPosition fromList:[self readValue]]; - case 134: + case 136: return [FGMPlatformCameraUpdate fromList:[self readValue]]; - case 135: + case 137: return [FGMPlatformCameraUpdateNewCameraPosition fromList:[self readValue]]; - case 136: + case 138: return [FGMPlatformCameraUpdateNewLatLng fromList:[self readValue]]; - case 137: + case 139: return [FGMPlatformCameraUpdateNewLatLngBounds fromList:[self readValue]]; - case 138: + case 140: return [FGMPlatformCameraUpdateNewLatLngZoom fromList:[self readValue]]; - case 139: + case 141: return [FGMPlatformCameraUpdateScrollBy fromList:[self readValue]]; - case 140: + case 142: return [FGMPlatformCameraUpdateZoomBy fromList:[self readValue]]; - case 141: + case 143: return [FGMPlatformCameraUpdateZoom fromList:[self readValue]]; - case 142: + case 144: return [FGMPlatformCameraUpdateZoomTo fromList:[self readValue]]; - case 143: + case 145: return [FGMPlatformCircle fromList:[self readValue]]; - case 144: + case 146: return [FGMPlatformHeatmap fromList:[self readValue]]; - case 145: + case 147: return [FGMPlatformInfoWindow fromList:[self readValue]]; - case 146: + case 148: return [FGMPlatformCluster fromList:[self readValue]]; - case 147: + case 149: return [FGMPlatformClusterManager fromList:[self readValue]]; - case 148: + case 150: return [FGMPlatformMarker fromList:[self readValue]]; - case 149: + case 151: return [FGMPlatformPolygon fromList:[self readValue]]; - case 150: + case 152: return [FGMPlatformPolyline fromList:[self readValue]]; - case 151: + case 153: return [FGMPlatformPatternItem fromList:[self readValue]]; - case 152: + case 154: return [FGMPlatformTile fromList:[self readValue]]; - case 153: + case 155: return [FGMPlatformTileOverlay fromList:[self readValue]]; - case 154: + case 156: return [FGMPlatformEdgeInsets fromList:[self readValue]]; - case 155: + case 157: return [FGMPlatformLatLng fromList:[self readValue]]; - case 156: + case 158: return [FGMPlatformLatLngBounds fromList:[self readValue]]; - case 157: + case 159: return [FGMPlatformCameraTargetBounds fromList:[self readValue]]; - case 158: + case 160: return [FGMPlatformGroundOverlay fromList:[self readValue]]; - case 159: + case 161: return [FGMPlatformMapViewCreationParams fromList:[self readValue]]; - case 160: + case 162: return [FGMPlatformMapConfiguration fromList:[self readValue]]; - case 161: + case 163: return [FGMPlatformPoint fromList:[self readValue]]; - case 162: + case 164: return [FGMPlatformSize fromList:[self readValue]]; - case 163: + case 165: return [FGMPlatformTileLayer fromList:[self readValue]]; - case 164: + case 166: return [FGMPlatformZoomRange fromList:[self readValue]]; - case 165: + case 167: return [FGMPlatformBitmap fromList:[self readValue]]; - case 166: + case 168: return [FGMPlatformBitmapDefaultMarker fromList:[self readValue]]; - case 167: + case 169: return [FGMPlatformBitmapBytes fromList:[self readValue]]; - case 168: + case 170: return [FGMPlatformBitmapAsset fromList:[self readValue]]; - case 169: + case 171: return [FGMPlatformBitmapAssetImage fromList:[self readValue]]; - case 170: + case 172: return [FGMPlatformBitmapAssetMap fromList:[self readValue]]; - case 171: + case 173: return [FGMPlatformBitmapBytesMap fromList:[self readValue]]; + case 174: + return [FGMPlatformBitmapPinConfig fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -1725,134 +1814,145 @@ - (void)writeValue:(id)value { FGMPlatformMapTypeBox *box = (FGMPlatformMapTypeBox *)value; [self writeByte:129]; [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; + } else if ([value isKindOfClass:[FGMPlatformMarkerCollisionBehaviorBox class]]) { + FGMPlatformMarkerCollisionBehaviorBox *box = (FGMPlatformMarkerCollisionBehaviorBox *)value; + [self writeByte:130]; + [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; } else if ([value isKindOfClass:[FGMPlatformJointTypeBox class]]) { FGMPlatformJointTypeBox *box = (FGMPlatformJointTypeBox *)value; - [self writeByte:130]; + [self writeByte:131]; [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; } else if ([value isKindOfClass:[FGMPlatformPatternItemTypeBox class]]) { FGMPlatformPatternItemTypeBox *box = (FGMPlatformPatternItemTypeBox *)value; - [self writeByte:131]; + [self writeByte:132]; + [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; + } else if ([value isKindOfClass:[FGMPlatformMarkerTypeBox class]]) { + FGMPlatformMarkerTypeBox *box = (FGMPlatformMarkerTypeBox *)value; + [self writeByte:133]; [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; } else if ([value isKindOfClass:[FGMPlatformMapBitmapScalingBox class]]) { FGMPlatformMapBitmapScalingBox *box = (FGMPlatformMapBitmapScalingBox *)value; - [self writeByte:132]; + [self writeByte:134]; [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; } else if ([value isKindOfClass:[FGMPlatformCameraPosition class]]) { - [self writeByte:133]; + [self writeByte:135]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdate class]]) { - [self writeByte:134]; + [self writeByte:136]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateNewCameraPosition class]]) { - [self writeByte:135]; + [self writeByte:137]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateNewLatLng class]]) { - [self writeByte:136]; + [self writeByte:138]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateNewLatLngBounds class]]) { - [self writeByte:137]; + [self writeByte:139]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateNewLatLngZoom class]]) { - [self writeByte:138]; + [self writeByte:140]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateScrollBy class]]) { - [self writeByte:139]; + [self writeByte:141]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateZoomBy class]]) { - [self writeByte:140]; + [self writeByte:142]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateZoom class]]) { - [self writeByte:141]; + [self writeByte:143]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraUpdateZoomTo class]]) { - [self writeByte:142]; + [self writeByte:144]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCircle class]]) { - [self writeByte:143]; + [self writeByte:145]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformHeatmap class]]) { - [self writeByte:144]; + [self writeByte:146]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformInfoWindow class]]) { - [self writeByte:145]; + [self writeByte:147]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCluster class]]) { - [self writeByte:146]; + [self writeByte:148]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformClusterManager class]]) { - [self writeByte:147]; + [self writeByte:149]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformMarker class]]) { - [self writeByte:148]; + [self writeByte:150]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformPolygon class]]) { - [self writeByte:149]; + [self writeByte:151]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformPolyline class]]) { - [self writeByte:150]; + [self writeByte:152]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformPatternItem class]]) { - [self writeByte:151]; + [self writeByte:153]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformTile class]]) { - [self writeByte:152]; + [self writeByte:154]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformTileOverlay class]]) { - [self writeByte:153]; + [self writeByte:155]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformEdgeInsets class]]) { - [self writeByte:154]; + [self writeByte:156]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformLatLng class]]) { - [self writeByte:155]; + [self writeByte:157]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformLatLngBounds class]]) { - [self writeByte:156]; + [self writeByte:158]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformCameraTargetBounds class]]) { - [self writeByte:157]; + [self writeByte:159]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformGroundOverlay class]]) { - [self writeByte:158]; + [self writeByte:160]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformMapViewCreationParams class]]) { - [self writeByte:159]; + [self writeByte:161]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformMapConfiguration class]]) { - [self writeByte:160]; + [self writeByte:162]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformPoint class]]) { - [self writeByte:161]; + [self writeByte:163]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformSize class]]) { - [self writeByte:162]; + [self writeByte:164]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformTileLayer class]]) { - [self writeByte:163]; + [self writeByte:165]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformZoomRange class]]) { - [self writeByte:164]; + [self writeByte:166]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformBitmap class]]) { - [self writeByte:165]; + [self writeByte:167]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformBitmapDefaultMarker class]]) { - [self writeByte:166]; + [self writeByte:168]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformBitmapBytes class]]) { - [self writeByte:167]; + [self writeByte:169]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformBitmapAsset class]]) { - [self writeByte:168]; + [self writeByte:170]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformBitmapAssetImage class]]) { - [self writeByte:169]; + [self writeByte:171]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformBitmapAssetMap class]]) { - [self writeByte:170]; + [self writeByte:172]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FGMPlatformBitmapBytesMap class]]) { - [self writeByte:171]; + [self writeByte:173]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FGMPlatformBitmapPinConfig class]]) { + [self writeByte:174]; [self writeValue:[value toList]]; } else { [super writeValue:value]; @@ -2510,6 +2610,28 @@ void SetUpFGMMapsApiWithSuffix(id binaryMessenger, [channel setMessageHandler:nil]; } } + /// Returns true if the map supports advanced markers. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"MapsApi.isAdvancedMarkersAvailable", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(isAdvancedMarkersAvailable:)], + @"FGMMapsApi api (%@) doesn't respond to @selector(isAdvancedMarkersAvailable:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api isAdvancedMarkersAvailable:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } @interface FGMMapsCallbackApi () @property(nonatomic, strong) NSObject *binaryMessenger; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart index ab76471fc19..06975d98e72 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -483,6 +483,11 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { return _hostApi(mapId).getLastStyleError(); } + @override + Future isAdvancedMarkersAvailable({required int mapId}) { + return _hostApi(mapId).isAdvancedMarkersAvailable(); + } + Widget _buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -566,8 +571,9 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { creationId, onPlatformViewCreated, widgetConfiguration: MapWidgetConfiguration( - initialCameraPosition: initialCameraPosition, - textDirection: textDirection), + initialCameraPosition: initialCameraPosition, + textDirection: textDirection, + ), mapObjects: MapObjects( markers: markers, polygons: polygons, @@ -681,6 +687,10 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { zIndex: marker.zIndex, markerId: marker.markerId.value, clusterManagerId: marker.clusterManagerId?.value, + collisionBehavior: marker is AdvancedMarker + ? platformMarkerCollisionBehaviorFromMarkerCollisionBehavior( + marker.collisionBehavior) + : null, ); } @@ -836,6 +846,23 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { return PlatformMapBitmapScaling.auto; } + /// Converts [MarkersCollisionBehavior] from platform interface to + /// [PlatformMarkerCollisionBehavior] Pigeon. + @visibleForTesting + static PlatformMarkerCollisionBehavior + platformMarkerCollisionBehaviorFromMarkerCollisionBehavior( + MarkerCollisionBehavior collisionBehavior, + ) { + return switch (collisionBehavior) { + MarkerCollisionBehavior.requiredDisplay => + PlatformMarkerCollisionBehavior.requiredDisplay, + MarkerCollisionBehavior.optionalAndHidesLowerPriority => + PlatformMarkerCollisionBehavior.optionalAndHidesLowerPriority, + MarkerCollisionBehavior.requiredAndHidesOptional => + PlatformMarkerCollisionBehavior.requiredAndHidesOptional, + }; + } + /// Converts [BitmapDescriptor] from platform interface to [PlatformBitmap] pigeon. @visibleForTesting static PlatformBitmap platformBitmapFromBitmapDescriptor( @@ -882,6 +909,45 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { imagePixelRatio: bytes.imagePixelRatio, width: bytes.width, height: bytes.height)); + case final PinConfig pinConfig: + final int? backgroundColor = pinConfig.backgroundColor?.value; + final int? borderColor = pinConfig.borderColor?.value; + switch (pinConfig.glyph) { + case final CircleGlyph circleGlyph: + return PlatformBitmap( + bitmap: PlatformBitmapPinConfig( + backgroundColor: backgroundColor, + borderColor: borderColor, + glyphColor: circleGlyph.color.value, + ), + ); + case final TextGlyph textGlyph: + return PlatformBitmap( + bitmap: PlatformBitmapPinConfig( + backgroundColor: backgroundColor, + borderColor: borderColor, + glyphText: textGlyph.text, + glyphTextColor: textGlyph.textColor?.value, + ), + ); + case final BitmapGlyph bitmapGlyph: + return PlatformBitmap( + bitmap: PlatformBitmapPinConfig( + backgroundColor: backgroundColor, + borderColor: borderColor, + glyphBitmap: platformBitmapFromBitmapDescriptor( + bitmapGlyph.bitmap, + ), + ), + ); + case null: + return PlatformBitmap( + bitmap: PlatformBitmapPinConfig( + backgroundColor: backgroundColor, + borderColor: borderColor, + ), + ); + } default: throw ArgumentError( 'Unrecognized type of bitmap ${bitmap.runtimeType}', 'bitmap'); @@ -1124,11 +1190,20 @@ PlatformMapConfiguration _platformMapConfigurationFromMapConfiguration( indoorViewEnabled: config.indoorViewEnabled, trafficEnabled: config.trafficEnabled, buildingsEnabled: config.buildingsEnabled, - cloudMapId: config.cloudMapId, + markerType: _platformMarkerTypeFromMarkerType(config.markerType), + mapId: config.mapId, style: config.style, ); } +PlatformMarkerType? _platformMarkerTypeFromMarkerType(MarkerType? markerType) { + return switch (markerType) { + MarkerType.marker => PlatformMarkerType.marker, + MarkerType.advancedMarker => PlatformMarkerType.advancedMarker, + null => null, + }; +} + // For supporting the deprecated updateMapOptions API. PlatformMapConfiguration _platformMapConfigurationFromOptionsJson( Map options) { @@ -1162,7 +1237,8 @@ PlatformMapConfiguration _platformMapConfigurationFromOptionsJson( indoorViewEnabled: options['indoorEnabled'] as bool?, trafficEnabled: options['trafficEnabled'] as bool?, buildingsEnabled: options['buildingsEnabled'] as bool?, - cloudMapId: options['cloudMapId'] as String?, + markerType: PlatformMarkerType.marker, + mapId: options['mapId'] as String?, style: options['style'] as String?, ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart index c7b67e500c9..10b18e0a0c9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.7.4), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -38,6 +38,12 @@ enum PlatformMapType { hybrid, } +enum PlatformMarkerCollisionBehavior { + requiredDisplay, + optionalAndHidesLowerPriority, + requiredAndHidesOptional, +} + /// Join types for polyline joints. enum PlatformJointType { mitered, @@ -52,6 +58,11 @@ enum PlatformPatternItemType { gap, } +enum PlatformMarkerType { + marker, + advancedMarker, +} + /// Pigeon equivalent of [MapBitmapScaling]. enum PlatformMapBitmapScaling { auto, @@ -511,6 +522,7 @@ class PlatformMarker { this.zIndex = 0.0, required this.markerId, this.clusterManagerId, + this.collisionBehavior, }); double alpha; @@ -539,6 +551,8 @@ class PlatformMarker { String? clusterManagerId; + PlatformMarkerCollisionBehavior? collisionBehavior; + Object encode() { return [ alpha, @@ -554,6 +568,7 @@ class PlatformMarker { zIndex, markerId, clusterManagerId, + collisionBehavior, ]; } @@ -573,6 +588,7 @@ class PlatformMarker { zIndex: result[10]! as double, markerId: result[11]! as String, clusterManagerId: result[12] as String?, + collisionBehavior: result[13] as PlatformMarkerCollisionBehavior?, ); } } @@ -1095,7 +1111,8 @@ class PlatformMapConfiguration { this.indoorViewEnabled, this.trafficEnabled, this.buildingsEnabled, - this.cloudMapId, + this.markerType, + this.mapId, this.style, }); @@ -1129,7 +1146,9 @@ class PlatformMapConfiguration { bool? buildingsEnabled; - String? cloudMapId; + PlatformMarkerType? markerType; + + String? mapId; String? style; @@ -1150,7 +1169,8 @@ class PlatformMapConfiguration { indoorViewEnabled, trafficEnabled, buildingsEnabled, - cloudMapId, + markerType, + mapId, style, ]; } @@ -1173,8 +1193,9 @@ class PlatformMapConfiguration { indoorViewEnabled: result[12] as bool?, trafficEnabled: result[13] as bool?, buildingsEnabled: result[14] as bool?, - cloudMapId: result[15] as String?, - style: result[16] as String?, + markerType: result[15] as PlatformMarkerType?, + mapId: result[16] as String?, + style: result[17] as String?, ); } } @@ -1520,6 +1541,53 @@ class PlatformBitmapBytesMap { } } +/// Pigeon equivalent of [PinConfig]. +class PlatformBitmapPinConfig { + PlatformBitmapPinConfig({ + this.backgroundColor, + this.borderColor, + this.glyphColor, + this.glyphTextColor, + this.glyphText, + this.glyphBitmap, + }); + + int? backgroundColor; + + int? borderColor; + + int? glyphColor; + + int? glyphTextColor; + + String? glyphText; + + PlatformBitmap? glyphBitmap; + + Object encode() { + return [ + backgroundColor, + borderColor, + glyphColor, + glyphTextColor, + glyphText, + glyphBitmap, + ]; + } + + 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?, + glyphTextColor: result[3] as int?, + glyphText: result[4] as String?, + glyphBitmap: result[5] as PlatformBitmap?, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -1530,131 +1598,140 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformMapType) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformJointType) { + } else if (value is PlatformMarkerCollisionBehavior) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is PlatformPatternItemType) { + } else if (value is PlatformJointType) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is PlatformMapBitmapScaling) { + } else if (value is PlatformPatternItemType) { buffer.putUint8(132); writeValue(buffer, value.index); - } else if (value is PlatformCameraPosition) { + } else if (value is PlatformMarkerType) { buffer.putUint8(133); + writeValue(buffer, value.index); + } else if (value is PlatformMapBitmapScaling) { + buffer.putUint8(134); + writeValue(buffer, value.index); + } else if (value is PlatformCameraPosition) { + buffer.putUint8(135); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdate) { - buffer.putUint8(134); + buffer.putUint8(136); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewCameraPosition) { - buffer.putUint8(135); + buffer.putUint8(137); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewLatLng) { - buffer.putUint8(136); + buffer.putUint8(138); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewLatLngBounds) { - buffer.putUint8(137); + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateNewLatLngZoom) { - buffer.putUint8(138); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateScrollBy) { - buffer.putUint8(139); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateZoomBy) { - buffer.putUint8(140); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateZoom) { - buffer.putUint8(141); + buffer.putUint8(143); writeValue(buffer, value.encode()); } else if (value is PlatformCameraUpdateZoomTo) { - buffer.putUint8(142); + buffer.putUint8(144); writeValue(buffer, value.encode()); } else if (value is PlatformCircle) { - buffer.putUint8(143); + buffer.putUint8(145); writeValue(buffer, value.encode()); } else if (value is PlatformHeatmap) { - buffer.putUint8(144); + buffer.putUint8(146); writeValue(buffer, value.encode()); } else if (value is PlatformInfoWindow) { - buffer.putUint8(145); + buffer.putUint8(147); writeValue(buffer, value.encode()); } else if (value is PlatformCluster) { - 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 PlatformMarker) { - buffer.putUint8(148); + buffer.putUint8(150); writeValue(buffer, value.encode()); } else if (value is PlatformPolygon) { - buffer.putUint8(149); + buffer.putUint8(151); writeValue(buffer, value.encode()); } else if (value is PlatformPolyline) { - buffer.putUint8(150); + buffer.putUint8(152); writeValue(buffer, value.encode()); } else if (value is PlatformPatternItem) { - buffer.putUint8(151); + buffer.putUint8(153); writeValue(buffer, value.encode()); } else if (value is PlatformTile) { - buffer.putUint8(152); + buffer.putUint8(154); writeValue(buffer, value.encode()); } else if (value is PlatformTileOverlay) { - buffer.putUint8(153); + buffer.putUint8(155); writeValue(buffer, value.encode()); } else if (value is PlatformEdgeInsets) { - buffer.putUint8(154); + buffer.putUint8(156); writeValue(buffer, value.encode()); } else if (value is PlatformLatLng) { - buffer.putUint8(155); + buffer.putUint8(157); writeValue(buffer, value.encode()); } else if (value is PlatformLatLngBounds) { - buffer.putUint8(156); + buffer.putUint8(158); writeValue(buffer, value.encode()); } else if (value is PlatformCameraTargetBounds) { - buffer.putUint8(157); + buffer.putUint8(159); writeValue(buffer, value.encode()); } else if (value is PlatformGroundOverlay) { - buffer.putUint8(158); + buffer.putUint8(160); writeValue(buffer, value.encode()); } else if (value is PlatformMapViewCreationParams) { - buffer.putUint8(159); + buffer.putUint8(161); writeValue(buffer, value.encode()); } else if (value is PlatformMapConfiguration) { - buffer.putUint8(160); + buffer.putUint8(162); writeValue(buffer, value.encode()); } else if (value is PlatformPoint) { - buffer.putUint8(161); + buffer.putUint8(163); writeValue(buffer, value.encode()); } else if (value is PlatformSize) { - buffer.putUint8(162); + buffer.putUint8(164); writeValue(buffer, value.encode()); } else if (value is PlatformTileLayer) { - buffer.putUint8(163); + buffer.putUint8(165); writeValue(buffer, value.encode()); } else if (value is PlatformZoomRange) { - buffer.putUint8(164); + buffer.putUint8(166); writeValue(buffer, value.encode()); } else if (value is PlatformBitmap) { - buffer.putUint8(165); + buffer.putUint8(167); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapDefaultMarker) { - buffer.putUint8(166); + buffer.putUint8(168); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapBytes) { - buffer.putUint8(167); + buffer.putUint8(169); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapAsset) { - buffer.putUint8(168); + buffer.putUint8(170); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapAssetImage) { - buffer.putUint8(169); + buffer.putUint8(171); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapAssetMap) { - buffer.putUint8(170); + buffer.putUint8(172); writeValue(buffer, value.encode()); } else if (value is PlatformBitmapBytesMap) { - buffer.putUint8(171); + buffer.putUint8(173); + writeValue(buffer, value.encode()); + } else if (value is PlatformBitmapPinConfig) { + buffer.putUint8(174); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -1669,91 +1746,101 @@ class _PigeonCodec extends StandardMessageCodec { return value == null ? null : PlatformMapType.values[value]; case 130: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformJointType.values[value]; + return value == null + ? null + : PlatformMarkerCollisionBehavior.values[value]; case 131: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformPatternItemType.values[value]; + return value == null ? null : PlatformJointType.values[value]; case 132: final int? value = readValue(buffer) as int?; - return value == null ? null : PlatformMapBitmapScaling.values[value]; + return value == null ? null : PlatformPatternItemType.values[value]; case 133: - return PlatformCameraPosition.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformMarkerType.values[value]; case 134: - return PlatformCameraUpdate.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformMapBitmapScaling.values[value]; case 135: - return PlatformCameraUpdateNewCameraPosition.decode(readValue(buffer)!); + return PlatformCameraPosition.decode(readValue(buffer)!); case 136: - return PlatformCameraUpdateNewLatLng.decode(readValue(buffer)!); + return PlatformCameraUpdate.decode(readValue(buffer)!); case 137: - return PlatformCameraUpdateNewLatLngBounds.decode(readValue(buffer)!); + return PlatformCameraUpdateNewCameraPosition.decode(readValue(buffer)!); case 138: - return PlatformCameraUpdateNewLatLngZoom.decode(readValue(buffer)!); + return PlatformCameraUpdateNewLatLng.decode(readValue(buffer)!); case 139: - return PlatformCameraUpdateScrollBy.decode(readValue(buffer)!); + return PlatformCameraUpdateNewLatLngBounds.decode(readValue(buffer)!); case 140: - return PlatformCameraUpdateZoomBy.decode(readValue(buffer)!); + return PlatformCameraUpdateNewLatLngZoom.decode(readValue(buffer)!); case 141: - return PlatformCameraUpdateZoom.decode(readValue(buffer)!); + return PlatformCameraUpdateScrollBy.decode(readValue(buffer)!); case 142: - return PlatformCameraUpdateZoomTo.decode(readValue(buffer)!); + return PlatformCameraUpdateZoomBy.decode(readValue(buffer)!); case 143: - return PlatformCircle.decode(readValue(buffer)!); + return PlatformCameraUpdateZoom.decode(readValue(buffer)!); case 144: - return PlatformHeatmap.decode(readValue(buffer)!); + return PlatformCameraUpdateZoomTo.decode(readValue(buffer)!); case 145: - return PlatformInfoWindow.decode(readValue(buffer)!); + return PlatformCircle.decode(readValue(buffer)!); case 146: - return PlatformCluster.decode(readValue(buffer)!); + return PlatformHeatmap.decode(readValue(buffer)!); case 147: - return PlatformClusterManager.decode(readValue(buffer)!); + return PlatformInfoWindow.decode(readValue(buffer)!); case 148: - return PlatformMarker.decode(readValue(buffer)!); + return PlatformCluster.decode(readValue(buffer)!); case 149: - return PlatformPolygon.decode(readValue(buffer)!); + return PlatformClusterManager.decode(readValue(buffer)!); case 150: - return PlatformPolyline.decode(readValue(buffer)!); + return PlatformMarker.decode(readValue(buffer)!); case 151: - return PlatformPatternItem.decode(readValue(buffer)!); + return PlatformPolygon.decode(readValue(buffer)!); case 152: - return PlatformTile.decode(readValue(buffer)!); + return PlatformPolyline.decode(readValue(buffer)!); case 153: - return PlatformTileOverlay.decode(readValue(buffer)!); + return PlatformPatternItem.decode(readValue(buffer)!); case 154: - return PlatformEdgeInsets.decode(readValue(buffer)!); + return PlatformTile.decode(readValue(buffer)!); case 155: - return PlatformLatLng.decode(readValue(buffer)!); + return PlatformTileOverlay.decode(readValue(buffer)!); case 156: - return PlatformLatLngBounds.decode(readValue(buffer)!); + return PlatformEdgeInsets.decode(readValue(buffer)!); case 157: - return PlatformCameraTargetBounds.decode(readValue(buffer)!); + return PlatformLatLng.decode(readValue(buffer)!); case 158: - return PlatformGroundOverlay.decode(readValue(buffer)!); + return PlatformLatLngBounds.decode(readValue(buffer)!); case 159: - return PlatformMapViewCreationParams.decode(readValue(buffer)!); + return PlatformCameraTargetBounds.decode(readValue(buffer)!); case 160: - return PlatformMapConfiguration.decode(readValue(buffer)!); + return PlatformGroundOverlay.decode(readValue(buffer)!); case 161: - return PlatformPoint.decode(readValue(buffer)!); + return PlatformMapViewCreationParams.decode(readValue(buffer)!); case 162: - return PlatformSize.decode(readValue(buffer)!); + return PlatformMapConfiguration.decode(readValue(buffer)!); case 163: - return PlatformTileLayer.decode(readValue(buffer)!); + return PlatformPoint.decode(readValue(buffer)!); case 164: - return PlatformZoomRange.decode(readValue(buffer)!); + return PlatformSize.decode(readValue(buffer)!); case 165: - return PlatformBitmap.decode(readValue(buffer)!); + return PlatformTileLayer.decode(readValue(buffer)!); case 166: - return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); + return PlatformZoomRange.decode(readValue(buffer)!); case 167: - return PlatformBitmapBytes.decode(readValue(buffer)!); + return PlatformBitmap.decode(readValue(buffer)!); case 168: - return PlatformBitmapAsset.decode(readValue(buffer)!); + return PlatformBitmapDefaultMarker.decode(readValue(buffer)!); case 169: - return PlatformBitmapAssetImage.decode(readValue(buffer)!); + return PlatformBitmapBytes.decode(readValue(buffer)!); case 170: - return PlatformBitmapAssetMap.decode(readValue(buffer)!); + return PlatformBitmapAsset.decode(readValue(buffer)!); case 171: + return PlatformBitmapAssetImage.decode(readValue(buffer)!); + case 172: + return PlatformBitmapAssetMap.decode(readValue(buffer)!); + case 173: return PlatformBitmapBytesMap.decode(readValue(buffer)!); + case 174: + return PlatformBitmapPinConfig.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -2400,6 +2487,36 @@ class MapsApi { return (pigeonVar_replyList[0] as Uint8List?); } } + + /// Returns true if the map supports advanced markers. + Future isAdvancedMarkersAvailable() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.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?)!; + } + } } /// Interface for calls from the native SDK to Dart. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart b/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart index dcbe6691bd1..d4a43def614 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart @@ -189,6 +189,7 @@ class PlatformMarker { this.visible = true, this.zIndex = 0.0, this.clusterManagerId, + this.collisionBehavior, }); final double alpha; @@ -205,6 +206,13 @@ 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. @@ -413,6 +421,11 @@ class PlatformMapViewCreationParams { final List initialGroundOverlays; } +enum PlatformMarkerType { + marker, + advancedMarker, +} + /// Pigeon equivalent of MapConfiguration. class PlatformMapConfiguration { PlatformMapConfiguration({ @@ -431,7 +444,8 @@ class PlatformMapConfiguration { required this.indoorViewEnabled, required this.trafficEnabled, required this.buildingsEnabled, - required this.cloudMapId, + required this.markerType, + required this.mapId, required this.style, }); @@ -450,7 +464,8 @@ class PlatformMapConfiguration { final bool? indoorViewEnabled; final bool? trafficEnabled; final bool? buildingsEnabled; - final String? cloudMapId; + final PlatformMarkerType? markerType; + final String? mapId; final String? style; } @@ -577,6 +592,26 @@ enum PlatformMapBitmapScaling { none, } +/// Pigeon equivalent of [PinConfig]. +class PlatformBitmapPinConfig { + PlatformBitmapPinConfig({ + required this.backgroundColor, + required this.borderColor, + required this.glyphColor, + required this.glyphTextColor, + required this.glyphText, + required this.glyphBitmap, + }); + + final int? backgroundColor; + final int? borderColor; + + final int? glyphColor; + final int? glyphTextColor; + final String? glyphText; + final PlatformBitmap? glyphBitmap; +} + /// Interface for non-test interactions with the native SDK. /// /// For test-only state queries, see [MapsInspectorApi]. @@ -694,6 +729,10 @@ abstract class MapsApi { /// Takes a snapshot of the map and returns its image data. Uint8List? takeSnapshot(); + + /// Returns true if the map supports advanced markers. + @ObjCSelector('isAdvancedMarkersAvailable') + bool isAdvancedMarkersAvailable(); } /// Interface for calls from the native SDK to Dart. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml index 08ef36c1402..cb2d59505a8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -35,3 +35,8 @@ topics: - google-maps - google-maps-flutter - map + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_platform_interface: {path: ../../google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart index ba920814a7e..32a7bc60815 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart @@ -10,6 +10,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_ios/google_maps_flutter_ios.dart'; import 'package:google_maps_flutter_ios/src/messages.g.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/advanced_marker.dart' + as advanced_marker; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -479,6 +481,106 @@ void main() { } }); + test('updateMarkers passes expected arguments (AdvancedMarkers)', () async { + const int mapId = 1; + final (GoogleMapsFlutterIOS 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: advanced_marker + .MarkerCollisionBehavior.optionalAndHidesLowerPriority); + final AdvancedMarker object3 = AdvancedMarker( + markerId: const MarkerId('3'), + collisionBehavior: + advanced_marker.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, + GoogleMapsFlutterIOS.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, + GoogleMapsFlutterIOS + .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, + GoogleMapsFlutterIOS.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, + GoogleMapsFlutterIOS + .platformMarkerCollisionBehaviorFromMarkerCollisionBehavior( + object3.collisionBehavior)); + } + }); + test('updatePolygons passes expected arguments', () async { const int mapId = 1; final (GoogleMapsFlutterIOS maps, MockMapsApi api) = @@ -1169,8 +1271,7 @@ void main() { MapsApi.pigeonChannelCodec.decodeMessage(byteData) as PlatformMapViewCreationParams?; if (creationParams != null) { - final String? passedMapId = - creationParams.mapConfiguration.cloudMapId; + final String? passedMapId = creationParams.mapConfiguration.mapId; if (passedMapId != null) { passedCloudMapIdCompleter.complete(passedMapId); } @@ -1187,10 +1288,11 @@ void main() { textDirection: TextDirection.ltr, child: maps.buildViewWithConfiguration(1, (int id) {}, widgetConfiguration: const MapWidgetConfiguration( - initialCameraPosition: - CameraPosition(target: LatLng(0, 0), zoom: 1), - textDirection: TextDirection.ltr), - mapConfiguration: const MapConfiguration(cloudMapId: cloudMapId)))); + initialCameraPosition: + CameraPosition(target: LatLng(0, 0), zoom: 1), + textDirection: TextDirection.ltr, + ), + mapConfiguration: const MapConfiguration(mapId: cloudMapId)))); expect( await passedCloudMapIdCompleter.future, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index f81473aaad3..b5e6457909c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -430,6 +430,11 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { return null; } + /// Returns true if [AdvancedMarker]s can be used with this map. + Future isAdvancedMarkersAvailable({required int mapId}) async { + return false; + } + /// Returns a widget displaying the map view. @Deprecated('Use buildViewWithConfiguration instead.') Widget buildView( diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/advanced_marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/advanced_marker.dart new file mode 100644 index 00000000000..3877e928445 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/advanced_marker.dart @@ -0,0 +1,171 @@ +// 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:ui' show Offset; +import 'package:flutter/foundation.dart'; + +import '../../google_maps_flutter_platform_interface.dart'; + +/// Marks a geographical location on the map. +/// +/// Extends [Marker] and provides additional features. +@immutable +class AdvancedMarker extends Marker { + /// Creates a set of marker configuration options. + /// + /// Specifies a marker with a given [collisionBehavior]. Default is + /// [MarkerCollisionBehavior.required]. + AdvancedMarker({ + required super.markerId, + super.alpha, + super.anchor, + super.consumeTapEvents, + super.draggable, + super.flat, + super.icon, + super.infoWindow, + super.position, + super.rotation, + super.visible, + super.clusterManagerId, + super.onTap, + super.onDrag, + super.onDragStart, + super.onDragEnd, + int zIndex = 0, + this.collisionBehavior = MarkerCollisionBehavior.requiredDisplay, + }) : super(zIndex: zIndex.toDouble()); + + /// Indicates how the marker behaves when it collides with other markers. + final MarkerCollisionBehavior collisionBehavior; + + /// Creates a new [AdvancedMarker] object whose values are the same as this + /// instance, unless overwritten by the specified parameters. + @override + AdvancedMarker copyWith({ + double? alphaParam, + Offset? anchorParam, + bool? consumeTapEventsParam, + bool? draggableParam, + bool? flatParam, + BitmapDescriptor? iconParam, + InfoWindow? infoWindowParam, + LatLng? positionParam, + double? rotationParam, + bool? visibleParam, + double? zIndexParam, + VoidCallback? onTapParam, + ValueChanged? onDragStartParam, + ValueChanged? onDragParam, + ValueChanged? onDragEndParam, + ClusterManagerId? clusterManagerIdParam, + MarkerCollisionBehavior? collisionBehaviorParam, + double? altitudeParam, + }) { + return AdvancedMarker( + markerId: markerId, + alpha: alphaParam ?? alpha, + anchor: anchorParam ?? anchor, + consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents, + draggable: draggableParam ?? draggable, + flat: flatParam ?? flat, + icon: iconParam ?? icon, + infoWindow: infoWindowParam ?? infoWindow, + position: positionParam ?? position, + rotation: rotationParam ?? rotation, + visible: visibleParam ?? visible, + zIndex: (zIndexParam ?? zIndex).toInt(), + onTap: onTapParam ?? onTap, + onDragStart: onDragStartParam ?? onDragStart, + onDrag: onDragParam ?? onDrag, + onDragEnd: onDragEndParam ?? onDragEnd, + clusterManagerId: clusterManagerIdParam ?? clusterManagerId, + collisionBehavior: collisionBehaviorParam ?? collisionBehavior, + ); + } + + /// Converts this object to something serializable in JSON. + @override + Object toJson() { + final String? clusterManagerIdValue = clusterManagerId?.value; + + return { + 'markerId': markerId.value, + 'alpha': alpha, + 'consumeTapEvents': consumeTapEvents, + 'draggable': draggable, + 'flat': flat, + 'icon': icon.toJson(), + 'infoWindow': infoWindow.toJson(), + 'position': position.toJson(), + 'rotation': rotation, + 'visible': visible, + 'zIndex': zIndex, + 'collisionBehavior': collisionBehavior.index, + 'anchor': _offsetToJson(anchor), + if (clusterManagerIdValue != null) + 'clusterManagerId': clusterManagerIdValue, + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is AdvancedMarker && + markerId == other.markerId && + alpha == other.alpha && + anchor == other.anchor && + consumeTapEvents == other.consumeTapEvents && + draggable == other.draggable && + flat == other.flat && + icon == other.icon && + infoWindow == other.infoWindow && + position == other.position && + rotation == other.rotation && + visible == other.visible && + zIndex == other.zIndex && + clusterManagerId == other.clusterManagerId && + collisionBehavior == other.collisionBehavior; + } + + @override + int get hashCode => markerId.hashCode; + + @override + String toString() { + return 'AdvancedMarker{markerId: $markerId, alpha: $alpha, anchor: $anchor, ' + 'consumeTapEvents: $consumeTapEvents, draggable: $draggable, flat: $flat, ' + 'icon: $icon, infoWindow: $infoWindow, position: $position, rotation: $rotation, ' + 'visible: $visible, zIndex: $zIndex, onTap: $onTap, onDragStart: $onDragStart, ' + 'onDrag: $onDrag, onDragEnd: $onDragEnd, clusterManagerId: $clusterManagerId, ' + 'collisionBehavior: $collisionBehavior}'; + } +} + +/// Indicates how the marker behaves when it collides with other markers. +enum MarkerCollisionBehavior { + /// (default) Always display the marker regardless of collision. + requiredDisplay, + + /// Display the marker only if it does not overlap with other markers. + /// If two markers of this type would overlap, the one with the higher zIndex + /// is shown. If they have the same zIndex, the one with the lower vertical + /// screen position is shown. + optionalAndHidesLowerPriority, + + /// Always display the marker regardless of collision, and hide any + /// [optionalAndHidesLowerPriority] markers or labels that would overlap with + /// the marker. + requiredAndHidesOptional, +} + +/// Convert [Offset] to JSON object. +Object _offsetToJson(Offset offset) { + return [offset.dx, offset.dy]; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart index e25d3c7a2c5..0598b2c570f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart @@ -4,6 +4,7 @@ import 'dart:async' show Future; import 'dart:typed_data' show Uint8List; +import 'dart:ui'; import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; import 'package:flutter/material.dart' @@ -52,6 +53,8 @@ const double _naturalPixelRatio = 1.0; /// a default marker icon. /// Use the [BitmapDescriptor.defaultMarkerWithHue] to create a /// [BitmapDescriptor] for a default marker icon with a hue value. +/// Use the [BitmapDescriptor.pinConfig] to create a custom icon for +/// [AdvancedMarker]. abstract class BitmapDescriptor { const BitmapDescriptor._(); @@ -208,6 +211,9 @@ abstract class BitmapDescriptor { /// Creates a BitmapDescriptor that refers to a colorization of the default /// marker image. For convenience, there is a predefined set of hue values. /// See e.g. [hueYellow]. + /// + /// Doesn't work with [AdvancedMarker]s, [BitmapDescriptor.pinConfig] should + /// be used instead. static BitmapDescriptor defaultMarkerWithHue(double hue) { assert(0.0 <= hue && hue < 360.0); return DefaultMarker(hue: hue); @@ -330,6 +336,28 @@ abstract class BitmapDescriptor { ); } + /// Creates a [BitmapDescriptor] that can be used to customize + /// [AdvancedMarker]'s pin. + /// + /// [backgroundColor] is the color of the pin's background. + /// [borderColor] is the color of the pin's border. + /// [glyph] is the pin's glyph to be displayed on the pin. + /// + /// See [PinConfig] for more information on the parameters. + /// + /// Returns a new [PinConfig] instance. + static BitmapDescriptor pinConfig({ + Color? backgroundColor, + Color? borderColor, + AdvancedMarkerGlyph? glyph, + }) { + return PinConfig( + backgroundColor: backgroundColor, + borderColor: borderColor, + glyph: glyph, + ); + } + /// Convert the object to a Json format. Object toJson(); } @@ -954,3 +982,173 @@ class BytesMapBitmap extends MapBitmap { } ]; } + +/// Represents a [BitmapDescriptor] that is created from a pin configuration. +/// Can only be used with [AdvancedMarker]s. +/// +/// The [backgroundColor] and [borderColor] are used to define the color of the +/// standard pin marker. +/// +/// The [glyph] parameter is used to define the glyph that is displayed on the +/// pin marker (default glyph is a circle). +/// +/// The following example demonstrates how to change colors of the default map +/// pin to white and blue: +/// +/// ```dart +/// PinConfig( +/// backgroundColor: Colors.blue, +/// borderColor: Colors.white, +/// glyph: CircleGlyph(color: Colors.blue) +/// ) +/// ``` +/// +/// The following example demonstrates how to customize a marker pin by showing +/// a short text on the pin: +/// +/// ```dart +/// PinConfig( +/// backgroundColor: Colors.blue, +/// glyph: TextGlyph(text: 'Pin', textColor: Colors.white) +/// ) +/// ``` +/// +/// The following example demonstrates how to customize a marker pin by showing +/// a custom image on the pin: +/// +/// ```dart +/// PinConfig( +/// glyph: BitmapGlyph( +/// bitmap: BitmapDescriptor.asset( +/// ImageConfiguration(size: Size(12, 12)), +/// 'assets/cat.png' +/// ) +/// ) +/// ``` +/// +class PinConfig extends BitmapDescriptor { + /// Constructs a [PinConfig] that is created from a pin configuration. + /// + /// The [backgroundColor] and [borderColor] are used to define the color of + /// the standard pin marker. + /// + /// The [glyph] parameter is used to define the glyph that is displayed on the + /// pin marker. + /// + /// At least one of the parameters must not be null. + const PinConfig({ + this.backgroundColor, + this.borderColor, + this.glyph, + }) : assert( + backgroundColor != null || borderColor != null || glyph != null, + 'Cannot create PinConfig with all parameters being null.', + ), + super._(); + + /// The type of the MapBitmap object, used for the JSON serialization. + static const String type = 'pinConfig'; + + /// The background color of the pin. + final Color? backgroundColor; + + /// The border color of the pin. + final Color? borderColor; + + /// The glyph that is displayed on the pin marker. If null, the default + /// circular glyph is used. + /// + /// Can be one of the following: + /// * [CircleGlyph] to define a circular glyph with a custom color. + /// * [BitmapGlyph] to define a glyph with a specified bitmap. + /// * [TextGlyph] to define a glyph with a specified text and its color. + final AdvancedMarkerGlyph? glyph; + + @override + Object toJson() => [ + type, + { + if (backgroundColor != null) + 'backgroundColor': backgroundColor?.value, + if (borderColor != null) 'borderColor': borderColor?.value, + if (glyph != null) 'glyph': glyph?.toJson(), + } + ]; +} + +/// Defines a glyph (the element at the center of an [AdvancedMarker] icon). +sealed class AdvancedMarkerGlyph extends BitmapDescriptor { + const AdvancedMarkerGlyph._() : super._(); +} + +/// Defines a circular glyph with a given color. +class CircleGlyph extends AdvancedMarkerGlyph { + /// Constructs a glyph instance, using the default circle, but with + /// a custom color. + const CircleGlyph({ + required this.color, + }) : super._(); + + /// Color of the circular icon. + final Color color; + + @override + Object toJson() => [ + 'circleGlyph', + { + 'color': color.value, + } + ]; +} + +/// Defines a glyph instance with a specified bitmap. +class BitmapGlyph extends AdvancedMarkerGlyph { + /// Constructs a glyph with the specified [bitmap]. + /// + /// [bitmap] is the image to be displayed in the center of the glyph. Must not + /// be an [AdvancedMarkerGlyph]. + const BitmapGlyph({ + required this.bitmap, + }) : assert( + bitmap is! AdvancedMarkerGlyph, + 'BitmapDescriptor cannot be an AdvancedMarkerGlyph.', + ), + super._(); + + /// Bitmap image to be displayed in the center of the glyph. + final BitmapDescriptor bitmap; + + @override + Object toJson() => [ + 'bitmapGlyph', + { + 'bitmap': bitmap.toJson(), + } + ]; +} + +/// Defines a glyph instance with a specified text and color. +class TextGlyph extends AdvancedMarkerGlyph { + /// Constructs a glyph with the specified [text] and [textColor]. + const TextGlyph({ + required this.text, + this.textColor, + }) : super._(); + + /// Text to be displayed in the glyph. + final String text; + + /// Color of the text. + final Color? textColor; + + @override + Object toJson() { + return [ + 'textGlyph', + { + 'text': text, + if (textColor != null) 'textColor': textColor!.value, + } + ]; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart index 6abd6c641e8..5162cd50813 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/map_configuration.dart @@ -35,9 +35,12 @@ class MapConfiguration { this.indoorViewEnabled, this.trafficEnabled, this.buildingsEnabled, - this.cloudMapId, + String? mapId, + @Deprecated('cloudMapId is deprecated. Use mapId instead.') + String? cloudMapId, this.style, - }); + this.markerType, + }) : mapId = mapId ?? cloudMapId; /// This setting controls how the API handles gestures on the map. Web only. /// @@ -112,13 +115,29 @@ class MapConfiguration { /// /// See https://developers.google.com/maps/documentation/get-map-id /// for more details. - final String? cloudMapId; + final String? mapId; /// Locally configured JSON style. /// /// To clear a previously set style, set this to an empty string. final String? style; + /// The type of marker that the map should use. + /// + /// Advanced and legacy markers could be handled differently by platform + /// implementations. This property indicates which type of marker should be + /// used. + final MarkerType? markerType; + + /// Identifier that's associated with a specific cloud-based map style. + /// + /// See https://developers.google.com/maps/documentation/get-map-id + /// for more details. + /// + /// Deprecated in favor of [mapId]. + @Deprecated('cloudMapId is deprecated. Use mapId instead.') + String? get cloudMapId => mapId; + /// Returns a new options object containing only the values of this instance /// that are different from [other]. MapConfiguration diffFrom(MapConfiguration other) { @@ -179,8 +198,9 @@ class MapConfiguration { trafficEnabled != other.trafficEnabled ? trafficEnabled : null, buildingsEnabled: buildingsEnabled != other.buildingsEnabled ? buildingsEnabled : null, - cloudMapId: cloudMapId != other.cloudMapId ? cloudMapId : null, + mapId: mapId != other.mapId ? mapId : null, style: style != other.style ? style : null, + markerType: markerType != other.markerType ? markerType : null, ); } @@ -212,8 +232,9 @@ class MapConfiguration { indoorViewEnabled: diff.indoorViewEnabled ?? indoorViewEnabled, trafficEnabled: diff.trafficEnabled ?? trafficEnabled, buildingsEnabled: diff.buildingsEnabled ?? buildingsEnabled, - cloudMapId: diff.cloudMapId ?? cloudMapId, + mapId: diff.mapId ?? mapId, style: diff.style ?? style, + markerType: diff.markerType ?? markerType, ); } @@ -239,8 +260,9 @@ class MapConfiguration { indoorViewEnabled == null && trafficEnabled == null && buildingsEnabled == null && - cloudMapId == null && - style == null; + mapId == null && + style == null && + markerType == null; @override bool operator ==(Object other) { @@ -271,8 +293,9 @@ class MapConfiguration { indoorViewEnabled == other.indoorViewEnabled && trafficEnabled == other.trafficEnabled && buildingsEnabled == other.buildingsEnabled && - cloudMapId == other.cloudMapId && - style == other.style; + mapId == other.mapId && + style == other.style && + markerType == other.markerType; } @override @@ -297,7 +320,18 @@ class MapConfiguration { indoorViewEnabled, trafficEnabled, buildingsEnabled, - cloudMapId, + mapId, style, + markerType, ]); } + +/// Indicates the type of marker that the map should use. +enum MarkerType { + /// Represents the default marker type, [Marker]. This marker type is + /// deprecated on the web. + marker, + + /// Represents the advanced marker type, [AdvancedMarker]. + advancedMarker, +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart index 0921d2d4ec5..76f35c05fdb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart @@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart' import 'types.dart'; +/// Convert [Offset] to JSON object. Object _offsetToJson(Offset offset) { return [offset.dx, offset.dy]; } @@ -64,7 +65,8 @@ class InfoWindow { ); } - Object _toJson() { + /// Convert this object to something serializable in JSON. + Object toJson() { final Map json = {}; void addIfPresent(String fieldName, Object? value) { @@ -117,6 +119,8 @@ class MarkerId extends MapsObjectId { /// A marker icon is drawn oriented against the device's screen rather than /// the map's surface; that is, it will not necessarily change orientation /// due to map rotations, tilting, or zooming. +/// +/// Deprecated on the web in favor of [AdvancedMarker]. @immutable class Marker implements MapsObject { /// Creates a set of marker configuration options. @@ -296,7 +300,7 @@ class Marker implements MapsObject { addIfPresent('draggable', draggable); addIfPresent('flat', flat); addIfPresent('icon', icon.toJson()); - addIfPresent('infoWindow', infoWindow._toJson()); + addIfPresent('infoWindow', infoWindow.toJson()); addIfPresent('position', position.toJson()); addIfPresent('rotation', rotation); addIfPresent('visible', visible); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart index 95c27d5bf95..08c34d29ea7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // All the public types exposed by this package. +export 'advanced_marker.dart'; export 'bitmap.dart'; export 'callbacks.dart'; export 'camera.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/map_configuration_serialization.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/map_configuration_serialization.dart index 43b25fa642d..bba79dfcc37 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/map_configuration_serialization.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/map_configuration_serialization.dart @@ -59,6 +59,8 @@ Map jsonForMapConfiguration(MapConfiguration config) { if (config.buildingsEnabled != null) 'buildingsEnabled': config.buildingsEnabled!, if (config.cloudMapId != null) 'cloudMapId': config.cloudMapId!, + if (config.mapId != null) 'mapId': config.mapId!, if (config.style != null) 'style': config.style!, + if (config.markerType != null) 'markerType': config.markerType!.index, }; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index faccbab1d66..6d8583e82a0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -122,6 +122,15 @@ void main() { }, ); + test( + 'default implementation of isAdvancedMarkersAvailable returns false', + () async { + final GoogleMapsFlutterPlatform platform = + BuildViewGoogleMapsFlutterPlatform(); + expect(await platform.isAdvancedMarkersAvailable(mapId: 0), isFalse); + }, + ); + test( 'default implementation of `animateCameraWithConfiguration` delegates to `animateCamera`', () { @@ -170,6 +179,7 @@ class BuildViewGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { Set>? gestureRecognizers = const >{}, Map mapOptions = const {}, + MarkerType markerType = MarkerType.marker, }) { return const Text(''); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/advanced_marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/advanced_marker_test.dart new file mode 100644 index 00000000000..d00ef98c052 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/advanced_marker_test.dart @@ -0,0 +1,184 @@ +// 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_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$AdvancedMarker', () { + test('constructor defaults', () { + final AdvancedMarker marker = + AdvancedMarker(markerId: const MarkerId('ABC123')); + + expect(marker.alpha, equals(1.0)); + expect(marker.anchor, equals(const Offset(0.5, 1.0))); + expect(marker.consumeTapEvents, equals(false)); + expect(marker.draggable, equals(false)); + expect(marker.flat, equals(false)); + expect(marker.icon, equals(BitmapDescriptor.defaultMarker)); + expect(marker.infoWindow, equals(InfoWindow.noText)); + expect(marker.position, equals(const LatLng(0.0, 0.0))); + expect(marker.rotation, equals(0.0)); + expect(marker.visible, equals(true)); + expect(marker.zIndex, equals(0.0)); + expect(marker.onTap, equals(null)); + expect(marker.onDrag, equals(null)); + expect(marker.onDragStart, equals(null)); + expect(marker.onDragEnd, equals(null)); + expect(marker.collisionBehavior, MarkerCollisionBehavior.requiredDisplay); + }); + + test('constructor alpha is >= 0.0 and <= 1.0', () { + void initWithAlpha(double alpha) { + AdvancedMarker(markerId: const MarkerId('ABC123'), alpha: alpha); + } + + expect(() => initWithAlpha(-0.5), throwsAssertionError); + expect(() => initWithAlpha(0.0), isNot(throwsAssertionError)); + expect(() => initWithAlpha(0.5), isNot(throwsAssertionError)); + expect(() => initWithAlpha(1.0), isNot(throwsAssertionError)); + expect(() => initWithAlpha(100), throwsAssertionError); + }); + + test('toJson', () { + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final AdvancedMarker marker = AdvancedMarker( + markerId: const MarkerId('ABC123'), + alpha: 0.12345, + anchor: const Offset(100, 100), + consumeTapEvents: true, + draggable: true, + flat: true, + icon: testDescriptor, + infoWindow: const InfoWindow( + title: 'Test title', + snippet: 'Test snippet', + anchor: Offset(100, 200), + ), + position: const LatLng(50, 50), + rotation: 100, + visible: false, + zIndex: 100, + onTap: () {}, + onDragStart: (LatLng latLng) {}, + onDrag: (LatLng latLng) {}, + onDragEnd: (LatLng latLng) {}, + collisionBehavior: MarkerCollisionBehavior.requiredAndHidesOptional, + ); + + final Map json = marker.toJson() as Map; + + expect(json, { + 'markerId': 'ABC123', + 'alpha': 0.12345, + 'anchor': [100, 100], + 'consumeTapEvents': true, + 'draggable': true, + 'flat': true, + 'icon': testDescriptor.toJson(), + 'infoWindow': { + 'title': 'Test title', + 'snippet': 'Test snippet', + 'anchor': [100.0, 200.0], + }, + 'position': [50, 50], + 'rotation': 100.0, + 'visible': false, + 'zIndex': 100.0, + 'collisionBehavior': + MarkerCollisionBehavior.requiredAndHidesOptional.index, + }); + }); + + test('clone', () { + final Marker marker = AdvancedMarker(markerId: const MarkerId('ABC123')); + final Marker clone = marker.clone(); + + expect(clone, isA()); + expect(identical(clone, marker), isFalse); + expect(clone, equals(marker)); + }); + + test('copyWith', () { + const MarkerId markerId = MarkerId('ABC123'); + final AdvancedMarker marker = AdvancedMarker(markerId: markerId); + + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + const double testAlphaParam = 0.12345; + const Offset testAnchorParam = Offset(100, 100); + final bool testConsumeTapEventsParam = !marker.consumeTapEvents; + final bool testDraggableParam = !marker.draggable; + final bool testFlatParam = !marker.flat; + final BitmapDescriptor testIconParam = testDescriptor; + const InfoWindow testInfoWindowParam = InfoWindow(title: 'Test'); + const LatLng testPositionParam = LatLng(100, 100); + const double testRotationParam = 100; + final bool testVisibleParam = !marker.visible; + const double testZIndexParam = 100; + const ClusterManagerId testClusterManagerIdParam = + ClusterManagerId('DEF123'); + final List log = []; + const MarkerCollisionBehavior testCollisionBehavior = + MarkerCollisionBehavior.requiredAndHidesOptional; + + final AdvancedMarker copy = marker.copyWith( + alphaParam: testAlphaParam, + anchorParam: testAnchorParam, + consumeTapEventsParam: testConsumeTapEventsParam, + draggableParam: testDraggableParam, + flatParam: testFlatParam, + iconParam: testIconParam, + infoWindowParam: testInfoWindowParam, + positionParam: testPositionParam, + rotationParam: testRotationParam, + visibleParam: testVisibleParam, + zIndexParam: testZIndexParam, + clusterManagerIdParam: testClusterManagerIdParam, + collisionBehaviorParam: testCollisionBehavior, + onTapParam: () { + log.add('onTapParam'); + }, + onDragStartParam: (LatLng latLng) { + log.add('onDragStartParam'); + }, + onDragParam: (LatLng latLng) { + log.add('onDragParam'); + }, + onDragEndParam: (LatLng latLng) { + log.add('onDragEndParam'); + }, + ); + + expect(copy.markerId, equals(markerId)); + expect(copy.alpha, equals(testAlphaParam)); + expect(copy.anchor, equals(testAnchorParam)); + expect(copy.consumeTapEvents, equals(testConsumeTapEventsParam)); + expect(copy.draggable, equals(testDraggableParam)); + expect(copy.flat, equals(testFlatParam)); + expect(copy.icon, equals(testIconParam)); + expect(copy.infoWindow, equals(testInfoWindowParam)); + expect(copy.position, equals(testPositionParam)); + expect(copy.rotation, equals(testRotationParam)); + expect(copy.visible, equals(testVisibleParam)); + expect(copy.zIndex, equals(testZIndexParam)); + expect(copy.clusterManagerId, equals(testClusterManagerIdParam)); + expect(copy.collisionBehavior, equals(testCollisionBehavior)); + + copy.onTap!(); + expect(log, contains('onTapParam')); + + copy.onDragStart!(const LatLng(0, 1)); + expect(log, contains('onDragStartParam')); + + copy.onDrag!(const LatLng(0, 1)); + expect(log, contains('onDragParam')); + + copy.onDragEnd!(const LatLng(0, 1)); + expect(log, contains('onDragEndParam')); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart index 001dbe755e3..fb8875d4999 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; void main() { @@ -682,6 +681,91 @@ void main() { }); }); + group('PinConfig', () { + test('construct with empty values, throws assertion error', () { + expect(() => PinConfig(), throwsAssertionError); + }); + + test('construct', () { + const PinConfig pinConfig = PinConfig( + backgroundColor: Colors.green, + borderColor: Colors.blue, + ); + expect(pinConfig, isA()); + expect(pinConfig.backgroundColor, Colors.green); + expect(pinConfig.borderColor, Colors.blue); + expect( + pinConfig.toJson(), + [ + PinConfig.type, + { + 'backgroundColor': Colors.green.value, + 'borderColor': Colors.blue.value, + }, + ], + ); + }); + + test('construct with glyph text', () { + const PinConfig pinConfig = PinConfig( + backgroundColor: Colors.green, + borderColor: Colors.blue, + glyph: TextGlyph(text: 'Hello', textColor: Colors.red), + ); + expect(pinConfig.glyph, isA()); + expect((pinConfig.glyph! as TextGlyph).text, 'Hello'); + expect((pinConfig.glyph! as TextGlyph).textColor, Colors.red); + expect( + pinConfig.toJson(), + [ + PinConfig.type, + { + 'backgroundColor': Colors.green.value, + 'borderColor': Colors.blue.value, + 'glyph': [ + 'textGlyph', + { + 'text': 'Hello', + 'textColor': Colors.red.value, + } + ], + }, + ], + ); + }); + + test('construct with glyph bitmap', () async { + const BitmapDescriptor bitmap = AssetBitmap(name: 'red_square.png'); + const PinConfig pinConfig = PinConfig( + backgroundColor: Colors.black, + borderColor: Colors.red, + glyph: BitmapGlyph(bitmap: bitmap), + ); + + expect(pinConfig.backgroundColor, Colors.black); + expect(pinConfig.borderColor, Colors.red); + expect( + pinConfig.toJson(), + [ + PinConfig.type, + { + 'glyph': [ + 'bitmapGlyph', + { + 'bitmap': [ + 'fromAsset', + 'red_square.png', + ], + }, + ], + 'backgroundColor': Colors.black.value, + 'borderColor': Colors.red.value, + }, + ], + ); + }); + }); + test('mapBitmapScaling from String', () { expect(mapBitmapScalingFromString('auto'), MapBitmapScaling.auto); expect(mapBitmapScalingFromString('none'), MapBitmapScaling.none); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart index e34f32676e0..bcd69a0e82f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/map_configuration_test.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -const String _kCloudMapId = '000000000000000'; // Dummy map ID. +const String _kMapId = '000000000000000'; // Dummy map ID. void main() { group('diffs', () { @@ -58,7 +58,7 @@ void main() { expect(updated.liteModeEnabled, isNot(null)); expect(updated.padding, isNot(null)); expect(updated.trafficEnabled, isNot(null)); - expect(updated.cloudMapId, null); + expect(updated.mapId, null); }); test('handle webGestureHandling', () async { @@ -395,7 +395,7 @@ void main() { }); test('handle cloudMapId', () async { - const MapConfiguration diff = MapConfiguration(cloudMapId: _kCloudMapId); + const MapConfiguration diff = MapConfiguration(cloudMapId: _kMapId); const MapConfiguration empty = MapConfiguration(); final MapConfiguration updated = diffBase.applyDiff(diff); @@ -405,7 +405,24 @@ void main() { // The diff from empty options should be the diff itself. expect(diff.diffFrom(empty), diff); // A diff applied to non-empty options should update that field. - expect(updated.cloudMapId, _kCloudMapId); + expect(updated.cloudMapId, _kMapId); + expect(updated.mapId, _kMapId); + // The hash code should change. + expect(empty.hashCode, isNot(diff.hashCode)); + }); + + test('handle mapId', () async { + const MapConfiguration diff = MapConfiguration(mapId: _kMapId); + + const MapConfiguration empty = MapConfiguration(); + final MapConfiguration updated = diffBase.applyDiff(diff); + + // A diff applied to empty options should be the diff itself. + expect(empty.applyDiff(diff), diff); + // The diff from empty options should be the diff itself. + expect(diff.diffFrom(empty), diff); + // A diff applied to non-empty options should update that field. + expect(updated.mapId, _kMapId); // The hash code should change. expect(empty.hashCode, isNot(diff.hashCode)); }); @@ -555,7 +572,13 @@ void main() { }); test('is false with cloudMapId', () async { - const MapConfiguration diff = MapConfiguration(cloudMapId: _kCloudMapId); + const MapConfiguration diff = MapConfiguration(mapId: _kMapId); + + expect(diff.isEmpty, false); + }); + + test('is false with mapId', () async { + const MapConfiguration diff = MapConfiguration(mapId: _kMapId); expect(diff.isEmpty, false); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/utils/map_configuration_serialization_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/utils/map_configuration_serialization_test.dart index 80618356c1c..3d00e04495b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/utils/map_configuration_serialization_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/utils/map_configuration_serialization_test.dart @@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_platform_interface/src/types/utils/map_configuration_serialization.dart'; -const String _kCloudMapId = '000000000000000'; // Dummy map ID. +const String _kMapId = '000000000000000'; // Dummy map ID. void main() { test('empty serialization', () async { @@ -20,26 +20,28 @@ void main() { test('complete serialization', () async { final MapConfiguration config = MapConfiguration( - compassEnabled: false, - mapToolbarEnabled: false, - cameraTargetBounds: CameraTargetBounds(LatLngBounds( - northeast: const LatLng(30, 20), southwest: const LatLng(10, 40))), - mapType: MapType.normal, - minMaxZoomPreference: const MinMaxZoomPreference(1.0, 10.0), - rotateGesturesEnabled: false, - scrollGesturesEnabled: false, - tiltGesturesEnabled: false, - trackCameraPosition: false, - zoomControlsEnabled: false, - zoomGesturesEnabled: false, - liteModeEnabled: false, - myLocationEnabled: false, - myLocationButtonEnabled: false, - padding: const EdgeInsets.all(5.0), - indoorViewEnabled: false, - trafficEnabled: false, - buildingsEnabled: false, - cloudMapId: _kCloudMapId); + compassEnabled: false, + mapToolbarEnabled: false, + cameraTargetBounds: CameraTargetBounds(LatLngBounds( + northeast: const LatLng(30, 20), southwest: const LatLng(10, 40))), + mapType: MapType.normal, + minMaxZoomPreference: const MinMaxZoomPreference(1.0, 10.0), + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + tiltGesturesEnabled: false, + trackCameraPosition: false, + zoomControlsEnabled: false, + zoomGesturesEnabled: false, + liteModeEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + padding: const EdgeInsets.all(5.0), + indoorViewEnabled: false, + trafficEnabled: false, + buildingsEnabled: false, + mapId: _kMapId, + cloudMapId: _kMapId, + ); final Map json = jsonForMapConfiguration(config); @@ -72,7 +74,31 @@ void main() { 'indoorEnabled': false, 'trafficEnabled': false, 'buildingsEnabled': false, - 'cloudMapId': _kCloudMapId + 'mapId': _kMapId, + 'cloudMapId': _kMapId, + }); + }); + + test('mapId preferred over cloudMapId', () { + const MapConfiguration config = MapConfiguration( + mapId: 'map-id', + cloudMapId: 'cloud-map-id', + ); + final Map json = jsonForMapConfiguration(config); + expect(json, { + 'mapId': 'map-id', + 'cloudMapId': 'map-id', + }); + }); + + test('mapId falls back to cloudMapId', () { + const MapConfiguration config = MapConfiguration( + cloudMapId: 'cloud-map-id', + ); + final Map json = jsonForMapConfiguration(config); + expect(json, { + 'mapId': 'cloud-map-id', + 'cloudMapId': 'cloud-map-id', }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md index 9a92c70bad1..2dd51dcaf8d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -32,7 +32,7 @@ Modify the `` tag of your `web/index.html` to load the Google Maps JavaScr The Google Maps Web SDK splits some of its functionality in [separate libraries](https://developers.google.com/maps/documentation/javascript/libraries#libraries-for-dynamic-library-import). If your app needs the `drawing` library (to draw polygons, rectangles, polylines, -circles or markers on a map), include it like this: +circles or legacy markers on a map), include it like this: ```html +``` + To request multiple libraries, separate them with commas: ```html ``` diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_marker_test.dart new file mode 100644 index 00000000000..95ee0f0deed --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_marker_test.dart @@ -0,0 +1,223 @@ +// 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:async'; +import 'dart:js_interop'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps_flutter_web/src/utils.dart'; +import 'package:integration_test/integration_test.dart'; + +/// Test Markers +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Since onTap/DragEnd events happen asynchronously, we need to store when the event + // is fired. We use a completer so the test can wait for the future to be completed. + late Completer methodCalledCompleter; + + /// This is the future value of the [methodCalledCompleter]. Reinitialized + /// in the [setUp] method, and completed (as `true`) by [onTap] and [onDragEnd] + /// when those methods are called from the MarkerController. + late Future methodCalled; + + void onTap() { + methodCalledCompleter.complete(true); + } + + void onDragStart(gmaps.LatLng _) { + methodCalledCompleter.complete(true); + } + + void onDrag(gmaps.LatLng _) { + methodCalledCompleter.complete(true); + } + + void onDragEnd(gmaps.LatLng _) { + methodCalledCompleter.complete(true); + } + + setUp(() { + methodCalledCompleter = Completer(); + methodCalled = methodCalledCompleter.future; + }); + + group('MarkerController', () { + late gmaps.AdvancedMarkerElement marker; + + setUp(() { + marker = gmaps.AdvancedMarkerElement(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onTap: onTap); + + // Trigger a click event... + gmaps.event.trigger( + marker, + 'click', + gmaps.MapMouseEvent(), + ); + + // The event handling is now truly async. Wait for it... + expect(await methodCalled, isTrue); + }); + + testWidgets('onDragStart gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onDragStart: onDragStart); + + // Trigger a drag end event... + gmaps.event.trigger( + marker, + 'dragstart', + gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0), + ); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDrag gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onDrag: onDrag); + + // Trigger a drag end event... + gmaps.event.trigger( + marker, + 'drag', + gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0), + ); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDragEnd gets called', (WidgetTester tester) async { + AdvancedMarkerController(marker: marker, onDragEnd: onDragEnd); + + // Trigger a drag end event... + gmaps.event.trigger( + marker, + 'dragend', + gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0), + ); + + expect(await methodCalled, isTrue); + }); + + testWidgets('update', (WidgetTester tester) async { + final AdvancedMarkerController controller = + AdvancedMarkerController(marker: marker); + final gmaps.AdvancedMarkerElementOptions options = + gmaps.AdvancedMarkerElementOptions() + ..collisionBehavior = + gmaps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY + ..gmpDraggable = true + ..position = gmaps.LatLng(42, 54); + + expect(marker.collisionBehavior, gmaps.CollisionBehavior.REQUIRED); + expect(marker.gmpDraggable, isFalse); + + controller.update(options); + + expect(marker.gmpDraggable, isTrue); + expect( + marker.collisionBehavior, + gmaps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY, + ); + final JSAny? position = marker.position; + expect(position, isNotNull); + expect(position is gmaps.LatLngLiteral, isTrue); + expect((position! as gmaps.LatLngLiteral).lat, equals(42)); + expect((position as gmaps.LatLngLiteral).lng, equals(54)); + }); + + testWidgets('infoWindow null, showInfoWindow.', + (WidgetTester tester) async { + final AdvancedMarkerController controller = + AdvancedMarkerController(marker: marker); + + controller.showInfoWindow(); + + expect(controller.infoWindowShown, isFalse); + }); + + testWidgets('showInfoWindow', (WidgetTester tester) async { + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.Map map = gmaps.Map(createDivElement()); + marker.map = map; + final AdvancedMarkerController controller = AdvancedMarkerController( + marker: marker, + infoWindow: infoWindow, + ); + + controller.showInfoWindow(); + + expect(infoWindow.get('map'), map); + expect(controller.infoWindowShown, isTrue); + }); + + testWidgets('hideInfoWindow', (WidgetTester tester) async { + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.Map map = gmaps.Map(createDivElement()); + marker.map = map; + final AdvancedMarkerController controller = AdvancedMarkerController( + marker: marker, + infoWindow: infoWindow, + ); + + controller.hideInfoWindow(); + + expect(infoWindow.get('map'), isNull); + expect(controller.infoWindowShown, isFalse); + }); + + group('remove', () { + late AdvancedMarkerController controller; + + setUp(() { + final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); + final gmaps.Map map = gmaps.Map(createDivElement()); + marker.map = map; + controller = + AdvancedMarkerController(marker: marker, infoWindow: infoWindow); + }); + + testWidgets('drops gmaps instance', (WidgetTester tester) async { + controller.remove(); + + expect(controller.marker, isNull); + }); + + testWidgets('cannot call update after remove', + (WidgetTester tester) async { + final gmaps.AdvancedMarkerElementOptions options = + gmaps.AdvancedMarkerElementOptions()..gmpDraggable = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); + + testWidgets('cannot call showInfoWindow after remove', + (WidgetTester tester) async { + controller.remove(); + + expect(() { + controller.showInfoWindow(); + }, throwsAssertionError); + }); + + testWidgets('cannot call hideInfoWindow after remove', + (WidgetTester tester) async { + controller.remove(); + + expect(() { + controller.hideInfoWindow(); + }, throwsAssertionError); + }); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_markers_test.dart new file mode 100644 index 00000000000..61cb1deff60 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/advanced_markers_test.dart @@ -0,0 +1,619 @@ +// 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:async'; +import 'dart:convert'; +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps_flutter_web/src/marker_clustering.dart'; +import 'package:google_maps_flutter_web/src/utils.dart'; +import 'package:http/http.dart' as http; +import 'package:integration_test/integration_test.dart'; +import 'package:web/src/dom.dart' as dom; +import 'package:web/web.dart'; + +import 'resources/icon_image_base64.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MarkersController', () { + late StreamController> events; + late MarkersController controller; + late ClusterManagersController + clusterManagersController; + late gmaps.Map map; + + setUp(() { + events = StreamController>(); + + clusterManagersController = + ClusterManagersController( + stream: events); + controller = AdvancedMarkersController( + stream: events, + clusterManagersController: clusterManagersController, + ); + map = gmaps.Map(createDivElement()); + clusterManagersController.bindToMap(123, map); + controller.bindToMap(123, map); + }); + + testWidgets('addMarkers', (WidgetTester tester) async { + final Set markers = { + AdvancedMarker(markerId: const MarkerId('1')), + AdvancedMarker(markerId: const MarkerId('2')), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 2); + expect(controller.markers, contains(const MarkerId('1'))); + expect(controller.markers, contains(const MarkerId('2'))); + expect(controller.markers, isNot(contains(const MarkerId('66')))); + }); + + testWidgets('changeMarkers', (WidgetTester tester) async { + gmaps.AdvancedMarkerElement? marker; + gmaps.LatLngLiteral? position; + + final Set markers = { + AdvancedMarker(markerId: const MarkerId('1')), + }; + await controller.addMarkers(markers); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isFalse); + + // By default, markers fall in LatLng(0, 0). + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(0)); + expect(position.lng, equals(0)); + + // Update the marker with draggable and position. + final Set updatedMarkers = { + AdvancedMarker( + markerId: const MarkerId('1'), + draggable: true, + position: const LatLng(42, 54), + ), + }; + await controller.changeMarkers(updatedMarkers); + expect(controller.markers.length, 1); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isTrue); + + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(42)); + expect(position.lng, equals(54)); + }); + + testWidgets( + 'changeMarkers resets marker position if not passed when updating!', + (WidgetTester tester) async { + gmaps.AdvancedMarkerElement? marker; + gmaps.LatLngLiteral? position; + + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + position: const LatLng(42, 54), + ), + }; + await controller.addMarkers(markers); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isFalse); + + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(42)); + expect(position.lng, equals(54)); + + // Update the marker without position. + final Set updatedMarkers = { + AdvancedMarker( + markerId: const MarkerId('1'), + draggable: true, + ), + }; + await controller.changeMarkers(updatedMarkers); + expect(controller.markers.length, 1); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.gmpDraggable, isTrue); + + position = marker.position! as gmaps.LatLngLiteral; + expect(position, isNotNull); + expect(position.lat, equals(0)); + expect(position.lng, equals(0)); + }); + + testWidgets('removeMarkers', (WidgetTester tester) async { + final Set markers = { + AdvancedMarker(markerId: const MarkerId('1')), + AdvancedMarker(markerId: const MarkerId('2')), + AdvancedMarker(markerId: const MarkerId('3')), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 3); + + // Remove some markers. + final Set markerIdsToRemove = { + const MarkerId('1'), + const MarkerId('3'), + }; + + controller.removeMarkers(markerIdsToRemove); + + expect(controller.markers.length, 1); + expect(controller.markers, isNot(contains(const MarkerId('1')))); + expect(controller.markers, contains(const MarkerId('2'))); + expect(controller.markers, isNot(contains(const MarkerId('3')))); + }); + + testWidgets('InfoWindow show/hide', (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow(title: 'Title', snippet: 'Snippet'), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(const MarkerId('1')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isTrue); + + controller.hideMarkerInfoWindow(const MarkerId('1')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + }); + + testWidgets('only single InfoWindow is visible', + (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow(title: 'Title', snippet: 'Snippet'), + ), + AdvancedMarker( + markerId: const MarkerId('2'), + infoWindow: const InfoWindow(title: 'Title', snippet: 'Snippet'), + ), + }; + await controller.addMarkers(markers); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(const MarkerId('1')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isTrue); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(const MarkerId('2')); + + expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isTrue); + }); + + testWidgets('markers with custom asset icon work', + (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/red_square.png')); + + // Asset size is 48x48 physical pixels. + expect(icon.style.width, '48px'); + expect(icon.style.height, '48px'); + }); + + testWidgets('markers with custom asset icon and pixel ratio work', + (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 2.0, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/red_square.png')); + + // Asset size is 48x48 physical pixels, and with pixel ratio 2.0 it + // should be drawn with size 24x24 logical pixels. + expect(icon.style.width, '24px'); + expect(icon.style.height, '24px'); + }); + + testWidgets('markers with custom asset icon with width and height work', + (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 2.0, + width: 64, + height: 64, + )), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/red_square.png')); + + // Asset size is 48x48 physical pixels, + // and scaled to requested 64x64 size. + expect(icon.style.width, '64px'); + expect(icon.style.height, '64px'); + }); + + testWidgets('markers with missing asset icon should not set size', + (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/broken_asset_name.png', + imagePixelRatio: 2.0, + )), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLImageElement?; + expect(icon, isNotNull); + + final String assetUrl = icon!.src; + expect(assetUrl, endsWith('assets/broken_asset_name.png')); + + // For invalid assets, the size and scaledSize should be null. + expect(icon.style.width, isEmpty); + expect(icon.style.height, isEmpty); + }); + + testWidgets('markers with custom bitmap icon work', + (WidgetTester tester) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: tester.view.devicePixelRatio, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLImageElement?; + expect(icon, isNotNull); + + final String blobUrl = icon!.src; + expect(blobUrl, startsWith('blob:')); + + final http.Response response = await http.get(Uri.parse(blobUrl)); + expect( + response.bodyBytes, + bytes, + reason: + 'Bytes from the Icon blob must match bytes used to create AdvancedMarker', + ); + + // Icon size is 16x16 pixels, this should be automatically read from the + // bitmap and set to the icon size scaled to 8x8 using the + // given imagePixelRatio. + final int expectedSize = 16 ~/ tester.view.devicePixelRatio; + expect(icon.style.width, '${expectedSize}px'); + expect(icon.style.height, '${expectedSize}px'); + }); + + testWidgets('markers with custom bitmap icon and pixel ratio work', + (WidgetTester tester) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: 1, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLImageElement?; + expect(icon, isNotNull); + + // Icon size is 16x16 pixels, this should be automatically read from the + // bitmap and set to the icon size and should not be changed as + // image pixel ratio is set to 1.0. + expect(icon!.style.width, '16px'); + expect(icon.style.height, '16px'); + }); + + testWidgets('markers with custom bitmap icon pass size to sdk', + (WidgetTester tester) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + width: 20, + height: 30, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLImageElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLImageElement?; + expect(icon, isNotNull); + expect(icon!.style.width, '20px'); + expect(icon.style.height, '30px'); + }); + + testWidgets('markers created with pin config and colored glyph work', + (WidgetTester widgetTester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.pinConfig( + backgroundColor: const Color(0xFF00FF00), + borderColor: const Color(0xFFFF0000), + glyph: const CircleGlyph(color: Color(0xFFFFFFFF)), + ), + ), + }; + await controller.addMarkers(markers); + expect(controller.markers.length, 1); + + final HTMLDivElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLDivElement?; + expect(icon, isNotNull); + + // Query nodes and check colors. This is a bit fragile as it depends on + // the implementation details of the icon which is not part of the public + // API. + final NodeList backgroundNodes = + icon!.querySelectorAll("[class*='maps-pin-view-background']"); + final NodeList borderNodes = + icon.querySelectorAll("[class*='maps-pin-view-border']"); + final NodeList glyphNodes = + icon.querySelectorAll("[class*='maps-pin-view-default-glyph']"); + + expect(backgroundNodes.length, 1); + expect(borderNodes.length, 1); + expect(glyphNodes.length, 1); + + expect( + (backgroundNodes.item(0)! as dom.Element) + .getAttribute('fill') + ?.toUpperCase(), + '#00FF00', + ); + expect( + (borderNodes.item(0)! as dom.Element) + .getAttribute('fill') + ?.toUpperCase(), + '#FF0000', + ); + expect( + (glyphNodes.item(0)! as dom.Element) + .getAttribute('fill') + ?.toUpperCase(), + '#FFFFFF', + ); + }); + + testWidgets('markers created with text glyph work', + (WidgetTester widgetTester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.pinConfig( + backgroundColor: Colors.black, + borderColor: Colors.black, + glyph: const TextGlyph( + text: 'Hey', + textColor: Color(0xFF0000FF), + ), + ), + ), + }; + await controller.addMarkers(markers); + expect(controller.markers.length, 1); + + final HTMLDivElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLDivElement?; + expect(icon, isNotNull); + + // Query pin nodes and find text element. This is a bit fragile as it + // depends on the implementation details of the icon which is not part of + // the public API. + dom.Element? paragraphElement; + final NodeList paragraphs = icon!.querySelectorAll('p'); + for (int i = 0; i < paragraphs.length; i++) { + final dom.Element? paragraph = paragraphs.item(i) as dom.Element?; + if (paragraph?.innerHTML.toString() == 'Hey') { + paragraphElement = paragraph; + break; + } + } + + expect(paragraphElement, isNotNull); + expect(paragraphElement!.innerHTML.toString(), 'Hey'); + + expect( + paragraphElement.getAttribute('style')?.toLowerCase(), + contains('color: #0000ff'), + ); + }); + + testWidgets('markers created with bitmap glyph work', + (WidgetTester widgetTester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.pinConfig( + backgroundColor: Colors.black, + borderColor: Colors.black, + glyph: BitmapGlyph( + bitmap: await BitmapDescriptor.asset( + const ImageConfiguration( + size: Size.square(12), + ), + 'assets/red_square.png', + ), + ), + ), + ), + }; + await controller.addMarkers(markers); + expect(controller.markers.length, 1); + + final HTMLDivElement? icon = controller + .markers[const MarkerId('1')]?.marker?.content as HTMLDivElement?; + expect(icon, isNotNull); + + // Query pin nodes and find text element. This is a bit fragile as it + // depends on the implementation details of the icon which is not part of + // the public API. + HTMLImageElement? imgElement; + final NodeList imgElements = icon!.querySelectorAll('img'); + for (int i = 0; i < imgElements.length; i++) { + final dom.Element? img = imgElements.item(i) as dom.Element?; + final String src = (img! as HTMLImageElement).src; + if (src.endsWith('assets/red_square.png')) { + imgElement = img as HTMLImageElement; + break; + } + } + + expect(imgElement, isNotNull); + expect(imgElement!.src, endsWith('assets/red_square.png')); + expect( + imgElement.getAttribute('style')?.toLowerCase(), + contains('width: 12.0px; height: 12.0px;'), + ); + }); + + testWidgets('InfoWindow snippet can have links', + (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow( + title: 'title for test', + snippet: 'Go to Google >>>', + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLElement? content = controller + .markers[const MarkerId('1')]?.infoWindow?.content as HTMLElement?; + expect(content, isNotNull); + + final String innerHtml = (content!.innerHTML as JSString).toDart; + expect(innerHtml, contains('title for test')); + expect( + innerHtml, + contains( + 'Go to Google >>>', + )); + }); + + testWidgets('InfoWindow content is clickable', (WidgetTester tester) async { + final Set markers = { + AdvancedMarker( + markerId: const MarkerId('1'), + infoWindow: const InfoWindow( + title: 'title for test', + snippet: 'some snippet', + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final HTMLElement? content = controller + .markers[const MarkerId('1')]?.infoWindow?.content as HTMLElement?; + + content?.click(); + + final MapEvent event = await events.stream.first; + + expect(event, isA()); + expect((event as InfoWindowTapEvent).value, equals(const MarkerId('1'))); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index dcf5345ae95..b55de6a0034 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -18,7 +18,7 @@ import 'google_maps_controller_test.mocks.dart'; // This value is used when comparing long~num, like // LatLng values. -const String _kCloudMapId = '000000000000000'; // Dummy map ID. +const String _kMapId = '000000000000000'; // Dummy map ID. gmaps.Map mapShim() => throw UnimplementedError(); @@ -35,7 +35,7 @@ gmaps.Map mapShim() => throw UnimplementedError(); MockSpec( fallbackGenerators: {#googleMap: mapShim}, ), - MockSpec( + MockSpec>( fallbackGenerators: {#googleMap: mapShim}, ), MockSpec( @@ -66,8 +66,9 @@ void main() { mapId: mapId, streamController: stream, widgetConfiguration: MapWidgetConfiguration( - initialCameraPosition: initialCameraPosition, - textDirection: TextDirection.ltr), + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + ), mapObjects: mapObjects, mapConfiguration: mapConfiguration, ); @@ -434,7 +435,7 @@ void main() { mapConfiguration: const MapConfiguration( mapType: MapType.satellite, zoomControlsEnabled: true, - cloudMapId: _kCloudMapId, + mapId: _kMapId, fortyFiveDegreeImageryEnabled: false, )); controller.debugSetOverrides( @@ -448,7 +449,7 @@ void main() { expect(capturedOptions, isNotNull); expect(capturedOptions!.mapTypeId, gmaps.MapTypeId.SATELLITE); expect(capturedOptions!.zoomControl, true); - expect(capturedOptions!.mapId, _kCloudMapId); + expect(capturedOptions!.mapId, _kMapId); expect(capturedOptions!.gestureHandling, 'auto', reason: 'by default the map handles zoom/pan gestures internally'); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index 7904b7352cb..964e7450480 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -26,6 +26,17 @@ import 'google_maps_controller_test.dart' as _i5; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeMarkerController_0 extends _i1.SmartFake + implements _i2.MarkerController { + _FakeMarkerController_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [CirclesController]. /// /// See the documentation for Mockito's code generation for more information. @@ -385,13 +396,16 @@ class MockPolylinesController extends _i1.Mock /// A class which mocks [MarkersController]. /// /// See the documentation for Mockito's code generation for more information. -class MockMarkersController extends _i1.Mock implements _i2.MarkersController { +class MockMarkersController extends _i1.Mock + implements _i2.MarkersController { @override - Map<_i3.MarkerId, _i2.MarkerController> get markers => (super.noSuchMethod( + Map<_i3.MarkerId, _i2.MarkerController> get markers => + (super.noSuchMethod( Invocation.getter(#markers), - returnValue: <_i3.MarkerId, _i2.MarkerController>{}, - returnValueForMissingStub: <_i3.MarkerId, _i2.MarkerController>{}, - ) as Map<_i3.MarkerId, _i2.MarkerController>); + returnValue: <_i3.MarkerId, _i2.MarkerController>{}, + returnValueForMissingStub: <_i3.MarkerId, + _i2.MarkerController>{}, + ) as Map<_i3.MarkerId, _i2.MarkerController>); @override _i4.Map get googleMap => (super.noSuchMethod( @@ -436,6 +450,48 @@ class MockMarkersController extends _i1.Mock implements _i2.MarkersController { returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); + @override + _i6.Future<_i2.MarkerController> createMarkerController( + _i3.Marker? marker, + Object? currentMarker, + dynamic gmInfoWindow, + ) => + (super.noSuchMethod( + Invocation.method( + #createMarkerController, + [ + marker, + currentMarker, + gmInfoWindow, + ], + ), + returnValue: _i6.Future<_i2.MarkerController>.value( + _FakeMarkerController_0( + this, + Invocation.method( + #createMarkerController, + [ + marker, + currentMarker, + gmInfoWindow, + ], + ), + )), + returnValueForMissingStub: + _i6.Future<_i2.MarkerController>.value( + _FakeMarkerController_0( + this, + Invocation.method( + #createMarkerController, + [ + marker, + currentMarker, + gmInfoWindow, + ], + ), + )), + ) as _i6.Future<_i2.MarkerController>); + @override _i6.Future changeMarkers(Set<_i3.Marker>? markersToChange) => (super.noSuchMethod( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart index ce39bea20b7..1ad5c6844bc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_clustering_test.dart @@ -98,7 +98,7 @@ void main() { } // Repeatedly checks an asynchronous value against a test condition, waiting -// one frame between each check, returing the value if it passes the predicate +// one frame between each check, returning the value if it passes the predicate // before [maxTries] is reached. // // Returns null if the predicate is never satisfied. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart index 2859d082b78..cf54abe7179 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart @@ -53,7 +53,7 @@ void main() { }); testWidgets('onTap gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onTap: onTap); + LegacyMarkerController(marker: marker, onTap: onTap); // Trigger a click event... gmaps.event.trigger( @@ -67,7 +67,7 @@ void main() { }); testWidgets('onDragStart gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onDragStart: onDragStart); + LegacyMarkerController(marker: marker, onDragStart: onDragStart); // Trigger a drag end event... gmaps.event.trigger( @@ -80,7 +80,7 @@ void main() { }); testWidgets('onDrag gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onDrag: onDrag); + LegacyMarkerController(marker: marker, onDrag: onDrag); // Trigger a drag end event... gmaps.event.trigger( @@ -93,7 +93,7 @@ void main() { }); testWidgets('onDragEnd gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onDragEnd: onDragEnd); + LegacyMarkerController(marker: marker, onDragEnd: onDragEnd); // Trigger a drag end event... gmaps.event.trigger( @@ -106,7 +106,8 @@ void main() { }); testWidgets('update', (WidgetTester tester) async { - final MarkerController controller = MarkerController(marker: marker); + final LegacyMarkerController controller = + LegacyMarkerController(marker: marker); final gmaps.MarkerOptions options = gmaps.MarkerOptions() ..draggable = true ..position = gmaps.LatLng(42, 54); @@ -122,7 +123,8 @@ void main() { testWidgets('infoWindow null, showInfoWindow.', (WidgetTester tester) async { - final MarkerController controller = MarkerController(marker: marker); + final LegacyMarkerController controller = + LegacyMarkerController(marker: marker); controller.showInfoWindow(); @@ -133,7 +135,7 @@ void main() { final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); final gmaps.Map map = gmaps.Map(createDivElement()); marker.set('map', map); - final MarkerController controller = MarkerController( + final LegacyMarkerController controller = LegacyMarkerController( marker: marker, infoWindow: infoWindow, ); @@ -148,7 +150,7 @@ void main() { final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); final gmaps.Map map = gmaps.Map(createDivElement()); marker.set('map', map); - final MarkerController controller = MarkerController( + final LegacyMarkerController controller = LegacyMarkerController( marker: marker, infoWindow: infoWindow, ); @@ -160,13 +162,14 @@ void main() { }); group('remove', () { - late MarkerController controller; + late LegacyMarkerController controller; setUp(() { final gmaps.InfoWindow infoWindow = gmaps.InfoWindow(); final gmaps.Map map = gmaps.Map(createDivElement()); marker.set('map', map); - controller = MarkerController(marker: marker, infoWindow: infoWindow); + controller = + LegacyMarkerController(marker: marker, infoWindow: infoWindow); }); testWidgets('drops gmaps instance', (WidgetTester tester) async { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart index 45b218f139c..dcc10c1702c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart @@ -25,15 +25,16 @@ void main() { group('MarkersController', () { late StreamController> events; - late MarkersController controller; - late ClusterManagersController clusterManagersController; + late LegacyMarkersController controller; + late ClusterManagersController clusterManagersController; late gmaps.Map map; setUp(() { events = StreamController>(); - clusterManagersController = ClusterManagersController(stream: events); - controller = MarkersController( + clusterManagersController = + ClusterManagersController(stream: events); + controller = LegacyMarkersController( stream: events, clusterManagersController: clusterManagersController); map = gmaps.Map(createDivElement()); clusterManagersController.bindToMap(123, map); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index f1f0b079fc6..7cde497da89 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -28,9 +28,20 @@ flutter: assets: - assets/ + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins dependency_overrides: + # Override the google_maps_flutter dependency on google_maps_flutter_web. # TODO(ditman): Unwind the circular dependency. This will create problems # if we need to make a breaking change to google_maps_flutter_web. + google_maps_flutter: + path: ../../../google_maps_flutter/google_maps_flutter + google_maps_flutter_android: + path: ../../../google_maps_flutter/google_maps_flutter_android + google_maps_flutter_platform_interface: + path: ../../../google_maps_flutter/google_maps_flutter_platform_interface google_maps_flutter_web: - path: ../ + path: ../../../google_maps_flutter/google_maps_flutter_web + diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html index 51b7253b8e1..83a2193f41d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html @@ -6,10 +6,11 @@ Browser Tests - + + diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index f8b7dd0506b..4e13d2636ef 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -20,6 +20,7 @@ import 'package:google_maps/google_maps_visualization.dart' as visualization; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:sanitize_html/sanitize_html.dart'; import 'package:stream_transform/stream_transform.dart'; +import 'package:web/web.dart' as web; import 'package:web/web.dart'; import 'src/dom_window_extension.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index 88ae0fb849c..a645f00e7ab 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -101,7 +101,7 @@ gmaps.MapOptions _configurationAndStyleToGmapsOptions( // See updateMapConfiguration for why this is not using configuration.style. options.styles = styles; - options.mapId = configuration.cloudMapId; + options.mapId = configuration.mapId; return options; } @@ -193,7 +193,7 @@ LatLng gmLatLngToLatLng(gmaps.LatLng latLng) { } /// Converts a [gmaps.LatLngBounds] into a [LatLngBounds]. -LatLngBounds gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) { +LatLngBounds gmLatLngBoundsToLatLngBounds(gmaps.LatLngBounds latLngBounds) { return LatLngBounds( southwest: gmLatLngToLatLng(latLngBounds.southWest), northeast: gmLatLngToLatLng(latLngBounds.northEast), @@ -289,21 +289,32 @@ gmaps.Size? _gmSizeFromIconConfig(List iconConfig, int sizeIndex) { ); } } + return size; } -/// Sets the size of the Google Maps icon. -void _setIconSize({ - required Size size, - required gmaps.Icon icon, +// Sets the size and style of the [icon] element. +void _setIconStyle({ + required web.Element icon, + required gmaps.Size? size, + required double? opacity, + required bool? isVisible, }) { - final gmaps.Size gmapsSize = gmaps.Size(size.width, size.height); - icon.size = gmapsSize; - icon.scaledSize = gmapsSize; + icon.setAttribute( + 'style', + [ + if (size != null) ...[ + 'width: ${size.width.toStringAsFixed(1)}px;', + 'height: ${size.height.toStringAsFixed(1)}px;', + ], + if (opacity != null) 'opacity: $opacity;', + if (isVisible != null) 'visibility: ${isVisible ? 'visible' : 'hidden'};', + ].join(' '), + ); } void _setIconAnchor({ - required Size size, + required gmaps.Size size, required Offset anchor, required gmaps.Icon icon, }) { @@ -314,6 +325,16 @@ void _setIconAnchor({ icon.anchor = gmapsAnchor; } +// Sets the size of the Google Maps icon. +void _setIconSize({ + required gmaps.Size size, + required gmaps.Icon icon, +}) { + final gmaps.Size gmapsSize = gmaps.Size(size.width, size.height); + icon.size = gmapsSize; + icon.scaledSize = gmapsSize; +} + /// Determines the appropriate size for a bitmap based on its descriptor. /// /// This method returns the icon's size based on the provided [width] and @@ -321,12 +342,12 @@ void _setIconAnchor({ /// [imagePixelRatio] based on the actual size of the image fetched from the /// [url]. If only one of the dimensions is provided, the other is calculated to /// maintain the image's original aspect ratio. -Future _getBitmapSize(MapBitmap mapBitmap, String url) async { +Future _getBitmapSize(MapBitmap mapBitmap, String url) async { final double? width = mapBitmap.width; final double? height = mapBitmap.height; if (width != null && height != null) { // If both, width and height are set, return the provided dimensions. - return Size(width, height); + return gmaps.Size(width, height); } else { assert( url.isNotEmpty, 'URL must not be empty when calculating dimensions.'); @@ -355,7 +376,7 @@ Future _getBitmapSize(MapBitmap mapBitmap, String url) async { } // Return the calculated size. - return Size(targetWidth, targetHeight); + return gmaps.Size(targetWidth, targetHeight); } } @@ -387,19 +408,155 @@ void _cleanUpBitmapConversionCaches() { _bitmapBlobUrlCache.clear(); } +/// Converts a [BitmapDescriptor] into a [Node] that can be used as +/// [AdvancedMarker]'s icon. +Future _advancedMarkerIconFromBitmapDescriptor( + BitmapDescriptor bitmapDescriptor, { + required double? opacity, + required bool isVisible, + required double? rotation, +}) async { + if (bitmapDescriptor is PinConfig) { + final gmaps.PinElementOptions options = gmaps.PinElementOptions() + ..background = bitmapDescriptor.backgroundColor != null + ? _getCssColor(bitmapDescriptor.backgroundColor!) + : null + ..borderColor = bitmapDescriptor.borderColor != null + ? _getCssColor(bitmapDescriptor.borderColor!) + : null; + + final AdvancedMarkerGlyph? glyph = bitmapDescriptor.glyph; + switch (glyph) { + case final CircleGlyph circleGlyph: + options.glyphColor = _getCssColor(circleGlyph.color); + case final TextGlyph textGlyph: + final web.Element element = document.createElement('p'); + element.innerHTML = textGlyph.text.toJS; + if (textGlyph.textColor != null) { + element.setAttribute( + 'style', + 'color: ${_getCssColor(textGlyph.textColor!)}', + ); + } + options.glyph = element; + case final BitmapGlyph bitmapGlyph: + final Node? glyphBitmap = await _advancedMarkerIconFromBitmapDescriptor( + bitmapGlyph.bitmap, + // Always opaque, opacity is handled by the parent marker. + opacity: 1.0, + // Always visible, as the visibility is handled by the parent marker. + isVisible: true, + rotation: rotation, + ); + options.glyph = glyphBitmap; + case null: + break; + } + + final gmaps.PinElement pinElement = gmaps.PinElement(options); + final HTMLElement htmlElement = pinElement.element; + htmlElement.style + ..visibility = isVisible ? 'visible' : 'hidden' + ..opacity = opacity?.toString() ?? '1.0' + ..transform = rotation != null ? 'rotate(${rotation}deg)' : ''; + return htmlElement; + } + + if (bitmapDescriptor is MapBitmap) { + final String url = switch (bitmapDescriptor) { + (final BytesMapBitmap bytesMapBitmap) => + _bitmapBlobUrlCache.putIfAbsent(bytesMapBitmap.byteData.hashCode, () { + final Blob blob = + Blob([bytesMapBitmap.byteData.toJS].toJS); + return URL.createObjectURL(blob as JSObject); + }), + (final AssetMapBitmap assetMapBitmap) => + ui_web.assetManager.getAssetUrl(assetMapBitmap.assetName), + _ => throw UnimplementedError(), + }; + + final web.Element icon = document.createElement('img') + ..setAttribute('src', url); + + final gmaps.Size? size = switch (bitmapDescriptor.bitmapScaling) { + MapBitmapScaling.auto => await _getBitmapSize(bitmapDescriptor, url), + MapBitmapScaling.none => null, + }; + _setIconStyle( + icon: icon, size: size, opacity: opacity, isVisible: isVisible); + + return icon; + } + + // The following code is for the deprecated BitmapDescriptor.fromBytes + // and BitmapDescriptor.fromAssetImage. + final List iconConfig = bitmapDescriptor.toJson() as List; + if (iconConfig[0] == 'fromAssetImage') { + assert(iconConfig.length >= 2); + // iconConfig[2] contains the DPIs of the screen, but that information is + // already encoded in the iconConfig[1] + final web.Element icon = document.createElement('img') + ..setAttribute( + 'src', + ui_web.assetManager.getAssetUrl(iconConfig[1]! as String), + ); + + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 3); + _setIconStyle( + icon: icon, size: size, opacity: opacity, isVisible: isVisible); + return icon; + } else if (iconConfig[0] == 'fromBytes') { + // Grab the bytes, and put them into a blob. + final List bytes = iconConfig[1]! as List; + // Create a Blob from bytes, but let the browser figure out the encoding. + final Blob blob; + + assert( + bytes is Uint8List, + 'The bytes for a BitmapDescriptor icon must be a Uint8List', + ); + + // TODO(ditman): Improve this conversion + // See https://github.com/dart-lang/web/issues/180 + blob = Blob([(bytes as Uint8List).toJS].toJS); + + final web.Element icon = document.createElement('img') + ..setAttribute('src', URL.createObjectURL(blob as JSObject)); + + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 2); + _setIconStyle( + size: size, icon: icon, opacity: opacity, isVisible: isVisible); + return icon; + } + + return null; +} + // Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. Future _gmIconFromBitmapDescriptor( - BitmapDescriptor bitmapDescriptor, Offset anchor) async { + BitmapDescriptor bitmapDescriptor, + Offset anchor, +) async { gmaps.Icon? icon; if (bitmapDescriptor is MapBitmap) { - final String url = urlFromMapBitmap(bitmapDescriptor); + final String url = switch (bitmapDescriptor) { + (final BytesMapBitmap bytesMapBitmap) => + _bitmapBlobUrlCache.putIfAbsent(bytesMapBitmap.byteData.hashCode, () { + final Blob blob = + Blob([bytesMapBitmap.byteData.toJS].toJS); + return URL.createObjectURL(blob as JSObject); + }), + (final AssetMapBitmap assetMapBitmap) => + ui_web.assetManager.getAssetUrl(assetMapBitmap.assetName), + _ => throw UnimplementedError(), + }; icon = gmaps.Icon()..url = url; switch (bitmapDescriptor.bitmapScaling) { case MapBitmapScaling.auto: - final Size? size = await _getBitmapSize(bitmapDescriptor, url); + final gmaps.Size? size = await _getBitmapSize(bitmapDescriptor, url); if (size != null) { _setIconSize(size: size, icon: icon); _setIconAnchor(size: size, anchor: anchor, icon: icon); @@ -407,6 +564,7 @@ Future _gmIconFromBitmapDescriptor( case MapBitmapScaling.none: break; } + return icon; } @@ -455,23 +613,63 @@ Future _gmIconFromBitmapDescriptor( // Computes the options for a new [gmaps.Marker] from an incoming set of options // [marker], and the existing marker registered with the map: [currentMarker]. -Future _markerOptionsFromMarker( +Future _markerOptionsFromMarker( Marker marker, - gmaps.Marker? currentMarker, + T? currentMarker, ) async { - return gmaps.MarkerOptions() - ..position = gmaps.LatLng( - marker.position.latitude, - marker.position.longitude, - ) - ..title = sanitizeHtml(marker.infoWindow.title ?? '') - ..zIndex = marker.zIndex - ..visible = marker.visible - ..opacity = marker.alpha - ..draggable = marker.draggable - ..icon = await _gmIconFromBitmapDescriptor(marker.icon, marker.anchor); - // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. - // Flat and Rotation are not supported directly on the web. + if (marker is AdvancedMarker) { + final gmaps.AdvancedMarkerElementOptions options = + gmaps.AdvancedMarkerElementOptions() + ..collisionBehavior = _markerCollisionBehaviorToGmCollisionBehavior( + marker.collisionBehavior, + ) + ..content = await _advancedMarkerIconFromBitmapDescriptor( + marker.icon, + opacity: marker.alpha, + isVisible: marker.visible, + rotation: marker.rotation, + ) + ..position = gmaps.LatLng( + marker.position.latitude, + marker.position.longitude, + ) + ..title = sanitizeHtml(marker.infoWindow.title ?? '') + ..zIndex = marker.zIndex + ..gmpDraggable = marker.draggable; + return options as O; + } else { + final gmaps.MarkerOptions options = gmaps.MarkerOptions() + ..position = gmaps.LatLng( + marker.position.latitude, + marker.position.longitude, + ) + ..icon = await _gmIconFromBitmapDescriptor(marker.icon, marker.anchor) + ..title = sanitizeHtml(marker.infoWindow.title ?? '') + ..zIndex = marker.zIndex + ..visible = marker.visible + ..opacity = marker.alpha + ..draggable = marker.draggable; + + // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. + // Flat and Rotation are not supported directly on the web. + + return options as O; + } +} + +/// Gets marker Id from a [marker] object. +MarkerId getMarkerId(Object? marker) { + final JSObject object = marker! as JSObject; + final gmaps.MVCObject mapObject = marker as gmaps.MVCObject; + if (object.isA()) { + return MarkerId((mapObject.get('markerId')! as JSString).toDart); + } else if (object.isA()) { + return MarkerId((mapObject.get('id')! as JSString).toDart); + } else { + throw ArgumentError( + 'Must be either a gmaps.Marker or a gmaps.AdvancedMarkerElement', + ); + } } gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { @@ -730,3 +928,15 @@ gmaps.LatLng _pixelToLatLng(gmaps.Map map, int x, int y) { return projection.fromPointToLatLng(point)!; } + +gmaps.CollisionBehavior _markerCollisionBehaviorToGmCollisionBehavior( + MarkerCollisionBehavior markerCollisionBehavior, +) { + return switch (markerCollisionBehavior) { + MarkerCollisionBehavior.requiredDisplay => gmaps.CollisionBehavior.REQUIRED, + MarkerCollisionBehavior.optionalAndHidesLowerPriority => + gmaps.CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY, + MarkerCollisionBehavior.requiredAndHidesOptional => + gmaps.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL, + }; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index 4588c4717ba..0d4a90b8571 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -38,11 +38,54 @@ class GoogleMapController { _heatmapsController = HeatmapsController(); _polygonsController = PolygonsController(stream: _streamController); _polylinesController = PolylinesController(stream: _streamController); - _clusterManagersController = - ClusterManagersController(stream: _streamController); - _markersController = MarkersController( - stream: _streamController, - clusterManagersController: _clusterManagersController!); + + // Check if all markers are of the same type. Mixing marker types is not + // allowed. + final Set markerTypes = + _markers.map((Marker e) => e.runtimeType).toSet(); + if (markerTypes.isNotEmpty) { + assert(markerTypes.length == 1, 'All markers must be of the same type.'); + + switch (mapConfiguration.markerType) { + case null: + case MarkerType.marker: + assert( + markerTypes.first == Marker, + 'All markers must be of type Marker because ' + 'mapConfiguration.markerType is MarkerType.marker', + ); + case MarkerType.advancedMarker: + assert( + markerTypes.first == AdvancedMarker, + 'All markers must be of type AdvancedMarker because ' + 'mapConfiguration.markerType is MarkerType.advanced', + ); + } + } + + // Advanced and legacy markers are handled differently so markers controller + // and cluster manager need be initialized with the correct marker type. + _clusterManagersController = switch (mapConfiguration.markerType) { + null || + MarkerType.marker => + ClusterManagersController(stream: _streamController), + MarkerType.advancedMarker => + ClusterManagersController( + stream: _streamController), + }; + _markersController = switch (mapConfiguration.markerType) { + null || MarkerType.marker => LegacyMarkersController( + stream: stream, + clusterManagersController: _clusterManagersController! + as ClusterManagersController, + ), + MarkerType.advancedMarker => AdvancedMarkersController( + stream: stream, + clusterManagersController: clusterManagersController! + as ClusterManagersController, + ), + }; + _tileOverlaysController = TileOverlaysController(); _groundOverlaysController = GroundOverlaysController(stream: _streamController); @@ -132,8 +175,8 @@ class GoogleMapController { HeatmapsController? _heatmapsController; PolygonsController? _polygonsController; PolylinesController? _polylinesController; - MarkersController? _markersController; - ClusterManagersController? _clusterManagersController; + MarkersController? _markersController; + ClusterManagersController? _clusterManagersController; TileOverlaysController? _tileOverlaysController; GroundOverlaysController? _groundOverlaysController; @@ -145,7 +188,7 @@ class GoogleMapController { /// The ClusterManagersController of this Map. Only for integration testing. @visibleForTesting - ClusterManagersController? get clusterManagersController => + ClusterManagersController? get clusterManagersController => _clusterManagersController; /// The GroundOverlaysController of this Map. Only for integration testing. @@ -158,12 +201,12 @@ class GoogleMapController { void debugSetOverrides({ DebugCreateMapFunction? createMap, DebugSetOptionsFunction? setOptions, - MarkersController? markers, + MarkersController? markers, CirclesController? circles, HeatmapsController? heatmaps, PolygonsController? polygons, PolylinesController? polylines, - ClusterManagersController? clusterManagers, + ClusterManagersController? clusterManagers, TileOverlaysController? tileOverlays, GroundOverlaysController? groundOverlays, }) { @@ -421,7 +464,7 @@ class GoogleMapController { await Future.value(_googleMap!.bounds) ?? _nullGmapsLatLngBounds; - return gmLatLngBoundsTolatLngBounds(bounds); + return gmLatLngBoundsToLatLngBounds(bounds); } /// Returns the [ScreenCoordinate] for a given viewport [LatLng]. @@ -571,6 +614,13 @@ class GoogleMapController { return _markersController?.isInfoWindowShown(markerId) ?? false; } + /// Returns true if this map supports [AdvancedMarker]s. + bool isAdvancedMarkersAvailable() { + assert(_googleMap != null, 'Cannot get map capabilities of a null map.'); + + return _googleMap!.mapCapabilities.isAdvancedMarkersAvailable ?? false; + } + // Cleanup /// Disposes of this controller and its resources. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index d205a747690..fc173ec913b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -319,6 +319,12 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { return _map(mapId).lastStyleError; } + @override + Future isAdvancedMarkersAvailable({required int mapId}) async { + final GoogleMapController map = _map(mapId); + return map.isAdvancedMarkersAvailable(); + } + /// Disposes of the current map. It can't be used afterwards! @override void dispose({required int mapId}) { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart index d5a23c434a2..f5b3d27f020 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart @@ -15,8 +15,8 @@ import 'marker_clustering.dart'; typedef ConfigurationProvider = MapConfiguration Function(int mapId); /// Function that gets the [ClusterManagersController] for a given `mapId`. -typedef ClusterManagersControllerProvider = ClusterManagersController? Function( - int mapId); +typedef ClusterManagersControllerProvider = ClusterManagersController? + Function(int mapId); /// Function that gets the [GroundOverlaysController] for a given `mapId`. typedef GroundOverlaysControllerProvider = GroundOverlaysController? Function( @@ -105,7 +105,7 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { Uint8List.fromList([0]), bitmapScaling: MapBitmapScaling.none, ), - bounds: gmLatLngBoundsTolatLngBounds(groundOverlay.bounds), + bounds: gmLatLngBoundsToLatLngBounds(groundOverlay.bounds), transparency: 1.0 - groundOverlay.opacity, visible: groundOverlay.map != null, clickable: clickable != null && (clickable as JSBoolean).toDart); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart index 518dce6de77..8f0fb5f429a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart @@ -4,11 +4,15 @@ part of '../google_maps_flutter_web.dart'; -/// The `MarkerController` class wraps a [gmaps.Marker], how it handles events, and its associated (optional) [gmaps.InfoWindow] widget. -class MarkerController { - /// Creates a `MarkerController`, which wraps a [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its associated [gmaps.InfoWindow]. +/// The `MarkerController` class wraps a [gmaps.AdvancedMarkerElement] +/// or [gmaps.Marker], how it handles events, and its associated (optional) +/// [gmaps.InfoWindow] widget. +abstract class MarkerController { + /// Creates a `MarkerController`, which wraps a [gmaps.AdvancedMarkerElement] + /// or [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its + /// associated [gmaps.InfoWindow]. MarkerController({ - required gmaps.Marker marker, + required T marker, gmaps.InfoWindow? infoWindow, bool consumeTapEvents = false, LatLngCallback? onDragStart, @@ -20,32 +24,16 @@ class MarkerController { _infoWindow = infoWindow, _consumeTapEvents = consumeTapEvents, _clusterManagerId = clusterManagerId { - if (onTap != null) { - marker.onClick.listen((gmaps.MapMouseEvent event) { - onTap.call(); - }); - } - if (onDragStart != null) { - marker.onDragstart.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDragStart.call(event.latLng ?? _nullGmapsLatLng); - }); - } - if (onDrag != null) { - marker.onDrag.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDrag.call(event.latLng ?? _nullGmapsLatLng); - }); - } - if (onDragEnd != null) { - marker.onDragend.listen((gmaps.MapMouseEvent event) { - marker.position = event.latLng; - onDragEnd.call(event.latLng ?? _nullGmapsLatLng); - }); - } + initializeMarkerListener( + marker: marker, + onDragStart: onDragStart, + onDrag: onDrag, + onDragEnd: onDragEnd, + onTap: onTap, + ); } - gmaps.Marker? _marker; + T? _marker; final bool _consumeTapEvents; @@ -64,56 +52,222 @@ class MarkerController { /// Returns [ClusterManagerId] if marker belongs to cluster. ClusterManagerId? get clusterManagerId => _clusterManagerId; - /// Returns the [gmaps.Marker] associated to this controller. - gmaps.Marker? get marker => _marker; + /// Returns the marker associated to this controller. + T? get marker => _marker; /// Returns the [gmaps.InfoWindow] associated to the marker. @visibleForTesting gmaps.InfoWindow? get infoWindow => _infoWindow; - /// Updates the options of the wrapped [gmaps.Marker] object. + /// Updates the options of the wrapped marker object. /// /// This cannot be called after [remove]. void update( - gmaps.MarkerOptions options, { + O options, { HTMLElement? newInfoWindowContent, + }); + + /// Initializes the listener for the wrapped marker object. + void initializeMarkerListener({ + required T marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, + }); + + /// Disposes of the currently wrapped marker object. + void remove(); + + /// Hide the associated [gmaps.InfoWindow]. + /// + /// This cannot be called after [remove]. + void hideInfoWindow() { + assert(_marker != null, 'Cannot `hideInfoWindow` on a `remove`d Marker.'); + if (_infoWindow != null) { + _infoWindow.close(); + _infoWindowShown = false; + } + } + + /// Show the associated [gmaps.InfoWindow]. + /// + /// This cannot be called after [remove]. + void showInfoWindow(); +} + +/// A `MarkerController` that wraps a [gmaps.Marker] object. +/// +/// [gmaps.Marker] is a legacy class that is being replaced +/// by [gmaps.AdvancedMarkerElement]. +class LegacyMarkerController + extends MarkerController { + /// Creates a `LegacyMarkerController`, which wraps a [gmaps.Marker] object. + LegacyMarkerController({ + required super.marker, + super.infoWindow, + super.consumeTapEvents, + super.onDragStart, + super.onDrag, + super.onDragEnd, + super.onTap, + super.clusterManagerId, + }); + + @override + void initializeMarkerListener({ + required gmaps.Marker marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, }) { - assert(_marker != null, 'Cannot `update` Marker after calling `remove`.'); - _marker!.options = options; - if (_infoWindow != null && newInfoWindowContent != null) { - _infoWindow.content = newInfoWindowContent; + if (onTap != null) { + marker.onClick.listen((gmaps.MapMouseEvent event) { + onTap.call(); + }); + } + if (onDragStart != null) { + marker.onDragstart.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragStart.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDrag != null) { + marker.onDrag.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDrag.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDragEnd != null) { + marker.onDragend.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragEnd.call(event.latLng ?? _nullGmapsLatLng); + }); } } - /// Disposes of the currently wrapped [gmaps.Marker]. + @override void remove() { if (_marker != null) { _infoWindowShown = false; - _marker!.visible = false; _marker!.map = null; _marker = null; } } - /// Hide the associated [gmaps.InfoWindow]. - /// - /// This cannot be called after [remove]. - void hideInfoWindow() { - assert(_marker != null, 'Cannot `hideInfoWindow` on a `remove`d Marker.'); + @override + void showInfoWindow() { + assert(_marker != null, 'Cannot `showInfoWindow` on a `remove`d Marker.'); if (_infoWindow != null) { - _infoWindow.close(); + _infoWindow.open(_marker!.map, _marker); + _infoWindowShown = true; + } + } + + @override + void update(gmaps.MarkerOptions options, + {web.HTMLElement? newInfoWindowContent}) { + assert(_marker != null, 'Cannot `update` Marker after calling `remove`.'); + _marker!.options = options; + + if (_infoWindow != null && newInfoWindowContent != null) { + _infoWindow.content = newInfoWindowContent; + } + } +} + +/// A `MarkerController` that wraps a [gmaps.AdvancedMarkerElement] object. +/// +/// [gmaps.AdvancedMarkerElement] is a new class that is +/// replacing [gmaps.Marker]. +class AdvancedMarkerController extends MarkerController< + gmaps.AdvancedMarkerElement, gmaps.AdvancedMarkerElementOptions> { + /// Creates a `AdvancedMarkerController`, which wraps + /// a [gmaps.AdvancedMarkerElement] object. + AdvancedMarkerController({ + required super.marker, + super.infoWindow, + super.consumeTapEvents, + super.onDragStart, + super.onDrag, + super.onDragEnd, + super.onTap, + super.clusterManagerId, + }); + + @override + void initializeMarkerListener({ + required gmaps.AdvancedMarkerElement marker, + required LatLngCallback? onDragStart, + required LatLngCallback? onDrag, + required LatLngCallback? onDragEnd, + required VoidCallback? onTap, + }) { + if (onTap != null) { + marker.onClick.listen((gmaps.MapMouseEvent event) { + onTap.call(); + }); + } + if (onDragStart != null) { + marker.onDragstart.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragStart.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDrag != null) { + marker.onDrag.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDrag.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDragEnd != null) { + marker.onDragend.listen((gmaps.MapMouseEvent event) { + marker.position = event.latLng; + onDragEnd.call(event.latLng ?? _nullGmapsLatLng); + }); + } + } + + @override + void remove() { + if (_marker != null) { _infoWindowShown = false; + + _marker!.remove(); + _marker!.map = null; + _marker = null; } } - /// Show the associated [gmaps.InfoWindow]. - /// - /// This cannot be called after [remove]. + @override void showInfoWindow() { assert(_marker != null, 'Cannot `showInfoWindow` on a `remove`d Marker.'); + if (_infoWindow != null) { _infoWindow.open(_marker!.map, _marker); _infoWindowShown = true; } } + + @override + void update( + gmaps.AdvancedMarkerElementOptions options, { + web.HTMLElement? newInfoWindowContent, + }) { + assert(_marker != null, 'Cannot `update` Marker after calling `remove`.'); + + final gmaps.AdvancedMarkerElement marker = _marker!; + marker.collisionBehavior = options.collisionBehavior; + marker.content = options.content; + marker.gmpClickable = options.gmpClickable; + marker.gmpDraggable = options.gmpDraggable; + marker.position = options.position; + marker.title = options.title ?? ''; + marker.zIndex = options.zIndex; + + if (_infoWindow != null && newInfoWindowContent != null) { + _infoWindow.content = newInfoWindowContent; + } + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart index 8dbb4308ba6..0d3d83df4a9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering.dart @@ -17,7 +17,10 @@ import 'types.dart'; /// This class maps [ClusterManager] objects to javascript [MarkerClusterer] /// objects and provides an interface for adding and removing markers from /// clusters. -class ClusterManagersController extends GeometryController { +/// +/// [T] must extend [JSObject]. It's not specified in code because our mocking +/// framework does not support mocking JSObjects. +class ClusterManagersController extends GeometryController { /// Creates a new [ClusterManagersController] instance. /// /// The [stream] parameter is a required [StreamController] used for @@ -26,13 +29,13 @@ class ClusterManagersController extends GeometryController { {required StreamController> stream}) : _streamController = stream, _clusterManagerIdToMarkerClusterer = - {}; + >{}; // The stream over which cluster managers broadcast their events final StreamController> _streamController; // A cache of [MarkerClusterer]s indexed by their [ClusterManagerId]. - final Map + final Map> _clusterManagerIdToMarkerClusterer; /// Adds a set of [ClusterManager] objects to the cache. @@ -41,12 +44,12 @@ class ClusterManagersController extends GeometryController { } void _addClusterManager(ClusterManager clusterManager) { - final MarkerClusterer markerClusterer = createMarkerClusterer( - googleMap, - (gmaps.MapMouseEvent event, MarkerClustererCluster cluster, - gmaps.Map map) => - _clusterClicked( - clusterManager.clusterManagerId, event, cluster, map)); + final MarkerClusterer markerClusterer = createMarkerClusterer( + googleMap, + (gmaps.MapMouseEvent event, MarkerClustererCluster cluster, + gmaps.Map map) => + _clusterClicked(clusterManager.clusterManagerId, event, cluster, map), + ); _clusterManagerIdToMarkerClusterer[clusterManager.clusterManagerId] = markerClusterer; @@ -59,7 +62,7 @@ class ClusterManagersController extends GeometryController { } void _removeClusterManager(ClusterManagerId clusterManagerId) { - final MarkerClusterer? markerClusterer = + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { markerClusterer.clearMarkers(true); @@ -68,10 +71,12 @@ class ClusterManagersController extends GeometryController { _clusterManagerIdToMarkerClusterer.remove(clusterManagerId); } - /// Adds given [gmaps.Marker] to the [MarkerClusterer] with given - /// [ClusterManagerId]. - void addItem(ClusterManagerId clusterManagerId, gmaps.Marker marker) { - final MarkerClusterer? markerClusterer = + /// Adds given markers to the [MarkerClusterer] with given [ClusterManagerId]. + void addItem( + ClusterManagerId clusterManagerId, + T marker, + ) { + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { markerClusterer.addMarker(marker, true); @@ -79,11 +84,14 @@ class ClusterManagersController extends GeometryController { } } - /// Removes given [gmaps.Marker] from the [MarkerClusterer] with given - /// [ClusterManagerId]. - void removeItem(ClusterManagerId clusterManagerId, gmaps.Marker? marker) { + /// Removes given marker from the [MarkerClusterer] with + /// given [ClusterManagerId]. + void removeItem( + ClusterManagerId clusterManagerId, + T? marker, + ) { if (marker != null) { - final MarkerClusterer? markerClusterer = + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { markerClusterer.removeMarker(marker, true); @@ -95,11 +103,11 @@ class ClusterManagersController extends GeometryController { /// Returns list of clusters in [MarkerClusterer] with given /// [ClusterManagerId]. List getClusters(ClusterManagerId clusterManagerId) { - final MarkerClusterer? markerClusterer = + final MarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { return markerClusterer.clusters - .map((MarkerClustererCluster cluster) => + .map((MarkerClustererCluster cluster) => _convertCluster(clusterManagerId, cluster)) .toList(); } @@ -109,7 +117,7 @@ class ClusterManagersController extends GeometryController { void _clusterClicked( ClusterManagerId clusterManagerId, gmaps.MapMouseEvent event, - MarkerClustererCluster markerClustererCluster, + MarkerClustererCluster markerClustererCluster, gmaps.Map map) { if (markerClustererCluster.count > 0 && markerClustererCluster.bounds != null) { @@ -121,15 +129,13 @@ class ClusterManagersController extends GeometryController { /// Converts [MarkerClustererCluster] to [Cluster]. Cluster _convertCluster(ClusterManagerId clusterManagerId, - MarkerClustererCluster markerClustererCluster) { + MarkerClustererCluster markerClustererCluster) { final LatLng position = gmLatLngToLatLng(markerClustererCluster.position); final LatLngBounds bounds = - gmLatLngBoundsTolatLngBounds(markerClustererCluster.bounds!); + gmLatLngBoundsToLatLngBounds(markerClustererCluster.bounds!); + final List markerIds = + markerClustererCluster.markers.map(getMarkerId).toList(); - final List markerIds = markerClustererCluster.markers - .map((gmaps.Marker marker) => - MarkerId((marker.get('markerId')! as JSString).toDart)) - .toList(); return Cluster( clusterManagerId, markerIds, diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart index 80e7e5fc1c1..130c0774abd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker_clustering_js_interop.dart @@ -13,9 +13,9 @@ import 'dart:js_interop'; import 'package:google_maps/google_maps.dart' as gmaps; /// A typedef representing a callback function for handling cluster tap events. -typedef ClusterClickHandler = void Function( +typedef ClusterClickHandler = void Function( gmaps.MapMouseEvent, - MarkerClustererCluster, + MarkerClustererCluster, gmaps.Map, ); @@ -24,18 +24,18 @@ typedef ClusterClickHandler = void Function( /// See: https://googlemaps.github.io/js-markerclusterer/interfaces/MarkerClustererOptions.html @JS() @anonymous -extension type MarkerClustererOptions._(JSObject _) implements JSObject { +extension type MarkerClustererOptions._(JSObject _) implements JSObject { /// Constructs a new [MarkerClustererOptions] object. factory MarkerClustererOptions({ gmaps.Map? map, - List? markers, - ClusterClickHandler? onClusterClick, + List? markers, + ClusterClickHandler? onClusterClick, }) => - MarkerClustererOptions._js( + MarkerClustererOptions._js( map: map as JSAny?, markers: markers?.cast().toJS ?? JSArray(), onClusterClick: onClusterClick != null - ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => + ? ((JSAny event, MarkerClustererCluster cluster, JSAny map) => onClusterClick(event as gmaps.MapMouseEvent, cluster, map as gmaps.Map)).toJS : null, @@ -52,13 +52,13 @@ extension type MarkerClustererOptions._(JSObject _) implements JSObject { @JS('map') external JSAny? get _map; - /// Returns the list of [gmaps.Marker] objects. - List? get markers => _markers?.toDart.cast(); + /// Returns the list of marker objects. + List? get markers => _markers?.toDart.cast(); @JS('markers') external JSArray? get _markers; /// Returns the onClusterClick handler. - ClusterClickHandler? get onClusterClick => + ClusterClickHandler? get onClusterClick => _onClusterClick?.toDart as ClusterClickHandler?; @JS('onClusterClick') external JSExportedDartFunction? get _onClusterClick; @@ -68,14 +68,14 @@ extension type MarkerClustererOptions._(JSObject _) implements JSObject { /// /// https://googlemaps.github.io/js-markerclusterer/classes/Cluster.html @JS('markerClusterer.Cluster') -extension type MarkerClustererCluster._(JSObject _) implements JSObject { +extension type MarkerClustererCluster._(JSObject _) implements JSObject { /// Getter for the cluster marker. - gmaps.Marker get marker => _marker as gmaps.Marker; + T get marker => _marker as T; @JS('marker') external JSAny get _marker; /// List of markers in the cluster. - List get markers => _markers.toDart.cast(); + List get markers => _markers.toDart.cast(); @JS('markers') external JSArray get _markers; @@ -96,7 +96,7 @@ extension type MarkerClustererCluster._(JSObject _) implements JSObject { external void delete(); /// Adds a marker to the cluster. - void push(gmaps.Marker marker) => _push(marker as JSAny); + void push(T marker) => _push(marker as JSAny); @JS('push') external void _push(JSAny marker); } @@ -105,30 +105,29 @@ extension type MarkerClustererCluster._(JSObject _) implements JSObject { /// /// https://googlemaps.github.io/js-markerclusterer/classes/MarkerClusterer.html @JS('markerClusterer.MarkerClusterer') -extension type MarkerClusterer._(JSObject _) implements JSObject { +extension type MarkerClusterer._(JSObject _) implements JSObject { /// Constructs a new [MarkerClusterer] object. - external MarkerClusterer(MarkerClustererOptions options); + external MarkerClusterer(MarkerClustererOptions options); /// Adds a marker to be clustered by the [MarkerClusterer]. - void addMarker(gmaps.Marker marker, bool? noDraw) => - _addMarker(marker as JSAny, noDraw); + void addMarker(T marker, bool? noDraw) => _addMarker(marker as JSAny, noDraw); @JS('addMarker') external void _addMarker(JSAny marker, bool? noDraw); /// Adds a list of markers to be clustered by the [MarkerClusterer]. - void addMarkers(List? markers, bool? noDraw) => + void addMarkers(List? markers, bool? noDraw) => _addMarkers(markers?.cast().toJS, noDraw); @JS('addMarkers') external void _addMarkers(JSArray? markers, bool? noDraw); /// Removes a marker from the [MarkerClusterer]. - bool removeMarker(gmaps.Marker marker, bool? noDraw) => + bool removeMarker(T marker, bool? noDraw) => _removeMarker(marker as JSAny, noDraw); @JS('removeMarker') external bool _removeMarker(JSAny marker, bool? noDraw); /// Removes a list of markers from the [MarkerClusterer]. - bool removeMarkers(List? markers, bool? noDraw) => + bool removeMarkers(List? markers, bool? noDraw) => _removeMarkers(markers?.cast().toJS, noDraw); @JS('removeMarkers') external bool _removeMarkers(JSArray? markers, bool? noDraw); @@ -143,8 +142,8 @@ extension type MarkerClusterer._(JSObject _) implements JSObject { external void onRemove(); /// Returns the list of clusters. - List get clusters => - _clusters.toDart.cast(); + List> get clusters => + _clusters.toDart.cast>(); @JS('clusters') external JSArray get _clusters; @@ -154,11 +153,11 @@ extension type MarkerClusterer._(JSObject _) implements JSObject { /// Creates [MarkerClusterer] object with given [gmaps.Map] and /// [ClusterClickHandler]. -MarkerClusterer createMarkerClusterer( - gmaps.Map map, ClusterClickHandler onClusterClickHandler) { - final MarkerClustererOptions options = MarkerClustererOptions( +MarkerClusterer createMarkerClusterer( + gmaps.Map map, ClusterClickHandler onClusterClickHandler) { + final MarkerClustererOptions options = MarkerClustererOptions( map: map, onClusterClick: onClusterClickHandler, ); - return MarkerClusterer(options); + return MarkerClusterer(options); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart index f5204403528..b4599bbecfa 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -5,26 +5,34 @@ part of '../google_maps_flutter_web.dart'; /// This class manages a set of [MarkerController]s associated to a [GoogleMapController]. -class MarkersController extends GeometryController { +/// +/// * [LegacyMarkersController] implements the [MarkersController] for the +/// legacy [gmaps.Marker] class. +/// * [AdvancedMarkersController] implements the [MarkersController] for the +/// advanced [gmaps.AdvancedMarkerElement] class. +/// +/// [T] must extend [JSObject]. It's not specified in code because our mocking +/// framework does not support mocking JSObjects. +abstract class MarkersController extends GeometryController { /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. MarkersController({ required StreamController> stream, - required ClusterManagersController clusterManagersController, + required ClusterManagersController clusterManagersController, }) : _streamController = stream, _clusterManagersController = clusterManagersController, - _markerIdToController = {}; + _markerIdToController = >{}; // A cache of [MarkerController]s indexed by their [MarkerId]. - final Map _markerIdToController; + final Map> _markerIdToController; // The stream over which markers broadcast their events final StreamController> _streamController; - final ClusterManagersController _clusterManagersController; + final ClusterManagersController _clusterManagersController; /// Returns the cache of [MarkerController]s. Test only. @visibleForTesting - Map get markers => _markerIdToController; + Map> get markers => _markerIdToController; /// Adds a set of [Marker] objects to the cache. /// @@ -52,51 +60,36 @@ class MarkersController extends GeometryController { } } - final gmaps.Marker? currentMarker = - _markerIdToController[marker.markerId]?.marker; - - final gmaps.MarkerOptions markerOptions = + final MarkerController? markerController = + _markerIdToController[marker.markerId]; + final T? currentMarker = markerController?.marker; + final O markerOptions = await _markerOptionsFromMarker(marker, currentMarker); - - final gmaps.Marker gmMarker = gmaps.Marker(markerOptions); - - gmMarker.set('markerId', marker.markerId.value.toJS); - - if (marker.clusterManagerId != null) { - _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker); - } else { - gmMarker.map = googleMap; - } - - final MarkerController controller = MarkerController( - marker: gmMarker, - clusterManagerId: marker.clusterManagerId, - infoWindow: gmInfoWindow, - consumeTapEvents: marker.consumeTapEvents, - onTap: () { - showMarkerInfoWindow(marker.markerId); - _onMarkerTap(marker.markerId); - }, - onDragStart: (gmaps.LatLng latLng) { - _onMarkerDragStart(marker.markerId, latLng); - }, - onDrag: (gmaps.LatLng latLng) { - _onMarkerDrag(marker.markerId, latLng); - }, - onDragEnd: (gmaps.LatLng latLng) { - _onMarkerDragEnd(marker.markerId, latLng); - }, - ); + final MarkerController controller = + await createMarkerController(marker, markerOptions, gmInfoWindow); _markerIdToController[marker.markerId] = controller; } + /// Creates a [MarkerController] for the given [marker]. + /// + /// [markerOptions] contains configuration that should be used to create + /// a [gmaps.Marker] or [gmaps.AdvancedMarkerElement] object. [markersOptions] + /// is either [gmaps.MarkerOptions] or [gmaps.AdvancedMarkerElementOptions]. + /// + /// [gmInfoWindow] is marker's info window to show on tap. + Future> createMarkerController( + Marker marker, + O markerOptions, + gmaps.InfoWindow? gmInfoWindow, + ); + /// Updates a set of [Marker] objects with new options. Future changeMarkers(Set markersToChange) async { await Future.wait(markersToChange.map(_changeMarker)); } Future _changeMarker(Marker marker) async { - final MarkerController? markerController = + final MarkerController? markerController = _markerIdToController[marker.markerId]; if (markerController != null) { final ClusterManagerId? oldClusterManagerId = @@ -108,8 +101,7 @@ class MarkersController extends GeometryController { _removeMarker(marker.markerId); await _addMarker(marker); } else { - final gmaps.MarkerOptions markerOptions = - await _markerOptionsFromMarker( + final O markerOptions = await _markerOptionsFromMarker( marker, markerController.marker, ); @@ -129,7 +121,8 @@ class MarkersController extends GeometryController { } void _removeMarker(MarkerId markerId) { - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; if (markerController?.clusterManagerId != null) { _clusterManagersController.removeItem( markerController!.clusterManagerId!, markerController.marker); @@ -145,7 +138,8 @@ class MarkersController extends GeometryController { /// See also [hideMarkerInfoWindow] and [isInfoWindowShown]. void showMarkerInfoWindow(MarkerId markerId) { _hideAllMarkerInfoWindow(); - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; markerController?.showInfoWindow(); } @@ -153,7 +147,8 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [isInfoWindowShown]. void hideMarkerInfoWindow(MarkerId markerId) { - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; markerController?.hideInfoWindow(); } @@ -161,7 +156,8 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [hideMarkerInfoWindow]. bool isInfoWindowShown(MarkerId markerId) { - final MarkerController? markerController = _markerIdToController[markerId]; + final MarkerController? markerController = + _markerIdToController[markerId]; return markerController?.infoWindowShown ?? false; } @@ -204,10 +200,106 @@ class MarkersController extends GeometryController { void _hideAllMarkerInfoWindow() { _markerIdToController.values - .where((MarkerController? controller) => + .where((MarkerController? controller) => controller?.infoWindowShown ?? false) - .forEach((MarkerController controller) { + .forEach((MarkerController controller) { controller.hideInfoWindow(); }); } } + +/// A [MarkersController] for the legacy [gmaps.Marker] class. +class LegacyMarkersController + extends MarkersController { + /// Initialize the markers controller for the legacy [gmaps.Marker] class. + LegacyMarkersController({ + required super.stream, + required super.clusterManagersController, + }); + + @override + Future createMarkerController( + Marker marker, + gmaps.MarkerOptions markerOptions, + gmaps.InfoWindow? gmInfoWindow, + ) async { + final gmaps.Marker gmMarker = gmaps.Marker(markerOptions); + gmMarker.set('markerId', marker.markerId.value.toJS); + + if (marker.clusterManagerId != null) { + _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker); + } else { + gmMarker.map = googleMap; + } + + return LegacyMarkerController( + marker: gmMarker, + clusterManagerId: marker.clusterManagerId, + infoWindow: gmInfoWindow, + consumeTapEvents: marker.consumeTapEvents, + onTap: () { + showMarkerInfoWindow(marker.markerId); + _onMarkerTap(marker.markerId); + }, + onDragStart: (gmaps.LatLng latLng) { + _onMarkerDragStart(marker.markerId, latLng); + }, + onDrag: (gmaps.LatLng latLng) { + _onMarkerDrag(marker.markerId, latLng); + }, + onDragEnd: (gmaps.LatLng latLng) { + _onMarkerDragEnd(marker.markerId, latLng); + }, + ); + } +} + +/// A [MarkersController] for the advanced [gmaps.AdvancedMarkerElement] class. +class AdvancedMarkersController extends MarkersController< + gmaps.AdvancedMarkerElement, gmaps.AdvancedMarkerElementOptions> { + /// Initialize the markers controller for advanced markers + /// ([gmaps.AdvancedMarkerElement]). + AdvancedMarkersController({ + required super.stream, + required super.clusterManagersController, + }); + + @override + Future createMarkerController( + Marker marker, + gmaps.AdvancedMarkerElementOptions markerOptions, + gmaps.InfoWindow? gmInfoWindow, + ) async { + assert(marker is AdvancedMarker, 'Marker must be an AdvancedMarker.'); + + final gmaps.AdvancedMarkerElement gmMarker = + gmaps.AdvancedMarkerElement(markerOptions); + gmMarker.setAttribute('id', marker.markerId.value); + + if (marker.clusterManagerId != null) { + _clusterManagersController.addItem(marker.clusterManagerId!, gmMarker); + } else { + gmMarker.map = googleMap; + } + + return AdvancedMarkerController( + marker: gmMarker, + clusterManagerId: marker.clusterManagerId, + infoWindow: gmInfoWindow, + consumeTapEvents: marker.consumeTapEvents, + onTap: () { + showMarkerInfoWindow(marker.markerId); + _onMarkerTap(marker.markerId); + }, + onDragStart: (gmaps.LatLng latLng) { + _onMarkerDragStart(marker.markerId, latLng); + }, + onDrag: (gmaps.LatLng latLng) { + _onMarkerDrag(marker.markerId, latLng); + }, + onDragEnd: (gmaps.LatLng latLng) { + _onMarkerDragEnd(marker.markerId, latLng); + }, + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 5cbffaec962..73e6901d0da 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: google_maps_flutter_platform_interface: ^2.10.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 - web: ">=0.5.1 <2.0.0" + web: ">=1.0.0 <2.0.0" dev_dependencies: flutter_test: @@ -40,3 +40,8 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {google_maps_flutter_platform_interface: {path: ../../google_maps_flutter/google_maps_flutter_platform_interface}}