diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/advanced_markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/advanced_markers_test.dart index c3868cd5ac6d..7a744153d8a7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/advanced_markers_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/advanced_markers_test.dart @@ -335,6 +335,72 @@ void main() { expect(icon.style.height, '${expectedSize}px'); }); + testWidgets('changeMarkers preserves bitmap icon content when position changes', ( + WidgetTester tester, + ) async { + const markerId = MarkerId('1'); + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final markers = { + AdvancedMarker( + markerId: markerId, + icon: BytesMapBitmap(bytes, imagePixelRatio: 1), + position: const LatLng(1, 2), + ), + }; + await controller.addMarkers(markers); + + final gmaps.AdvancedMarkerElement? marker = controller.markers[markerId]?.marker; + expect(marker, isNotNull); + final icon = marker!.content as HTMLImageElement?; + expect(icon, isNotNull); + final String blobUrl = icon!.src; + + final updatedMarkers = { + AdvancedMarker( + markerId: markerId, + icon: BytesMapBitmap(bytes, imagePixelRatio: 1), + position: const LatLng(42, 54), + ), + }; + await controller.changeMarkers(updatedMarkers); + + final position = marker.position! as gmaps.LatLngLiteral; + expect(position.lat, equals(42)); + expect(position.lng, equals(54)); + expect(icon.isSameNode(marker.content), isTrue); + expect(icon.src, blobUrl); + }); + + testWidgets('changeMarkers replaces bitmap icon content when alpha changes', ( + WidgetTester tester, + ) async { + const markerId = MarkerId('1'); + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final markers = { + AdvancedMarker(markerId: markerId, icon: BytesMapBitmap(bytes, imagePixelRatio: 1)), + }; + await controller.addMarkers(markers); + + final gmaps.AdvancedMarkerElement? marker = controller.markers[markerId]?.marker; + expect(marker, isNotNull); + final icon = marker!.content as HTMLImageElement?; + expect(icon, isNotNull); + + final updatedMarkers = { + AdvancedMarker( + markerId: markerId, + alpha: 0.5, + icon: BytesMapBitmap(bytes, imagePixelRatio: 1), + ), + }; + await controller.changeMarkers(updatedMarkers); + + final updatedIcon = marker.content as HTMLImageElement?; + expect(updatedIcon, isNotNull); + expect(updatedIcon!.isSameNode(icon), isFalse); + expect(updatedIcon.style.opacity, '0.5'); + }); + testWidgets('markers with custom bitmap icon and pixel ratio work', ( WidgetTester tester, ) async { 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 99d7986fe308..aa62e05176e0 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 @@ -689,22 +689,68 @@ Future _gmIconFromBitmapDescriptor( return icon; } +class _AdvancedMarkerContentConfiguration { + _AdvancedMarkerContentConfiguration({ + required this.alpha, + required this.visible, + required this.rotation, + required this.iconHash, + }); + + factory _AdvancedMarkerContentConfiguration.fromMarker(AdvancedMarker marker) { + return _AdvancedMarkerContentConfiguration( + alpha: marker.alpha, + visible: marker.visible, + rotation: marker.rotation, + iconHash: const DeepCollectionEquality().hash(marker.icon.toJson()), + ); + } + + final double alpha; + final bool visible; + final double rotation; + final int iconHash; + + bool isMatchFor(AdvancedMarker marker) { + return alpha == marker.alpha && + visible == marker.visible && + rotation == marker.rotation && + iconHash == const DeepCollectionEquality().hash(marker.icon.toJson()); + } +} + +bool _isAdvancedMarkerContentUpdateRequired( + Marker marker, + _AdvancedMarkerContentConfiguration? previousConfiguration, +) { + if (marker is! AdvancedMarker || previousConfiguration == null) { + return true; + } + return !previousConfiguration.isMatchFor(marker); +} + // 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(Marker marker, T? currentMarker) async { +Future _markerOptionsFromMarker( + Marker marker, + T? currentMarker, { + bool isAdvancedMarkerContentUpdateRequired = true, +}) async { if (marker is AdvancedMarker) { final 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; + if (isAdvancedMarkerContentUpdateRequired) { + options.content = await _advancedMarkerIconFromBitmapDescriptor( + marker.icon, + opacity: marker.alpha, + isVisible: marker.visible, + rotation: marker.rotation, + ); + } return options as O; } else { final options = gmaps.MarkerOptions() 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 d905b01eab3e..267e3e339c48 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 @@ -67,7 +67,11 @@ abstract class MarkerController { /// Updates the options of the wrapped marker object. /// /// This cannot be called after [remove]. - void update(O options, {web.HTMLElement? newInfoWindowContent}); + void update( + O options, { + bool isContentUpdateRequired = true, + web.HTMLElement? newInfoWindowContent, + }); /// Initializes the listener for the wrapped marker object. void addMarkerListener({ @@ -193,7 +197,11 @@ class LegacyMarkerController extends MarkerController extends GeometryController // A cache of [MarkerController]s indexed by their [MarkerId]. final Map> _markerIdToController; + // A cache of the advanced marker content configuration indexed by [MarkerId]. + final Map _contentConfigurationByMarkerId = + {}; + // The stream over which markers broadcast their events final StreamController> _streamController; @@ -103,6 +107,7 @@ abstract class MarkersController extends GeometryController gmInfoWindow, ); _markerIdToController[marker.markerId] = controller; + _updateAdvancedMarkerContentConfiguration(marker); return controller; } @@ -136,16 +141,37 @@ abstract class MarkersController extends GeometryController _removeMarker(marker.markerId); await _addMarker(marker); } else { - final O markerOptions = await _markerOptionsFromMarker(marker, markerController.marker); + final _AdvancedMarkerContentConfiguration? previousContentConfiguration = + _contentConfigurationByMarkerId[marker.markerId]; + final bool isAdvancedMarkerContentUpdateRequired = _isAdvancedMarkerContentUpdateRequired( + marker, + previousContentConfiguration, + ); + final O markerOptions = await _markerOptionsFromMarker( + marker, + markerController.marker, + isAdvancedMarkerContentUpdateRequired: isAdvancedMarkerContentUpdateRequired, + ); final gmaps.InfoWindowOptions? infoWindow = _infoWindowOptionsFromMarker(marker); markerController.update( markerOptions, + isContentUpdateRequired: isAdvancedMarkerContentUpdateRequired, newInfoWindowContent: infoWindow?.content as web.HTMLElement?, ); + _updateAdvancedMarkerContentConfiguration(marker); } } } + void _updateAdvancedMarkerContentConfiguration(Marker marker) { + if (marker is AdvancedMarker) { + _contentConfigurationByMarkerId[marker.markerId] = + _AdvancedMarkerContentConfiguration.fromMarker(marker); + } else { + _contentConfigurationByMarkerId.remove(marker.markerId); + } + } + /// Removes a set of [MarkerId]s from the cache. void removeMarkers(Set markerIdsToRemove) { final List?>> markersControllers = markerIdsToRemove @@ -182,6 +208,7 @@ abstract class MarkersController extends GeometryController for (final markerController in markersControllers) { markerController.value?.remove(); _markerIdToController.remove(markerController.key); + _contentConfigurationByMarkerId.remove(markerController.key); } } @@ -195,6 +222,7 @@ abstract class MarkersController extends GeometryController } markerController?.remove(); _markerIdToController.remove(markerId); + _contentConfigurationByMarkerId.remove(markerId); } // InfoWindow...