diff --git a/packages/google_maps_flutter/google_maps_flutter/example/assets/checkers.png b/packages/google_maps_flutter/google_maps_flutter/example/assets/checkers.png new file mode 100644 index 00000000000..444874d2279 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter/example/assets/checkers.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/bitmap_registry.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/bitmap_registry.dart new file mode 100644 index 00000000000..cb81ed77aad --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/bitmap_registry.dart @@ -0,0 +1,167 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.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'; + +class BitmapRegistryPage extends GoogleMapExampleAppPage { + const BitmapRegistryPage({Key? key}) + : super(const Icon(Icons.speed), 'Bitmap registry', key: key); + + @override + Widget build(BuildContext context) { + return const _BitmapRegistryBody(); + } +} + +// How many markers to place on the map. +const int _numberOfMarkers = 500; + +class _BitmapRegistryBody extends StatefulWidget { + const _BitmapRegistryBody(); + + @override + State<_BitmapRegistryBody> createState() => _BitmapRegistryBodyState(); +} + +class _BitmapRegistryBodyState extends State<_BitmapRegistryBody> { + final Set _markers = {}; + int? _registeredBitmapId; + + @override + void initState() { + super.initState(); + + _registerBitmap(); + } + + @override + void dispose() { + _unregisterBitmap(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 2 / 3, + child: GoogleMap( + markers: _markers, + initialCameraPosition: const CameraPosition( + target: LatLng(0, 0), + zoom: 1.0, + ), + ), + ), + MaterialButton( + onPressed: () async { + // Add markers to the map with a custom bitmap as the icon. + // + // To show potential performance issues: + // * large original image is used (800x600px, ~330KB) + // * bitmap is scaled down to 64x64px + // * bitmap is created once and reused for all markers. This + // doesn't help much because the bitmap is still sent to the + // platform side for each marker. + // + // Adding many markers may result in a performance hit, + // out of memory errors or even app crashes. + final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor(); + _updateMarkers(bitmap); + }, + child: const Text('Add $_numberOfMarkers markers, no registry'), + ), + MaterialButton( + onPressed: _registeredBitmapId == null + ? null + : () { + // Add markers to the map with a custom bitmap as the icon + // placed in the bitmap registry beforehand. + final RegisteredMapBitmap registeredBitmap = + RegisteredMapBitmap(id: _registeredBitmapId!); + _updateMarkers(registeredBitmap); + }, + child: const Text('Add $_numberOfMarkers markers using registry'), + ), + ], + ), + ); + } + + /// Register a bitmap in the bitmap registry. + Future _registerBitmap() async { + if (_registeredBitmapId != null) { + return; + } + + final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor(); + _registeredBitmapId = + await GoogleMapBitmapRegistry.instance.register(bitmap); + + // If the widget was disposed before the bitmap was registered, unregister + // the bitmap. + if (!mounted) { + _unregisterBitmap(); + return; + } + + setState(() {}); + } + + /// Unregister the bitmap from the bitmap registry. + void _unregisterBitmap() { + if (_registeredBitmapId == null) { + return; + } + + GoogleMapBitmapRegistry.instance.unregister(_registeredBitmapId!); + _registeredBitmapId = null; + } + + // Create a set of markers with the given bitmap and update the state with new + // markers. + void _updateMarkers(BitmapDescriptor bitmap) { + final List newMarkers = List.generate( + _numberOfMarkers, + (int id) { + return Marker( + markerId: MarkerId('$id'), + icon: bitmap, + position: LatLng( + Random().nextDouble() * 100 - 50, + Random().nextDouble() * 100 - 50, + ), + ); + }, + ); + + setState(() { + _markers + ..clear() + ..addAll(newMarkers); + }); + } + + /// Load a bitmap from an asset and create a scaled [BytesMapBitmap] from it. + Future _getAssetBitmapDescriptor() async { + final ByteData byteData = await rootBundle.load('assets/checkers.png'); + final Uint8List bytes = byteData.buffer.asUint8List(); + return BitmapDescriptor.bytes( + bytes, + width: 64, + height: 64, + ); + } +} 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 73a47db723e..3ebe35838c9 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 @@ -9,6 +9,7 @@ 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 'animate_camera.dart'; +import 'bitmap_registry.dart'; import 'clustering.dart'; import 'heatmap.dart'; import 'lite_mode.dart'; @@ -47,6 +48,7 @@ final List _allPages = [ const ClusteringPage(), const MapIdPage(), const HeatmapPage(), + const BitmapRegistryPage(), ]; /// MapsDemo is the Main Application. 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 0a949e262bb..95ca78d31db 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_android: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_android}, google_maps_flutter_ios: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}, google_maps_flutter_web: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_web}} diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index 66952bbe062..75cb3b97d19 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -59,3 +59,4 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf part 'src/controller.dart'; part 'src/google_map.dart'; +part 'src/bitmap_registry.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/bitmap_registry.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/bitmap_registry.dart new file mode 100644 index 00000000000..c615c49fb12 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/bitmap_registry.dart @@ -0,0 +1,57 @@ +part of '../google_maps_flutter.dart'; + +/// A bitmap registry. +/// +/// Bitmaps can be created before they are used in markers and then registered +/// with the registry. This allows for more efficient rendering of markers +/// on the map. For example, if multiple markers use the same bitmap, bitmap +/// can be registered once and then reused. This eliminates the need to +/// transfer the bitmap data multiple times to the platform side. +/// +/// Using bitmap registry is optional. +/// +/// Example: +/// ```dart +/// // Register a bitmap +/// final registeredBitmap = await GoogleMapBitmapRegistry.instance.register( +/// Bitmap.fromAsset('assets/image.png'), +/// ); +/// +/// // Use the registered bitmap as marker icon +/// Marker( +/// markerId: MarkerId('markerId'), +/// icon: registeredBitmap, +/// position: LatLng(0, 0) +/// ), +/// ) +/// ``` +class GoogleMapBitmapRegistry { + GoogleMapBitmapRegistry._(); + + /// The singleton instance of [GoogleMapBitmapRegistry]. + static final GoogleMapBitmapRegistry instance = GoogleMapBitmapRegistry._(); + + // The number of registered images. Also used as a unique identifier for each + // registered image. + int _imageCount = 0; + + /// Registers a [bitmap] with the registry. + /// + /// Returns a unique identifier for the registered bitmap. + Future register(MapBitmap bitmap) async { + _imageCount++; + final int id = _imageCount; + await GoogleMapsFlutterPlatform.instance.registerBitmap(id, bitmap); + return id; + } + + /// Unregisters a bitmap with the given [id]. + Future unregister(int id) { + return GoogleMapsFlutterPlatform.instance.unregisterBitmap(id); + } + + /// Unregister all previously registered bitmaps and clear the cache. + Future clear() { + return GoogleMapsFlutterPlatform.instance.clearBitmapCache(); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 6afe27288aa..31a9fcdf207 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: ../../../packages/google_maps_flutter/google_maps_flutter_android}, google_maps_flutter_ios: {path: ../../../packages/google_maps_flutter/google_maps_flutter_ios}, google_maps_flutter_platform_interface: {path: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}, google_maps_flutter_web: {path: ../../../packages/google_maps_flutter/google_maps_flutter_web}} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/bitmap_registry_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/bitmap_registry_test.dart new file mode 100644 index 00000000000..23558a25ff8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/bitmap_registry_test.dart @@ -0,0 +1,63 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'fake_google_maps_flutter_platform.dart'; + +void main() { + late FakeGoogleMapsFlutterPlatform platform; + + setUp(() { + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; + }); + + test('Adding bitmap to registry', () async { + final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20)); + final int id = await GoogleMapBitmapRegistry.instance.register(bitmap); + expect(id, 1); + expect( + platform.bitmapRegistryRecorder.bitmaps, + ['REGISTER $id $bitmap'], + ); + }); + + test('Removing bitmap from registry', () async { + final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20)); + final int id = await GoogleMapBitmapRegistry.instance.register(bitmap); + await GoogleMapBitmapRegistry.instance.unregister(id); + expect( + platform.bitmapRegistryRecorder.bitmaps, + ['REGISTER $id $bitmap', 'UNREGISTER $id'], + ); + }); + + test('Clearing bitmap registry', () async { + final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20)); + final int id1 = await GoogleMapBitmapRegistry.instance.register(bitmap); + final int id2 = await GoogleMapBitmapRegistry.instance.register(bitmap); + expect( + platform.bitmapRegistryRecorder.bitmaps, + ['REGISTER $id1 $bitmap', 'REGISTER $id2 $bitmap'], + ); + + await GoogleMapBitmapRegistry.instance.clear(); + expect( + platform.bitmapRegistryRecorder.bitmaps, + ['REGISTER $id1 $bitmap', 'REGISTER $id2 $bitmap', 'CLEAR CACHE'], + ); + }); + + test('Bitmap ID is incremental', () async { + final BytesMapBitmap bitmap = BytesMapBitmap(Uint8List(20)); + final int id1 = await GoogleMapBitmapRegistry.instance.register(bitmap); + final int id2 = await GoogleMapBitmapRegistry.instance.register(bitmap); + final int id3 = await GoogleMapBitmapRegistry.instance.register(bitmap); + final int id4 = await GoogleMapBitmapRegistry.instance.register(bitmap); + expect(id2, id1 + 1); + expect(id3, id1 + 2); + expect(id4, id1 + 3); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart index df83400c416..5213887d920 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart @@ -20,6 +20,10 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { Map mapInstances = {}; + /// A recorder for the bitmap registry calls. + PlatformBitmapRegistryRecorder bitmapRegistryRecorder = + PlatformBitmapRegistryRecorder(); + PlatformMapStateRecorder get lastCreatedMap => mapInstances[createdIds.last]!; /// Whether to add a small delay to async calls to simulate more realistic @@ -264,6 +268,21 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { return mapEventStreamController.stream.whereType(); } + @override + Future registerBitmap(int id, MapBitmap bitmap) async { + bitmapRegistryRecorder.bitmaps.add('REGISTER $id $bitmap'); + } + + @override + Future unregisterBitmap(int id) async { + bitmapRegistryRecorder.bitmaps.add('UNREGISTER $id'); + } + + @override + Future clearBitmapCache() async { + bitmapRegistryRecorder.bitmaps.add('CLEAR CACHE'); + } + @override void dispose({required int mapId}) { disposed = true; @@ -331,3 +350,11 @@ class PlatformMapStateRecorder { final List clusterManagerUpdates = []; } + +/// A fake implementation of native bitmap registry, which stores all the +/// updates for inspection in tests. +class PlatformBitmapRegistryRecorder { + PlatformBitmapRegistryRecorder(); + + final List bitmaps = []; +} 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 5701825f12d..7a274eac78b 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 @@ -61,16 +61,39 @@ class Convert { public static final String HEATMAP_GRADIENT_COLOR_MAP_SIZE_KEY = "colorMapSize"; private static BitmapDescriptor toBitmapDescriptor( - Messages.PlatformBitmap platformBitmap, AssetManager assetManager, float density) { + Messages.PlatformBitmap platformBitmap, + AssetManager assetManager, + float density, + ImageRegistry imageRegistry) { return toBitmapDescriptor( - platformBitmap, assetManager, density, new BitmapDescriptorFactoryWrapper()); + platformBitmap, + assetManager, + density, + new BitmapDescriptorFactoryWrapper(), + imageRegistry, + null); } - private static BitmapDescriptor toBitmapDescriptor( + /** + * Creates BitmapDescriptor object from Messages.PlatformBitmap data. + * + * @param platformBitmap a PlatformBitmap containing the bitmap data from which to construct a + * BitmapDescriptor. + * @param assetManager An instance of Android's AssetManager which provides access to any raw + * asset files stored in the application's assets directory. + * @param density the density of the display, used to calculate pixel dimensions. + * @param wrapper is an instance of the BitmapDescriptorFactoryWrapper. + * @param imageRegistry An instance of the ImageRegistry class. + * @param flutterInjectorWrapper An instance of the FlutterInjectorWrapper class. + * @return BitmapDescriptor object created from Messages.PlatformBitmap data. + */ + public static BitmapDescriptor toBitmapDescriptor( Messages.PlatformBitmap platformBitmap, AssetManager assetManager, float density, - BitmapDescriptorFactoryWrapper wrapper) { + BitmapDescriptorFactoryWrapper wrapper, + ImageRegistry imageRegistry, + @Nullable FlutterInjectorWrapper flutterInjectorWrapper) { Object bitmap = platformBitmap.getBitmap(); if (bitmap instanceof Messages.PlatformBitmapDefaultMarker) { Messages.PlatformBitmapDefaultMarker typedBitmap = @@ -109,12 +132,21 @@ private static BitmapDescriptor toBitmapDescriptor( if (bitmap instanceof Messages.PlatformBitmapAssetMap) { Messages.PlatformBitmapAssetMap typedBitmap = (Messages.PlatformBitmapAssetMap) bitmap; return getBitmapFromAsset( - typedBitmap, assetManager, density, wrapper, new FlutterInjectorWrapper()); + typedBitmap, + assetManager, + density, + wrapper, + flutterInjectorWrapper != null ? flutterInjectorWrapper : new FlutterInjectorWrapper()); } if (bitmap instanceof Messages.PlatformBitmapBytesMap) { Messages.PlatformBitmapBytesMap typedBitmap = (Messages.PlatformBitmapBytesMap) bitmap; return getBitmapFromBytes(typedBitmap, density, wrapper); } + if (bitmap instanceof Messages.PlatformRegisteredMapBitmap) { + Messages.PlatformRegisteredMapBitmap typedBitmap = + (Messages.PlatformRegisteredMapBitmap) bitmap; + return getBitmapFromRegisteredBitmap(imageRegistry, typedBitmap); + } throw new IllegalArgumentException("PlatformBitmap did not contain a supported subtype."); } @@ -185,6 +217,11 @@ public static BitmapDescriptor getBitmapFromBytes( } } + public static BitmapDescriptor getBitmapFromRegisteredBitmap( + ImageRegistry imageRegistry, Messages.PlatformRegisteredMapBitmap registeredBitmap) { + return imageRegistry.getBitmap(registeredBitmap.getId()); + } + /** * Creates a BitmapDescriptor object from asset, using given details and density. * @@ -588,13 +625,15 @@ static void interpretMarkerOptions( MarkerOptionsSink sink, AssetManager assetManager, float density, - BitmapDescriptorFactoryWrapper wrapper) { + BitmapDescriptorFactoryWrapper wrapper, + ImageRegistry imageRegistry) { sink.setAlpha(marker.getAlpha().floatValue()); sink.setAnchor(marker.getAnchor().getX().floatValue(), marker.getAnchor().getY().floatValue()); sink.setConsumeTapEvents(marker.getConsumeTapEvents()); sink.setDraggable(marker.getDraggable()); sink.setFlat(marker.getFlat()); - sink.setIcon(toBitmapDescriptor(marker.getIcon(), assetManager, density, wrapper)); + sink.setIcon( + toBitmapDescriptor(marker.getIcon(), assetManager, density, wrapper, imageRegistry, null)); interpretInfoWindowOptions(sink, marker.getInfoWindow()); sink.setPosition(toLatLng(marker.getPosition().toList())); sink.setRotation(marker.getRotation().floatValue()); @@ -642,11 +681,12 @@ static String interpretPolylineOptions( Messages.PlatformPolyline polyline, PolylineOptionsSink sink, AssetManager assetManager, + ImageRegistry imageRegistry, float density) { sink.setConsumeTapEvents(polyline.getConsumesTapEvents()); sink.setColor(polyline.getColor().intValue()); - sink.setEndCap(capFromPigeon(polyline.getEndCap(), assetManager, density)); - sink.setStartCap(capFromPigeon(polyline.getStartCap(), assetManager, density)); + sink.setEndCap(capFromPigeon(polyline.getEndCap(), assetManager, imageRegistry, density)); + sink.setStartCap(capFromPigeon(polyline.getStartCap(), assetManager, imageRegistry, density)); sink.setGeodesic(polyline.getGeodesic()); sink.setJointType(jointTypeFromPigeon(polyline.getJointType())); sink.setVisible(polyline.getVisible()); @@ -816,8 +856,12 @@ private static List patternFromPigeon( return pattern; } - private static Cap capFromPigeon( - Messages.PlatformCap cap, AssetManager assetManager, float density) { + @VisibleForTesting + public static Cap capFromPigeon( + Messages.PlatformCap cap, + AssetManager assetManager, + ImageRegistry imageRegistry, + float density) { switch (cap.getType()) { case BUTT_CAP: return new ButtCap(); @@ -830,7 +874,7 @@ private static Cap capFromPigeon( throw new IllegalArgumentException("A Custom Cap must specify a refWidth value."); } return new CustomCap( - toBitmapDescriptor(cap.getBitmapDescriptor(), assetManager, density), + toBitmapDescriptor(cap.getBitmapDescriptor(), assetManager, density, imageRegistry), cap.getRefWidth().floatValue()); } throw new IllegalArgumentException("Unrecognized PlatformCap type: " + cap.getType()); 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 c7abcb2ff94..05778789aba 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 @@ -36,9 +36,11 @@ GoogleMapController build( int id, Context context, BinaryMessenger binaryMessenger, - LifecycleProvider lifecycleProvider) { + LifecycleProvider lifecycleProvider, + ImageRegistry imageRegistry) { final GoogleMapController controller = - new GoogleMapController(id, context, binaryMessenger, lifecycleProvider, options); + new GoogleMapController( + id, context, binaryMessenger, lifecycleProvider, options, imageRegistry); 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 140938ea047..8101abbe9ca 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 @@ -106,13 +106,15 @@ class GoogleMapController private @Nullable String initialMapStyle; private boolean lastSetStyleSucceeded; @VisibleForTesting List initialPadding; + private final ImageRegistry imageRegistry; GoogleMapController( int id, Context context, BinaryMessenger binaryMessenger, LifecycleProvider lifecycleProvider, - GoogleMapOptions options) { + GoogleMapOptions options, + ImageRegistry imageRegistry) { this.id = id; this.context = context; this.options = options; @@ -131,12 +133,15 @@ class GoogleMapController clusterManagersController, assetManager, density, - new Convert.BitmapDescriptorFactoryWrapper()); + new Convert.BitmapDescriptorFactoryWrapper(), + imageRegistry); this.polygonsController = new PolygonsController(flutterApi, density); - this.polylinesController = new PolylinesController(flutterApi, assetManager, density); + this.polylinesController = + new PolylinesController(flutterApi, assetManager, imageRegistry, density); this.circlesController = new CirclesController(flutterApi, density); this.heatmapsController = new HeatmapsController(); this.tileOverlaysController = new TileOverlaysController(flutterApi); + this.imageRegistry = imageRegistry; } // Constructor for testing purposes only @@ -154,7 +159,8 @@ class GoogleMapController PolylinesController polylinesController, CirclesController circlesController, HeatmapsController heatmapController, - TileOverlaysController tileOverlaysController) { + TileOverlaysController tileOverlaysController, + ImageRegistry imageRegistry) { this.id = id; this.context = context; this.binaryMessenger = binaryMessenger; @@ -170,6 +176,7 @@ class GoogleMapController this.circlesController = circlesController; this.heatmapsController = heatmapController; this.tileOverlaysController = tileOverlaysController; + this.imageRegistry = imageRegistry; } @Override @@ -1093,4 +1100,10 @@ public Boolean isLiteModeEnabled() { } return data; } + + @NonNull + @Override + public Boolean hasRegisteredMapBitmap(@NonNull Long id) { + return imageRegistry.getBitmap(id) != null; + } } 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 c1a3496e7f4..a4336c882b1 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 @@ -18,14 +18,19 @@ public class GoogleMapFactory extends PlatformViewFactory { private final BinaryMessenger binaryMessenger; private final LifecycleProvider lifecycleProvider; private final GoogleMapInitializer googleMapInitializer; + private final ImageRegistry imageRegistry; GoogleMapFactory( - BinaryMessenger binaryMessenger, Context context, LifecycleProvider lifecycleProvider) { + BinaryMessenger binaryMessenger, + Context context, + LifecycleProvider lifecycleProvider, + ImageRegistry imageRegistry) { super(Messages.MapsApi.getCodec()); this.binaryMessenger = binaryMessenger; this.lifecycleProvider = lifecycleProvider; this.googleMapInitializer = new GoogleMapInitializer(context, binaryMessenger); + this.imageRegistry = imageRegistry; } @Override @@ -52,6 +57,6 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar builder.setMapId(cloudMapId); } - return builder.build(id, context, binaryMessenger, lifecycleProvider); + return builder.build(id, context, binaryMessenger, lifecycleProvider, imageRegistry); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java index 36b20cfe909..39af4fb612b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java @@ -17,6 +17,8 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; +import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; +import io.flutter.plugins.googlemaps.Messages.ImageRegistryApi; /** * Plugin for controlling a set of GoogleMap views to be shown as overlays on top of the Flutter @@ -36,6 +38,13 @@ public GoogleMapsPlugin() {} @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + final ImageRegistry imageRegistry = + new ImageRegistry( + binding.getApplicationContext().getAssets(), + new BitmapDescriptorFactoryWrapper(), + binding.getApplicationContext().getResources().getDisplayMetrics().density); + ImageRegistryApi.setUp(binding.getBinaryMessenger(), imageRegistry); + binding .getPlatformViewRegistry() .registerViewFactory( @@ -49,11 +58,14 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { public Lifecycle getLifecycle() { return lifecycle; } - })); + }, + imageRegistry)); } @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + ImageRegistryApi.setUp(binding.getBinaryMessenger(), null); + } // ActivityAware diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ImageRegistry.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ImageRegistry.java new file mode 100644 index 00000000000..1e7dd9b9151 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/ImageRegistry.java @@ -0,0 +1,80 @@ +package io.flutter.plugins.googlemaps; + +import android.content.res.AssetManager; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.maps.model.BitmapDescriptor; +import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; +import io.flutter.plugins.googlemaps.Convert.FlutterInjectorWrapper; +import io.flutter.plugins.googlemaps.Messages.ImageRegistryApi; +import io.flutter.plugins.googlemaps.Messages.PlatformBitmap; +import java.util.HashMap; + +/** + * A registry for BitmapDescriptors. + * + *

BitmapDescriptors are created from PlatformBitmaps and stored in the registry. The registry + * allows for the retrieval of BitmapDescriptors by their unique identifier. + */ +class ImageRegistry implements ImageRegistryApi { + private final AssetManager assetManager; + private final BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; + private final float density; + + @VisibleForTesting private FlutterInjectorWrapper flutterInjectorWrapper; + + private final HashMap registry = new HashMap<>(); + + ImageRegistry( + AssetManager assetManager, + BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper, + float density) { + this.assetManager = assetManager; + this.bitmapDescriptorFactoryWrapper = bitmapDescriptorFactoryWrapper; + this.density = density; + } + + @Override + public void addBitmapToCache(@NonNull Long id, PlatformBitmap bitmap) { + if (!(bitmap.getBitmap() instanceof Messages.PlatformBitmapAsset) + && !(bitmap.getBitmap() instanceof Messages.PlatformBitmapAssetMap) + && !(bitmap.getBitmap() instanceof Messages.PlatformBitmapBytesMap)) { + throw new IllegalArgumentException("PlatformBitmap must contain a supported subtype."); + } + + final BitmapDescriptor bitmapDescriptor = + Convert.toBitmapDescriptor( + bitmap, + assetManager, + density, + bitmapDescriptorFactoryWrapper, + this, + flutterInjectorWrapper); + registry.put(id, bitmapDescriptor); + } + + @VisibleForTesting + public void setFlutterInjectorWrapper(FlutterInjectorWrapper flutterInjectorWrapper) { + this.flutterInjectorWrapper = flutterInjectorWrapper; + } + + @Override + public void removeBitmapFromCache(@NonNull Long id) { + registry.remove(id); + } + + @Override + public void clearBitmapCache() { + registry.clear(); + } + + /** + * Retrieves a BitmapDescriptor from the registry using the provided ID. + * + * @param id unique identifier of a previously registered BitmapDescriptor. + * @return the BitmapDescriptor associated with the given ID, or null if no such object exists. + */ + public BitmapDescriptor getBitmap(Long id) { + return registry.get(id); + } +} 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..62c9ae4a606 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 @@ -25,13 +25,15 @@ class MarkersController { private final AssetManager assetManager; private final float density; private final Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; + private final ImageRegistry imageRegistry; MarkersController( @NonNull MapsCallbackApi flutterApi, ClusterManagersController clusterManagersController, AssetManager assetManager, float density, - Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper) { + Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper, + ImageRegistry imageRegistry) { this.markerIdToMarkerBuilder = new HashMap<>(); this.markerIdToController = new HashMap<>(); this.googleMapsMarkerIdToDartMarkerId = new HashMap<>(); @@ -40,6 +42,7 @@ class MarkersController { this.assetManager = assetManager; this.density = density; this.bitmapDescriptorFactoryWrapper = bitmapDescriptorFactoryWrapper; + this.imageRegistry = imageRegistry; } void setCollection(MarkerManager.Collection markerCollection) { @@ -176,7 +179,12 @@ private void addMarker(@NonNull Messages.PlatformMarker marker) { String clusterManagerId = marker.getClusterManagerId(); MarkerBuilder markerBuilder = new MarkerBuilder(markerId, clusterManagerId); Convert.interpretMarkerOptions( - marker, markerBuilder, assetManager, density, bitmapDescriptorFactoryWrapper); + marker, + markerBuilder, + assetManager, + density, + bitmapDescriptorFactoryWrapper, + imageRegistry); addMarker(markerBuilder); } @@ -233,13 +241,23 @@ private void changeMarker(@NonNull Messages.PlatformMarker marker) { // Update marker builder. Convert.interpretMarkerOptions( - marker, markerBuilder, assetManager, density, bitmapDescriptorFactoryWrapper); + marker, + markerBuilder, + assetManager, + density, + bitmapDescriptorFactoryWrapper, + imageRegistry); // Update existing marker on map. MarkerController markerController = markerIdToController.get(markerId); if (markerController != null) { Convert.interpretMarkerOptions( - marker, markerController, assetManager, density, bitmapDescriptorFactoryWrapper); + marker, + markerController, + assetManager, + density, + bitmapDescriptorFactoryWrapper, + imageRegistry); } } } 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 4a9afeed6d0..b8acf57f022 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.6.0), 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; @@ -5820,6 +5820,78 @@ ArrayList toList() { } } + /** + * Pigeon equivalent of a registered bitmap. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformRegisteredMapBitmap { + private @NonNull Long id; + + public @NonNull Long getId() { + return id; + } + + public void setId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"id\" is null."); + } + this.id = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformRegisteredMapBitmap() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformRegisteredMapBitmap that = (PlatformRegisteredMapBitmap) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public static final class Builder { + + private @Nullable Long id; + + @CanIgnoreReturnValue + public @NonNull Builder setId(@NonNull Long setterArg) { + this.id = setterArg; + return this; + } + + public @NonNull PlatformRegisteredMapBitmap build() { + PlatformRegisteredMapBitmap pigeonReturn = new PlatformRegisteredMapBitmap(); + pigeonReturn.setId(id); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(1); + toListResult.add(id); + return toListResult; + } + + static @NonNull PlatformRegisteredMapBitmap fromList( + @NonNull ArrayList pigeonVar_list) { + PlatformRegisteredMapBitmap pigeonResult = new PlatformRegisteredMapBitmap(); + Object id = pigeonVar_list.get(0); + pigeonResult.setId((Long) id); + return pigeonResult; + } + } + private static class PigeonCodec extends StandardMessageCodec { public static final PigeonCodec INSTANCE = new PigeonCodec(); @@ -5942,6 +6014,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { return PlatformBitmapAssetMap.fromList((ArrayList) readValue(buffer)); case (byte) 173: return PlatformBitmapBytesMap.fromList((ArrayList) readValue(buffer)); + case (byte) 174: + return PlatformRegisteredMapBitmap.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -6084,6 +6158,9 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof PlatformBitmapBytesMap) { stream.write(173); writeValue(stream, ((PlatformBitmapBytesMap) value).toList()); + } else if (value instanceof PlatformRegisteredMapBitmap) { + stream.write(174); + writeValue(stream, ((PlatformRegisteredMapBitmap) value).toList()); } else { super.writeValue(stream, value); } @@ -7363,6 +7440,9 @@ public interface MapsInspectorApi { @NonNull List getClusters(@NonNull String clusterManagerId); + @NonNull + Boolean hasRegisteredMapBitmap(@NonNull Long id); + /** The codec used by MapsInspectorApi. */ static @NonNull MessageCodec getCodec() { return PigeonCodec.INSTANCE; @@ -7705,6 +7785,136 @@ static void setUp( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.MapsInspectorApi.hasRegisteredMapBitmap" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + Long idArg = (Long) args.get(0); + try { + Boolean output = api.hasRegisteredMapBitmap(idArg); + wrapped.add(0, output); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** + * API for interacting with the image registry. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface ImageRegistryApi { + /** Adds a bitmap to the cache. */ + void addBitmapToCache(@NonNull Long id, @NonNull PlatformBitmap bitmap); + /** Removes a bitmap from the cache. */ + void removeBitmapFromCache(@NonNull Long id); + /** Clears the bitmap cache. */ + void clearBitmapCache(); + + /** The codec used by ImageRegistryApi. */ + static @NonNull MessageCodec getCodec() { + return PigeonCodec.INSTANCE; + } + /** + * Sets up an instance of `ImageRegistryApi` to handle messages through the `binaryMessenger`. + */ + static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable ImageRegistryApi api) { + setUp(binaryMessenger, "", api); + } + + static void setUp( + @NonNull BinaryMessenger binaryMessenger, + @NonNull String messageChannelSuffix, + @Nullable ImageRegistryApi api) { + messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.ImageRegistryApi.addBitmapToCache" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + Long idArg = (Long) args.get(0); + PlatformBitmap bitmapArg = (PlatformBitmap) args.get(1); + try { + api.addBitmapToCache(idArg, bitmapArg); + wrapped.add(0, null); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.ImageRegistryApi.removeBitmapFromCache" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + Long idArg = (Long) args.get(0); + try { + api.removeBitmapFromCache(idArg); + wrapped.add(0, null); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.ImageRegistryApi.clearBitmapCache" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + api.clearBitmapCache(); + wrapped.add(0, null); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java index 65b53eb7e07..69a83a80a40 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java @@ -22,14 +22,19 @@ class PolylinesController { private GoogleMap googleMap; private final float density; private final AssetManager assetManager; + private final ImageRegistry imageRegistry; PolylinesController( - @NonNull MapsCallbackApi flutterApi, AssetManager assetManager, float density) { + @NonNull MapsCallbackApi flutterApi, + AssetManager assetManager, + ImageRegistry imageRegistry, + float density) { this.assetManager = assetManager; this.polylineIdToController = new HashMap<>(); this.googleMapsPolylineIdToDartPolylineId = new HashMap<>(); this.flutterApi = flutterApi; this.density = density; + this.imageRegistry = imageRegistry; } void setGoogleMap(GoogleMap googleMap) { @@ -74,7 +79,8 @@ boolean onPolylineTap(String googlePolylineId) { private void addPolyline(@NonNull Messages.PlatformPolyline polyline) { PolylineBuilder polylineBuilder = new PolylineBuilder(density); String polylineId = - Convert.interpretPolylineOptions(polyline, polylineBuilder, assetManager, density); + Convert.interpretPolylineOptions( + polyline, polylineBuilder, assetManager, imageRegistry, density); PolylineOptions options = polylineBuilder.build(); addPolyline(polylineId, options, polylineBuilder.consumeTapEvents()); } @@ -91,7 +97,8 @@ private void changePolyline(@NonNull Messages.PlatformPolyline polyline) { String polylineId = polyline.getPolylineId(); PolylineController polylineController = polylineIdToController.get(polylineId); if (polylineController != null) { - Convert.interpretPolylineOptions(polyline, polylineController, assetManager, density); + Convert.interpretPolylineOptions( + polyline, polylineController, assetManager, imageRegistry, density); } } 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 df4208df5e6..680c913beb3 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 @@ -55,6 +55,7 @@ public class ClusterManagersControllerTest { private final float density = 1; @Mock Convert.BitmapDescriptorFactoryWrapper bitmapFactory; + @Mock ImageRegistry imageRegistry; private AutoCloseable mocksClosable; @@ -109,9 +110,9 @@ public void AddClusterManagersAndMarkers() { createPlatformMarker(markerId2, location2, clusterManagerId); Convert.interpretMarkerOptions( - markerData1, markerBuilder1, assetManager, density, bitmapFactory); + markerData1, markerBuilder1, assetManager, density, bitmapFactory, imageRegistry); Convert.interpretMarkerOptions( - markerData2, markerBuilder2, assetManager, density, bitmapFactory); + markerData2, markerBuilder2, assetManager, density, bitmapFactory, imageRegistry); controller.addItem(markerBuilder1); controller.addItem(markerBuilder2); 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 a2733980559..8b32e3a84ff 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 @@ -31,6 +31,8 @@ import android.util.Base64; import androidx.annotation.NonNull; import com.google.android.gms.maps.model.BitmapDescriptor; +import com.google.android.gms.maps.model.Cap; +import com.google.android.gms.maps.model.CustomCap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.maps.android.clustering.algo.StaticCluster; @@ -40,6 +42,9 @@ 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.PlatformBitmap; +import io.flutter.plugins.googlemaps.Messages.PlatformCap; +import io.flutter.plugins.googlemaps.Messages.PlatformCapType; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; @@ -335,6 +340,43 @@ public void GetBitmapFromBytesThrowsErrorIfInvalidImageData() { fail("Expected an IllegalArgumentException to be thrown"); } + @Test + public void GetBitmapFromNotRegisteredBitmap() { + ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + + Messages.PlatformRegisteredMapBitmap bitmap = + new Messages.PlatformRegisteredMapBitmap.Builder().setId(0L).build(); + + BitmapDescriptor result = Convert.getBitmapFromRegisteredBitmap(imageRegistry, bitmap); + Assert.assertNull(result); + } + + @Test + public void GetPreviouslyRegisteredBitmap() { + ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.NONE) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + PlatformBitmap platformBitmap = new PlatformBitmap.Builder().setBitmap(bitmap).build(); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + imageRegistry.addBitmapToCache(0L, platformBitmap); + + Messages.PlatformRegisteredMapBitmap registeredBitmap = + new Messages.PlatformRegisteredMapBitmap.Builder().setId(0L).build(); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + BitmapDescriptor result = + Convert.getBitmapFromRegisteredBitmap(imageRegistry, registeredBitmap); + BitmapDescriptor registryBitmapDescriptor = imageRegistry.getBitmap(0L); + Assert.assertEquals(result, registryBitmapDescriptor); + } + @Test public void interpretMapConfiguration_handlesNulls() { final Messages.PlatformMapConfiguration config = @@ -665,6 +707,39 @@ public void ConvertInterpretHeatmapOptionsReturnsCorrectData() { Assert.assertEquals(idData, id); } + @Test + public void capFromPigeonCreatesCustomCapWithRegisteredBitmap() { + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1f); + + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.NONE) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + PlatformBitmap platformBitmap = new PlatformBitmap.Builder().setBitmap(bitmap).build(); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + imageRegistry.addBitmapToCache(1L, platformBitmap); + Messages.PlatformRegisteredMapBitmap registeredBitmap = + new Messages.PlatformRegisteredMapBitmap.Builder().setId(1L).build(); + + PlatformBitmap platformRegisteredBitmap = + new PlatformBitmap.Builder().setBitmap(registeredBitmap).build(); + final PlatformCap platformCap = + new PlatformCap.Builder() + .setRefWidth(2d) + .setType(PlatformCapType.CUSTOM_CAP) + .setBitmapDescriptor(platformRegisteredBitmap) + .build(); + + final Cap cap = Convert.capFromPigeon(platformCap, assetManager, imageRegistry, 1f); + Assert.assertEquals(cap.getClass(), CustomCap.class); + Assert.assertEquals(((CustomCap) cap).bitmapDescriptor, mockBitmapDescriptor); + } + private InputStream buildImageInputStream() { Bitmap fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 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 48d8b619beb..ca1556cc9a4 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 @@ -51,6 +51,7 @@ public class GoogleMapControllerTest { @Mock CirclesController mockCirclesController; @Mock HeatmapsController mockHeatmapsController; @Mock TileOverlaysController mockTileOverlaysController; + @Mock ImageRegistry imageRegistry; @Before public void before() { @@ -63,7 +64,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, imageRegistry); googleMapController.init(); return googleMapController; } @@ -84,7 +86,8 @@ public GoogleMapController getGoogleMapControllerWithMockedDependencies() { mockPolylinesController, mockCirclesController, mockHeatmapsController, - mockTileOverlaysController); + mockTileOverlaysController, + imageRegistry); googleMapController.init(); return googleMapController; } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ImageRegistryTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ImageRegistryTest.java new file mode 100644 index 00000000000..d532e98b693 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ImageRegistryTest.java @@ -0,0 +1,281 @@ +package io.flutter.plugins.googlemaps; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Build; +import android.util.Base64; +import com.google.android.gms.maps.model.BitmapDescriptor; +import io.flutter.plugins.googlemaps.Convert.BitmapDescriptorFactoryWrapper; +import io.flutter.plugins.googlemaps.Convert.FlutterInjectorWrapper; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(minSdk = Build.VERSION_CODES.LOLLIPOP) +public class ImageRegistryTest { + + @Mock private AssetManager assetManager; + + @Mock private BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; + + @Mock private BitmapDescriptor mockBitmapDescriptor; + + @Mock private BitmapDescriptor mockBitmapDescriptor2; + + @Mock private FlutterInjectorWrapper flutterInjectorWrapper; + + AutoCloseable mockCloseable; + + @Before + public void before() { + mockCloseable = MockitoAnnotations.openMocks(this); + } + + @After + public void tearDown() throws Exception { + mockCloseable.close(); + } + + @Test + public void AddBitmapToCacheRegistersNewBitmap() { + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + Assert.assertNull(imageRegistry.getBitmap(1L)); + + byte[] bmpData = Base64.decode(generateBase64Image(Color.BLACK), Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + Messages.PlatformBitmap platformBitmap = + new Messages.PlatformBitmap.Builder().setBitmap(bitmap).build(); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + imageRegistry.addBitmapToCache(1L, platformBitmap); + + Assert.assertEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor); + } + + @Test + public void AddBitmapToCacheRegistersNewBitmapFromAssets() throws Exception { + String fakeAssetName = "fake_asset_name"; + String fakeAssetKey = "fake_asset_key"; + + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + Assert.assertNull(imageRegistry.getBitmap(1L)); + + Messages.PlatformBitmapAssetMap bitmap = + new Messages.PlatformBitmapAssetMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setWidth(15.0) + .setHeight(15.0) + .setImagePixelRatio(2.0) + .setAssetName(fakeAssetName) + .build(); + Messages.PlatformBitmap platformBitmap = + new Messages.PlatformBitmap.Builder().setBitmap(bitmap).build(); + + when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); + when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + imageRegistry.setFlutterInjectorWrapper(flutterInjectorWrapper); + + imageRegistry.addBitmapToCache(1L, platformBitmap); + Assert.assertEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor); + } + + @Test + public void AddBitmapToCacheReplacesExistingBitmap() { + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + + // Add bitmap 1 + byte[] bmpData = Base64.decode(generateBase64Image(Color.BLACK), Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + Messages.PlatformBitmap platformBitmap = + new Messages.PlatformBitmap.Builder().setBitmap(bitmap).build(); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + imageRegistry.addBitmapToCache(1L, platformBitmap); + Assert.assertEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor); + + // Add bitmap 2 + bmpData = Base64.decode(generateBase64Image(Color.RED), Base64.DEFAULT); + bitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + platformBitmap = new Messages.PlatformBitmap.Builder().setBitmap(bitmap).build(); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor2); + imageRegistry.addBitmapToCache(1L, platformBitmap); + Assert.assertNotEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor); + Assert.assertEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor2); + } + + @Test + public void RemoveBitmapFromCacheRemovesBitmap() { + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + + byte[] bmpData1 = Base64.decode(generateBase64Image(Color.BLACK), Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap1 = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData1) + .build(); + Messages.PlatformBitmap platformBitmap1 = + new Messages.PlatformBitmap.Builder().setBitmap(bitmap1).build(); + + byte[] bmpData2 = Base64.decode(generateBase64Image(Color.BLACK), Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap2 = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData1) + .build(); + Messages.PlatformBitmap platformBitmap2 = + new Messages.PlatformBitmap.Builder().setBitmap(bitmap2).build(); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + imageRegistry.addBitmapToCache(1L, platformBitmap1); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor2); + imageRegistry.addBitmapToCache(2L, platformBitmap2); + + Assert.assertEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor); + Assert.assertEquals(imageRegistry.getBitmap(2L), mockBitmapDescriptor2); + + imageRegistry.removeBitmapFromCache(1L); + Assert.assertNull(imageRegistry.getBitmap(1L)); + Assert.assertEquals(imageRegistry.getBitmap(2L), mockBitmapDescriptor2); + } + + @Test + public void ClearBitmapCacheRemovesAllSavedBitmaps() { + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + byte[] bmpData = Base64.decode(generateBase64Image(Color.BLACK), Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + Messages.PlatformBitmap platformBitmap = + new Messages.PlatformBitmap.Builder().setBitmap(bitmap).build(); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + imageRegistry.addBitmapToCache(1L, platformBitmap); + imageRegistry.addBitmapToCache(2L, platformBitmap); + imageRegistry.addBitmapToCache(3L, platformBitmap); + Assert.assertEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor); + Assert.assertEquals(imageRegistry.getBitmap(2L), mockBitmapDescriptor); + Assert.assertEquals(imageRegistry.getBitmap(3L), mockBitmapDescriptor); + + imageRegistry.clearBitmapCache(); + Assert.assertNull(imageRegistry.getBitmap(1L)); + Assert.assertNull(imageRegistry.getBitmap(2L)); + Assert.assertNull(imageRegistry.getBitmap(3L)); + } + + @Test + public void GetBitmapReturnsNullIfBitmapIsNotAvailable() { + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + Assert.assertNull(imageRegistry.getBitmap(0L)); + } + + @Test + public void GetBitmapReturnsRegisteredBitmap() { + final ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + byte[] bmpData = Base64.decode(generateBase64Image(Color.BLACK), Base64.DEFAULT); + Messages.PlatformBitmapBytesMap bitmap = + new Messages.PlatformBitmapBytesMap.Builder() + .setBitmapScaling(Messages.PlatformMapBitmapScaling.AUTO) + .setImagePixelRatio(2.0) + .setByteData(bmpData) + .build(); + Messages.PlatformBitmap platformBitmap = + new Messages.PlatformBitmap.Builder().setBitmap(bitmap).build(); + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + Assert.assertNull(imageRegistry.getBitmap(1L)); + imageRegistry.addBitmapToCache(1L, platformBitmap); + Assert.assertEquals(imageRegistry.getBitmap(1L), mockBitmapDescriptor); + } + + @Test(expected = IllegalArgumentException.class) + public void AddBitmapToCacheThrowsOnWrongBitmapType() { + ImageRegistry imageRegistry = + new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, 1L); + Messages.PlatformRegisteredMapBitmap registeredMapBitmap = + new Messages.PlatformRegisteredMapBitmap.Builder().setId(0L).build(); + Messages.PlatformBitmap platformBitmap = + new Messages.PlatformBitmap.Builder().setBitmap(registeredMapBitmap).build(); + try { + imageRegistry.addBitmapToCache(0L, platformBitmap); + } catch (IllegalArgumentException e) { + Assert.assertEquals(e.getMessage(), "PlatformBitmap must contain a supported subtype."); + throw e; + } + + fail("Expected an IllegalArgumentException to be thrown"); + } + + // Helper method to generate 1x1 pixel base64 encoded png test image + private String generateBase64Image(int color) { + int width = 1; + int height = 1; + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + // Draw on the Bitmap + Paint paint = new Paint(); + paint.setColor(color); + canvas.drawRect(0, 0, width, height, paint); + + // Convert the Bitmap to PNG format + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + byte[] pngBytes = outputStream.toByteArray(); + + // Encode the PNG bytes as a base64 string + return Base64.encodeToString(pngBytes, Base64.DEFAULT); + } + + private InputStream buildImageInputStream() { + Bitmap fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + fakeBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); + byte[] byteArray = byteArrayOutputStream.toByteArray(); + return new ByteArrayInputStream(byteArray); + } +} 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 1540ebccf69..227c2d35e44 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 @@ -51,6 +51,7 @@ public class MarkersControllerTest { private AssetManager assetManager; private final float density = 1; private AutoCloseable mocksClosable; + private ImageRegistry imageRegistry; @Mock private Convert.BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; @@ -94,13 +95,15 @@ public void setUp() { context = ApplicationProvider.getApplicationContext(); flutterApi = spy(new MapsCallbackApi(mock(BinaryMessenger.class))); clusterManagersController = spy(new ClusterManagersController(flutterApi, context)); + imageRegistry = new ImageRegistry(assetManager, bitmapDescriptorFactoryWrapper, density); controller = new MarkersController( flutterApi, clusterManagersController, assetManager, density, - bitmapDescriptorFactoryWrapper); + bitmapDescriptorFactoryWrapper, + imageRegistry); googleMap = mock(GoogleMap.class); markerManager = new MarkerManager(googleMap); markerCollection = markerManager.newCollection(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java index 695f9a4e449..83a1c1f89a9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java @@ -26,4 +26,7 @@ public void controller_SetsStrokeDensity() { Mockito.verify(polyline).setWidth(density * strokeWidth); } + + @Test + public void controller_addPolylineWithRegisteredBitmap() {} } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/assets/checkers.png b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/checkers.png new file mode 100644 index 00000000000..444874d2279 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/assets/checkers.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/bitmap_registry_tests.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/bitmap_registry_tests.dart new file mode 100644 index 00000000000..658d3400e88 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/bitmap_registry_tests.dart @@ -0,0 +1,100 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_example/example_google_map.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void bitmapRegistryTests() { + GoogleMapsFlutterPlatform.instance.enableDebugInspection(); + + testWidgets('Add bitmap to cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + }); + + testWidgets('Remove bitmap from cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + + await GoogleMapsFlutterPlatform.instance.unregisterBitmap(1); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isFalse, + ); + }); + + testWidgets('Clear bitmap cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.clearBitmapCache(); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + + await GoogleMapsFlutterPlatform.instance.clearBitmapCache(); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isFalse, + ); + }); +} + +// Pump a map and return the map ID. +Future _pumpMap(WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(0, 0)), + onMapCreated: (ExampleGoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + return controller.mapId; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart index f739211a97f..df205a58d24 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart @@ -8,6 +8,7 @@ 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 'package:integration_test/integration_test.dart'; +import 'bitmap_registry_tests.dart' show bitmapRegistryTests; import 'google_maps_tests.dart' show googleMapsTests; void main() { @@ -44,4 +45,5 @@ void main() { // Run tests. googleMapsTests(); + bitmapRegistryTests(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart index 95b1134d566..782cc3a271e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart @@ -8,6 +8,7 @@ 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 'package:integration_test/integration_test.dart'; +import 'bitmap_registry_tests.dart'; import 'google_maps_tests.dart' show googleMapsTests; void main() { @@ -37,4 +38,5 @@ void main() { // Run tests. googleMapsTests(); + bitmapRegistryTests(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/bitmap_registry.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/bitmap_registry.dart new file mode 100644 index 00000000000..88ee4549bc9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/bitmap_registry.dart @@ -0,0 +1,169 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class BitmapRegistryPage extends GoogleMapExampleAppPage { + const BitmapRegistryPage({Key? key}) + : super(const Icon(Icons.speed), 'Bitmap registry', key: key); + + @override + Widget build(BuildContext context) { + return const _BitmapRegistryBody(); + } +} + +// How many markers to place on the map. +const int _numberOfMarkers = 500; + +class _BitmapRegistryBody extends StatefulWidget { + const _BitmapRegistryBody(); + + @override + State<_BitmapRegistryBody> createState() => _BitmapRegistryBodyState(); +} + +class _BitmapRegistryBodyState extends State<_BitmapRegistryBody> { + final Set _markers = {}; + int? _registeredBitmapId; + + @override + void initState() { + super.initState(); + + _registerBitmap(); + } + + @override + void dispose() { + _unregisterBitmap(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 2 / 3, + child: ExampleGoogleMap( + markers: _markers, + initialCameraPosition: const CameraPosition( + target: LatLng(0, 0), + zoom: 1.0, + ), + ), + ), + MaterialButton( + onPressed: () async { + // Add markers to the map with a custom bitmap as the icon. + // + // To show potential performance issues: + // * large original image is used (800x600px, ~330KB) + // * bitmap is scaled down to 64x64px + // * bitmap is created once and reused for all markers. This + // doesn't help much because the bitmap is still sent to the + // platform side for each marker. + // + // Adding many markers may result in a performance hit, + // out of memory errors or even app crashes. + final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor(); + _updateMarkers(bitmap); + }, + child: const Text('Add $_numberOfMarkers markers, no registry'), + ), + MaterialButton( + onPressed: _registeredBitmapId == null + ? null + : () { + // Add markers to the map with a custom bitmap as the icon + // placed in the bitmap registry beforehand. + final RegisteredMapBitmap registeredBitmap = + RegisteredMapBitmap(id: _registeredBitmapId!); + _updateMarkers(registeredBitmap); + }, + child: const Text('Add $_numberOfMarkers markers using registry'), + ), + ], + ), + ); + } + + /// Register a bitmap in the bitmap registry. + Future _registerBitmap() async { + if (_registeredBitmapId != null) { + return; + } + + final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor(); + const int registeredBitmapId = 1; + await GoogleMapsFlutterPlatform.instance + .registerBitmap(registeredBitmapId, bitmap); + _registeredBitmapId = registeredBitmapId; + + // If the widget was disposed before the bitmap was registered, unregister + // the bitmap. + if (!mounted) { + _unregisterBitmap(); + return; + } + + setState(() {}); + } + + /// Unregister the bitmap from the bitmap registry. + void _unregisterBitmap() { + if (_registeredBitmapId == null) { + return; + } + + GoogleMapsFlutterPlatform.instance.unregisterBitmap(_registeredBitmapId!); + _registeredBitmapId = null; + } + + // Create a set of markers with the given bitmap and update the state with new + // markers. + void _updateMarkers(BitmapDescriptor bitmap) { + final List newMarkers = List.generate( + _numberOfMarkers, + (int id) { + return Marker( + markerId: MarkerId('$id'), + icon: bitmap, + position: LatLng( + Random().nextDouble() * 100 - 50, + Random().nextDouble() * 100 - 50, + ), + ); + }, + ); + + setState(() { + _markers + ..clear() + ..addAll(newMarkers); + }); + } + + /// Load a bitmap from an asset and create a scaled [BytesMapBitmap] from it. + Future _getAssetBitmapDescriptor() async { + final ByteData byteData = await rootBundle.load('assets/checkers.png'); + final Uint8List bytes = byteData.buffer.asUint8List(); + return BitmapDescriptor.bytes( + bytes, + width: 64, + height: 64, + ); + } +} 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 30665c1be23..9fe2b17f469 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 @@ -9,6 +9,7 @@ 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 'animate_camera.dart'; +import 'bitmap_registry.dart'; import 'clustering.dart'; import 'lite_mode.dart'; import 'map_click.dart'; @@ -45,6 +46,7 @@ final List _allPages = [ const TileOverlayPage(), const ClusteringPage(), const MapIdPage(), + const BitmapRegistryPage(), ]; /// MapsDemo is the Main Application. 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 4aac17d849b..64726d47ef8 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_platform_interface: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart index 76b8a8ce75d..4deceb24847 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart @@ -116,4 +116,12 @@ class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { GoogleMapsFlutterAndroid.clusterFromPlatformCluster(cluster!)) .toList(); } + + @override + Future hasRegisteredMapBitmap({ + required int mapId, + required int bitmapId, + }) { + return _inspectorProvider(mapId)!.hasRegisteredMapBitmap(bitmapId); + } } 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 dd19bed8646..63e9d80e0e4 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 @@ -456,6 +456,23 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { : _setStyleFailureMessage; } + @override + Future registerBitmap(int id, MapBitmap bitmap) { + final PlatformBitmap platformBitmap = + platformBitmapFromBitmapDescriptor(bitmap); + return ImageRegistryApi().addBitmapToCache(id, platformBitmap); + } + + @override + Future unregisterBitmap(int id) { + return ImageRegistryApi().removeBitmapFromCache(id); + } + + @override + Future clearBitmapCache() { + return ImageRegistryApi().clearBitmapCache(); + } + /// Set [GoogleMapsFlutterPlatform] to use [AndroidViewSurface] to build the /// Google Maps widget. /// @@ -925,6 +942,13 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { imagePixelRatio: bytes.imagePixelRatio, width: bytes.width, height: bytes.height)); + case final RegisteredMapBitmap registeredBitmap: + return PlatformBitmap( + bitmap: PlatformRegisteredMapBitmap( + id: registeredBitmap.id, + ), + ); + default: throw ArgumentError( 'Unrecognized type of bitmap ${bitmap.runtimeType}', 'bitmap'); 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 d5cc589f22e..1546bdd158e 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.6.0), 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 @@ -1525,6 +1525,28 @@ class PlatformBitmapBytesMap { } } +/// Pigeon equivalent of a registered bitmap. +class PlatformRegisteredMapBitmap { + PlatformRegisteredMapBitmap({ + required this.id, + }); + + int id; + + Object encode() { + return [ + id, + ]; + } + + static PlatformRegisteredMapBitmap decode(Object result) { + result as List; + return PlatformRegisteredMapBitmap( + id: result[0]! as int, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -1667,6 +1689,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformBitmapBytesMap) { buffer.putUint8(173); writeValue(buffer, value.encode()); + } else if (value is PlatformRegisteredMapBitmap) { + buffer.putUint8(174); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1771,6 +1796,8 @@ class _PigeonCodec extends StandardMessageCodec { return PlatformBitmapAssetMap.decode(readValue(buffer)!); case 173: return PlatformBitmapBytesMap.decode(readValue(buffer)!); + case 174: + return PlatformRegisteredMapBitmap.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -3413,4 +3440,125 @@ class MapsInspectorApi { .cast(); } } + + Future hasRegisteredMapBitmap(int id) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.MapsInspectorApi.hasRegisteredMapBitmap$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([id]) 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?)!; + } + } +} + +/// API for interacting with the image registry. +class ImageRegistryApi { + /// Constructor for [ImageRegistryApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImageRegistryApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Adds a bitmap to the cache. + Future addBitmapToCache(int id, PlatformBitmap bitmap) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.ImageRegistryApi.addBitmapToCache$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([id, bitmap]) 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 { + return; + } + } + + /// Removes a bitmap from the cache. + Future removeBitmapFromCache(int id) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.ImageRegistryApi.removeBitmapFromCache$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([id]) 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 { + return; + } + } + + /// Clears the bitmap cache. + Future clearBitmapCache() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.ImageRegistryApi.clearBitmapCache$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 { + return; + } + } } 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 9db93e75314..4d1aa5e4393 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 @@ -588,6 +588,15 @@ class PlatformBitmapBytesMap { final double? height; } +/// Pigeon equivalent of a registered bitmap. +class PlatformRegisteredMapBitmap { + PlatformRegisteredMapBitmap({ + required this.id, + }); + + final int id; +} + /// Interface for non-test interactions with the native SDK. /// /// For test-only state queries, see [MapsInspectorApi]. @@ -772,4 +781,18 @@ abstract class MapsInspectorApi { PlatformTileLayer? getTileOverlayInfo(String tileOverlayId); PlatformZoomRange getZoomRange(); List getClusters(String clusterManagerId); + bool hasRegisteredMapBitmap(int id); +} + +/// API for interacting with the image registry. +@HostApi() +abstract class ImageRegistryApi { + /// Adds a bitmap to the cache. + void addBitmapToCache(int id, PlatformBitmap bitmap); + + /// Removes a bitmap from the cache. + void removeBitmapFromCache(int id); + + /// Clears the bitmap cache. + void clearBitmapCache(); } 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 19db56e1913..c8ac0fbd287 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: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/assets/checkers.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/assets/checkers.png new file mode 100644 index 00000000000..444874d2279 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/assets/checkers.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/bitmap_registry_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/bitmap_registry_test.dart new file mode 100644 index 00000000000..b6fd8a1856b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/bitmap_registry_test.dart @@ -0,0 +1,102 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:maps_example_dart/example_google_map.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + GoogleMapsFlutterPlatform.instance.enableDebugInspection(); + + testWidgets('Add bitmap to cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + }); + + testWidgets('Remove bitmap from cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + + await GoogleMapsFlutterPlatform.instance.unregisterBitmap(1); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isFalse, + ); + }); + + testWidgets('Clear bitmap cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.clearBitmapCache(); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + + await GoogleMapsFlutterPlatform.instance.clearBitmapCache(); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isFalse, + ); + }); +} + +// Pump a map and return the map ID. +Future _pumpMap(WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(0, 0)), + onMapCreated: (ExampleGoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + return controller.mapId; +} 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..07941e6824f 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 @@ -30,12 +30,14 @@ - (void)testClustering { FGMClusterManagersController *clusterManagersController = [[FGMClusterManagersController alloc] initWithMapView:mapView callbackHandler:handler]; + FGMImageRegistry *imageRegistry = [[FGMImageRegistry alloc] initWithRegistrar:registrar]; FLTMarkersController *markersController = [[FLTMarkersController alloc] initWithMapView:mapView callbackHandler:handler clusterManagersController:clusterManagersController - registrar:registrar]; + registrar:registrar + imageRegistry:imageRegistry]; // Add cluster managers. NSString *clusterManagerId = @"cm"; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m index c175550dd2c..08237f8589a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m @@ -57,9 +57,12 @@ - (void)testFrameObserver { - (void)testMapsServiceSync { id registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); - FLTGoogleMapFactory *factory1 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; + FGMImageRegistry *imageRegistry = [[FGMImageRegistry alloc] initWithRegistrar:registrar]; + FLTGoogleMapFactory *factory1 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar + imageRegistry:imageRegistry]; XCTAssertNotNil(factory1.sharedMapServices); - FLTGoogleMapFactory *factory2 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; + FLTGoogleMapFactory *factory2 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar + imageRegistry:imageRegistry]; // Test pointer equality, should be same retained singleton +[GMSServices sharedServices] object. // Retaining the opaque object should be enough to avoid multiple internal initializations, // but don't test the internals of the GoogleMaps API. Assume that it does what is documented. 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 3144c2aff5e..7c2a272054d 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 @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:maps_example_dart/animate_camera.dart'; +import 'package:maps_example_dart/bitmap_registry.dart'; import 'package:maps_example_dart/clustering.dart'; import 'package:maps_example_dart/lite_mode.dart'; import 'package:maps_example_dart/map_click.dart'; @@ -43,5 +44,6 @@ void main() { TileOverlayPage(), ClusteringPage(), MapIdPage(), + BitmapRegistryPage(), ]))); } 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 f0a24119282..4f8dd3f0ec2 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_platform_interface: {path: ../../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/assets/checkers.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/assets/checkers.png new file mode 100644 index 00000000000..444874d2279 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/assets/checkers.png differ 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 3144c2aff5e..7c2a272054d 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 @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:maps_example_dart/animate_camera.dart'; +import 'package:maps_example_dart/bitmap_registry.dart'; import 'package:maps_example_dart/clustering.dart'; import 'package:maps_example_dart/lite_mode.dart'; import 'package:maps_example_dart/map_click.dart'; @@ -43,5 +44,6 @@ void main() { TileOverlayPage(), ClusteringPage(), MapIdPage(), + BitmapRegistryPage(), ]))); } 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 f0a24119282..4f8dd3f0ec2 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_platform_interface: {path: ../../../../../packages/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/bitmap_registry.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/bitmap_registry.dart new file mode 100644 index 00000000000..88ee4549bc9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/bitmap_registry.dart @@ -0,0 +1,169 @@ +// 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 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'example_google_map.dart'; +import 'page.dart'; + +class BitmapRegistryPage extends GoogleMapExampleAppPage { + const BitmapRegistryPage({Key? key}) + : super(const Icon(Icons.speed), 'Bitmap registry', key: key); + + @override + Widget build(BuildContext context) { + return const _BitmapRegistryBody(); + } +} + +// How many markers to place on the map. +const int _numberOfMarkers = 500; + +class _BitmapRegistryBody extends StatefulWidget { + const _BitmapRegistryBody(); + + @override + State<_BitmapRegistryBody> createState() => _BitmapRegistryBodyState(); +} + +class _BitmapRegistryBodyState extends State<_BitmapRegistryBody> { + final Set _markers = {}; + int? _registeredBitmapId; + + @override + void initState() { + super.initState(); + + _registerBitmap(); + } + + @override + void dispose() { + _unregisterBitmap(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + AspectRatio( + aspectRatio: 2 / 3, + child: ExampleGoogleMap( + markers: _markers, + initialCameraPosition: const CameraPosition( + target: LatLng(0, 0), + zoom: 1.0, + ), + ), + ), + MaterialButton( + onPressed: () async { + // Add markers to the map with a custom bitmap as the icon. + // + // To show potential performance issues: + // * large original image is used (800x600px, ~330KB) + // * bitmap is scaled down to 64x64px + // * bitmap is created once and reused for all markers. This + // doesn't help much because the bitmap is still sent to the + // platform side for each marker. + // + // Adding many markers may result in a performance hit, + // out of memory errors or even app crashes. + final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor(); + _updateMarkers(bitmap); + }, + child: const Text('Add $_numberOfMarkers markers, no registry'), + ), + MaterialButton( + onPressed: _registeredBitmapId == null + ? null + : () { + // Add markers to the map with a custom bitmap as the icon + // placed in the bitmap registry beforehand. + final RegisteredMapBitmap registeredBitmap = + RegisteredMapBitmap(id: _registeredBitmapId!); + _updateMarkers(registeredBitmap); + }, + child: const Text('Add $_numberOfMarkers markers using registry'), + ), + ], + ), + ); + } + + /// Register a bitmap in the bitmap registry. + Future _registerBitmap() async { + if (_registeredBitmapId != null) { + return; + } + + final BytesMapBitmap bitmap = await _getAssetBitmapDescriptor(); + const int registeredBitmapId = 1; + await GoogleMapsFlutterPlatform.instance + .registerBitmap(registeredBitmapId, bitmap); + _registeredBitmapId = registeredBitmapId; + + // If the widget was disposed before the bitmap was registered, unregister + // the bitmap. + if (!mounted) { + _unregisterBitmap(); + return; + } + + setState(() {}); + } + + /// Unregister the bitmap from the bitmap registry. + void _unregisterBitmap() { + if (_registeredBitmapId == null) { + return; + } + + GoogleMapsFlutterPlatform.instance.unregisterBitmap(_registeredBitmapId!); + _registeredBitmapId = null; + } + + // Create a set of markers with the given bitmap and update the state with new + // markers. + void _updateMarkers(BitmapDescriptor bitmap) { + final List newMarkers = List.generate( + _numberOfMarkers, + (int id) { + return Marker( + markerId: MarkerId('$id'), + icon: bitmap, + position: LatLng( + Random().nextDouble() * 100 - 50, + Random().nextDouble() * 100 - 50, + ), + ); + }, + ); + + setState(() { + _markers + ..clear() + ..addAll(newMarkers); + }); + } + + /// Load a bitmap from an asset and create a scaled [BytesMapBitmap] from it. + Future _getAssetBitmapDescriptor() async { + final ByteData byteData = await rootBundle.load('assets/checkers.png'); + final Uint8List bytes = byteData.buffer.asUint8List(); + return BitmapDescriptor.bytes( + bytes, + width: 64, + height: 64, + ); + } +} 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 9e6466288a9..83e95981dd3 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_platform_interface: {path: ../../../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageRegistry.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageRegistry.h new file mode 100644 index 00000000000..af9f8395939 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageRegistry.h @@ -0,0 +1,23 @@ +// 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 +#import + +#import "messages.g.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FGMImageRegistry : NSObject + +- (instancetype)initWithRegistrar:(NSObject *)registrar; + +/// Returns registered image with the given identifier or null if registered image is not found. +/// +/// @param identifier An identifier of the registered image. +- (nullable UIImage *)getBitmap:(NSNumber *)identifier; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageRegistry.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageRegistry.m new file mode 100644 index 00000000000..2374de756cc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMImageRegistry.m @@ -0,0 +1,55 @@ +// 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 "FGMImageRegistry.h" +#import "GoogleMapMarkerController.h" + +@interface FGMImageRegistry () + +/// A dictionary that stores all registered image IDs and corresponding images. +@property(nonatomic, strong) NSMutableDictionary *registry; + +/// A registration context of this plugin. +@property(weak, nonatomic) NSObject *registrar; + +@end + +@implementation FGMImageRegistry + +- (instancetype)initWithRegistrar:(NSObject *)registrar { + self = [super init]; + if (self) { + _registrar = registrar; + _registry = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)addBitmapToCacheId:(NSInteger)id + bitmap:(nonnull FGMPlatformBitmap *)bitmap + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + CGFloat screenScale = [[UIScreen mainScreen] scale]; + UIImage *image = [FLTGoogleMapMarkerController iconFromBitmap:bitmap + registrar:_registrar + screenScale:screenScale + imageRegistry:self]; + NSNumber *idNumber = [NSNumber numberWithInteger:id]; + [self.registry setObject:image forKey:idNumber]; +} + +- (void)removeBitmapFromCacheId:(NSInteger)id + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSNumber *idNumber = [NSNumber numberWithInteger:id]; + [self.registry removeObjectForKey:idNumber]; +} + +- (void)clearBitmapCacheWithError:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [self.registry removeAllObjects]; +} + +- (UIImage *)getBitmap:(NSNumber *)identifier { + return self.registry[identifier]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.h index cc69bcd39f2..5eb2b254d67 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.h @@ -6,6 +6,7 @@ #import #import "FGMClusterManagersController.h" +#import "FGMImageRegistry.h" #import "GoogleMapCircleController.h" #import "GoogleMapController.h" #import "GoogleMapMarkerController.h" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.m index 70bde9022a0..0253d9bea57 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapsPlugin.m @@ -9,7 +9,10 @@ @implementation FLTGoogleMapsPlugin + (void)registerWithRegistrar:(NSObject *)registrar { - FLTGoogleMapFactory *googleMapFactory = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; + FGMImageRegistry *imageRegistry = [[FGMImageRegistry alloc] initWithRegistrar:registrar]; + SetUpFGMImageRegistryApi(registrar.messenger, imageRegistry); + FLTGoogleMapFactory *googleMapFactory = + [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar imageRegistry:imageRegistry]; [registrar registerViewFactory:googleMapFactory withId:@"plugins.flutter.dev/google_maps_ios" gestureRecognizersBlockingPolicy: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h index a2b75d74c75..b7236a62d01 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h @@ -6,6 +6,7 @@ #import #import "FGMClusterManagersController.h" +#import "FGMImageRegistry.h" #import "GoogleMapCircleController.h" #import "GoogleMapMarkerController.h" #import "GoogleMapPolygonController.h" @@ -19,7 +20,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId creationParameters:(FGMPlatformMapViewCreationParams *)creationParameters - registrar:(NSObject *)registrar; + registrar:(NSObject *)registrar + imageRegistry:(FGMImageRegistry *)imageRegistry; - (void)showAtOrigin:(CGPoint)origin; - (void)hide; - (nullable GMSCameraPosition *)cameraPosition; @@ -27,7 +29,8 @@ NS_ASSUME_NONNULL_BEGIN // Allows the engine to create new Google Map instances. @interface FLTGoogleMapFactory : NSObject -- (instancetype)initWithRegistrar:(NSObject *)registrar; +- (instancetype)initWithRegistrar:(NSObject *)registrar + imageRegistry:(FGMImageRegistry *)imageRegistry; @end NS_ASSUME_NONNULL_END 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 0108a3f72b2..8bad6dcee57 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 @@ -6,6 +6,7 @@ #import "GoogleMapController.h" +#import "FGMImageRegistry.h" #import "FGMMarkerUserData.h" #import "FLTGoogleMapHeatmapController.h" #import "FLTGoogleMapJSONConversions.h" @@ -17,6 +18,7 @@ @interface FLTGoogleMapFactory () @property(weak, nonatomic) NSObject *registrar; +@property(weak, nonatomic) FGMImageRegistry *imageRegistry; @property(strong, nonatomic, readonly) id sharedMapServices; @end @@ -25,10 +27,12 @@ @implementation FLTGoogleMapFactory @synthesize sharedMapServices = _sharedMapServices; -- (instancetype)initWithRegistrar:(NSObject *)registrar { +- (instancetype)initWithRegistrar:(NSObject *)registrar + imageRegistry:(FGMImageRegistry *)imageRegistry { self = [super init]; if (self) { _registrar = registrar; + _imageRegistry = imageRegistry; } return self; } @@ -47,7 +51,8 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar return [[FLTGoogleMapController alloc] initWithFrame:frame viewIdentifier:viewId creationParameters:args - registrar:self.registrar]; + registrar:self.registrar + imageRegistry:self.imageRegistry]; } - (id)sharedMapServices { @@ -98,6 +103,7 @@ @interface FGMMapCallHandler () @interface FGMMapInspector : NSObject - (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controller messenger:(NSObject *)messenger + imageRegistry:(nonnull FGMImageRegistry *)imageRegistry pigeonSuffix:(NSString *)suffix; @end @@ -108,6 +114,8 @@ @interface FGMMapInspector () @property(nonatomic, weak) FLTGoogleMapController *controller; /// The messenger this instance was registered with by Pigeon. @property(nonatomic, copy) NSObject *messenger; +/// ImageRegistry for registering bitmaps. +@property(nonatomic, weak) FGMImageRegistry *imageRegistry; /// The suffix this instance was registered under with Pigeon. @property(nonatomic, copy) NSString *pigeonSuffix; @end @@ -146,7 +154,8 @@ @implementation FLTGoogleMapController - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId creationParameters:(FGMPlatformMapViewCreationParams *)creationParameters - registrar:(NSObject *)registrar { + registrar:(NSObject *)registrar + imageRegistry:(FGMImageRegistry *)imageRegistry { GMSCameraPosition *camera = FGMGetCameraPositionForPigeonCameraPosition(creationParameters.initialCameraPosition); @@ -163,13 +172,15 @@ - (instancetype)initWithFrame:(CGRect)frame return [self initWithMapView:mapView viewIdentifier:viewId creationParameters:creationParameters - registrar:registrar]; + registrar:registrar + imageRegistry:imageRegistry]; } - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView viewIdentifier:(int64_t)viewId creationParameters:(FGMPlatformMapViewCreationParams *)creationParameters - registrar:(NSObject *_Nonnull)registrar { + registrar:(NSObject *_Nonnull)registrar + imageRegistry:(FGMImageRegistry *)imageRegistry { if (self = [super init]) { _mapView = mapView; @@ -189,7 +200,8 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView _markersController = [[FLTMarkersController alloc] initWithMapView:_mapView callbackHandler:_dartCallbackHandler clusterManagersController:_clusterManagersController - registrar:registrar]; + registrar:registrar + imageRegistry:imageRegistry]; _polygonsController = [[FLTPolygonsController alloc] initWithMapView:_mapView callbackHandler:_dartCallbackHandler registrar:registrar]; @@ -223,6 +235,7 @@ - (instancetype)initWithMapView:(GMSMapView *_Nonnull)mapView SetUpFGMMapsApiWithSuffix(registrar.messenger, _callHandler, pigeonSuffix); _inspector = [[FGMMapInspector alloc] initWithMapController:self messenger:registrar.messenger + imageRegistry:imageRegistry pigeonSuffix:pigeonSuffix]; SetUpFGMMapsInspectorApiWithSuffix(registrar.messenger, _inspector, pigeonSuffix); } @@ -734,12 +747,14 @@ @implementation FGMMapInspector - (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controller messenger:(NSObject *)messenger + imageRegistry:(nonnull FGMImageRegistry *)imageRegistry pigeonSuffix:(NSString *)suffix { self = [super init]; if (self) { _controller = controller; _messenger = messenger; _pigeonSuffix = suffix; + _imageRegistry = imageRegistry; } return self; } @@ -822,4 +837,10 @@ - (nullable FGMPlatformZoomRange *)zoomRange: max:@(self.controller.mapView.maxZoom)]; } +- (nullable NSNumber *)hasRegisteredMapBitmapId:(NSInteger)id + error:(FlutterError *_Nullable __autoreleasing *_Nonnull) + error { + return [self.imageRegistry getBitmap:@(id)] ? @(YES) : @(NO); +} + @end 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 3b090a3ed02..328096f47f4 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 @@ -6,6 +6,7 @@ #import #import "FGMClusterManagersController.h" +#import "FGMImageRegistry.h" #import "GoogleMapController.h" #import "messages.g.h" @@ -22,13 +23,18 @@ NS_ASSUME_NONNULL_BEGIN - (void)hideInfoWindow; - (BOOL)isInfoWindowShown; - (void)removeMarker; ++ (UIImage *)iconFromBitmap:(FGMPlatformBitmap *)platformBitmap + registrar:(NSObject *)registrar + screenScale:(CGFloat)screenScale + imageRegistry:(FGMImageRegistry *)imageRegistry; @end @interface FLTMarkersController : NSObject - (instancetype)initWithMapView:(GMSMapView *)mapView callbackHandler:(FGMMapsCallbackApi *)callbackHandler clusterManagersController:(nullable FGMClusterManagersController *)clusterManagersController - registrar:(NSObject *)registrar; + registrar:(NSObject *)registrar + imageRegistry:(FGMImageRegistry *)imageRegistry; - (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 cfbc9392159..2ce3c1a5353 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 @@ -4,6 +4,7 @@ #import "GoogleMapMarkerController.h" +#import "FGMImageRegistry.h" #import "FGMMarkerUserData.h" #import "FLTGoogleMapJSONConversions.h" @@ -108,13 +109,15 @@ - (void)setZIndex:(int)zIndex { - (void)updateFromPlatformMarker:(FGMPlatformMarker *)platformMarker registrar:(NSObject *)registrar - screenScale:(CGFloat)screenScale { + screenScale:(CGFloat)screenScale + imageRegistry:(FGMImageRegistry *)imageRegistry { [self setAlpha:platformMarker.alpha]; [self setAnchor:FGMGetCGPointForPigeonPoint(platformMarker.anchor)]; [self setDraggable:platformMarker.draggable]; - UIImage *image = [self iconFromBitmap:platformMarker.icon - registrar:registrar - screenScale:screenScale]; + UIImage *image = [FLTGoogleMapMarkerController iconFromBitmap:platformMarker.icon + registrar:registrar + screenScale:screenScale + imageRegistry:imageRegistry]; [self setIcon:image]; [self setFlat:platformMarker.flat]; [self setConsumeTapEvents:platformMarker.consumeTapEvents]; @@ -139,15 +142,21 @@ - (void)interpretInfoWindow:(NSDictionary *)data { } } -- (UIImage *)iconFromBitmap:(FGMPlatformBitmap *)platformBitmap ++ (UIImage *)iconFromBitmap:(FGMPlatformBitmap *)platformBitmap registrar:(NSObject *)registrar - screenScale:(CGFloat)screenScale { + screenScale:(CGFloat)screenScale + imageRegistry:(FGMImageRegistry *)imageRegistry { NSAssert(screenScale > 0, @"Screen scale must be greater than 0"); // See comment in messages.dart for why this is so loosely typed. See also // https://github.com/flutter/flutter/issues/117819. id bitmap = platformBitmap.bitmap; UIImage *image; - if ([bitmap isKindOfClass:[FGMPlatformBitmapDefaultMarker class]]) { + if ([bitmap isKindOfClass:[FGMPlatformRegisteredMapBitmap class]]) { + FGMPlatformRegisteredMapBitmap *registeredBitmap = bitmap; + NSInteger imageId = registeredBitmap.id; + NSNumber *imageIdNumber = [NSNumber numberWithInteger:imageId]; + image = [imageRegistry getBitmap:imageIdNumber]; + } else if ([bitmap isKindOfClass:[FGMPlatformBitmapDefaultMarker class]]) { FGMPlatformBitmapDefaultMarker *bitmapDefaultMarker = bitmap; CGFloat hue = bitmapDefaultMarker.hue.doubleValue; image = [GMSMarker markerImageWithColor:[UIColor colorWithHue:hue / 360.0 @@ -240,7 +249,7 @@ - (UIImage *)iconFromBitmap:(FGMPlatformBitmap *)platformBitmap /// flutter google_maps_flutter_platform_interface package which has been replaced by 'bytes' /// message handling. It will be removed when the deprecated image bitmap description type /// 'fromBytes' is removed from the platform interface. -- (UIImage *)scaleImage:(UIImage *)image by:(double)scale { ++ (UIImage *)scaleImage:(UIImage *)image by:(double)scale { if (fabs(scale - 1) > 1e-3) { return [UIImage imageWithCGImage:[image CGImage] scale:(image.scale * scale) @@ -379,6 +388,7 @@ @interface FLTMarkersController () /// Controller for adding/removing/fetching cluster managers @property(weak, nonatomic, nullable) FGMClusterManagersController *clusterManagersController; @property(weak, nonatomic) NSObject *registrar; +@property(copy, nonatomic) FGMImageRegistry *imageRegistry; @property(weak, nonatomic) GMSMapView *mapView; @end @@ -388,7 +398,8 @@ @implementation FLTMarkersController - (instancetype)initWithMapView:(GMSMapView *)mapView callbackHandler:(FGMMapsCallbackApi *)callbackHandler clusterManagersController:(nullable FGMClusterManagersController *)clusterManagersController - registrar:(NSObject *)registrar { + registrar:(NSObject *)registrar + imageRegistry:(FGMImageRegistry *)imageRegistry { self = [super init]; if (self) { _callbackHandler = callbackHandler; @@ -396,6 +407,7 @@ - (instancetype)initWithMapView:(GMSMapView *)mapView _clusterManagersController = clusterManagersController; _markerIdentifierToController = [[NSMutableDictionary alloc] init]; _registrar = registrar; + _imageRegistry = imageRegistry; } return self; } @@ -418,7 +430,8 @@ - (void)addMarker:(FGMPlatformMarker *)markerToAdd { mapView:self.mapView]; [controller updateFromPlatformMarker:markerToAdd registrar:self.registrar - screenScale:[self getScreenScale]]; + screenScale:[self getScreenScale] + imageRegistry:self.imageRegistry]; if (clusterManagerIdentifier) { GMUClusterManager *clusterManager = [_clusterManagersController clusterManagerWithIdentifier:clusterManagerIdentifier]; @@ -450,7 +463,8 @@ - (void)changeMarker:(FGMPlatformMarker *)markerToChange { } else { [controller updateFromPlatformMarker:markerToChange registrar:self.registrar - screenScale:[self getScreenScale]]; + screenScale:[self getScreenScale] + imageRegistry:self.imageRegistry]; } } 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 b8c1bb5c35d..a8e88cd4102 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.6.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -104,6 +104,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @class FGMPlatformBitmapAssetImage; @class FGMPlatformBitmapAssetMap; @class FGMPlatformBitmapBytesMap; +@class FGMPlatformRegisteredMapBitmap; /// Pigeon representatation of a CameraPosition. @interface FGMPlatformCameraPosition : NSObject @@ -551,15 +552,13 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong) id bitmap; @end -/// Pigeon equivalent of [DefaultMarker]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#defaultMarker(float) +/// Pigeon equivalent of [DefaultMarker]. @interface FGMPlatformBitmapDefaultMarker : NSObject + (instancetype)makeWithHue:(nullable NSNumber *)hue; @property(nonatomic, strong, nullable) NSNumber *hue; @end -/// Pigeon equivalent of [BytesBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#fromBitmap(android.graphics.Bitmap) +/// Pigeon equivalent of [BytesBitmap]. @interface FGMPlatformBitmapBytes : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -569,8 +568,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) FGMPlatformSize *size; @end -/// Pigeon equivalent of [AssetBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetBitmap]. @interface FGMPlatformBitmapAsset : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -579,8 +577,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, copy, nullable) NSString *pkg; @end -/// Pigeon equivalent of [AssetImageBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetImageBitmap]. @interface FGMPlatformBitmapAssetImage : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -592,8 +589,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) FGMPlatformSize *size; @end -/// Pigeon equivalent of [AssetMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetMapBitmap]. @interface FGMPlatformBitmapAssetMap : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -609,8 +605,7 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) NSNumber *height; @end -/// Pigeon equivalent of [BytesMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-frombitmap-bitmap-image +/// Pigeon equivalent of [BytesMapBitmap]. @interface FGMPlatformBitmapBytesMap : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -626,6 +621,14 @@ typedef NS_ENUM(NSUInteger, FGMPlatformMapBitmapScaling) { @property(nonatomic, strong, nullable) NSNumber *height; @end +/// Pigeon equivalent of a registered bitmap. +@interface FGMPlatformRegisteredMapBitmap : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithId:(NSInteger)id; +@property(nonatomic, assign) NSInteger id; +@end + /// The codec used by all APIs. NSObject *FGMGetMessagesCodec(void); @@ -840,6 +843,9 @@ extern void SetUpFGMMapsPlatformViewApiWithSuffix(id bin - (nullable NSArray *) clustersWithIdentifier:(NSString *)clusterManagerId error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)hasRegisteredMapBitmapId:(NSInteger)id + error:(FlutterError *_Nullable *_Nonnull)error; @end extern void SetUpFGMMapsInspectorApi(id binaryMessenger, @@ -849,4 +855,23 @@ extern void SetUpFGMMapsInspectorApiWithSuffix(id binary NSObject *_Nullable api, NSString *messageChannelSuffix); +/// API for interacting with the image registry. +@protocol FGMImageRegistryApi +/// Adds a bitmap to the cache. +- (void)addBitmapToCacheId:(NSInteger)id + bitmap:(FGMPlatformBitmap *)bitmap + error:(FlutterError *_Nullable *_Nonnull)error; +/// Removes a bitmap from the cache. +- (void)removeBitmapFromCacheId:(NSInteger)id error:(FlutterError *_Nullable *_Nonnull)error; +/// Clears the bitmap cache. +- (void)clearBitmapCacheWithError:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void SetUpFGMImageRegistryApi(id binaryMessenger, + NSObject *_Nullable api); + +extern void SetUpFGMImageRegistryApiWithSuffix(id binaryMessenger, + NSObject *_Nullable api, + NSString *messageChannelSuffix); + NS_ASSUME_NONNULL_END 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 5d3474cf79c..e2867d850c7 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.6.1), 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" @@ -311,6 +311,12 @@ + (nullable FGMPlatformBitmapBytesMap *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FGMPlatformRegisteredMapBitmap () ++ (FGMPlatformRegisteredMapBitmap *)fromList:(NSArray *)list; ++ (nullable FGMPlatformRegisteredMapBitmap *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FGMPlatformCameraPosition + (instancetype)makeWithBearing:(double)bearing target:(FGMPlatformLatLng *)target @@ -1533,6 +1539,27 @@ + (nullable FGMPlatformBitmapBytesMap *)nullableFromList:(NSArray *)list { } @end +@implementation FGMPlatformRegisteredMapBitmap ++ (instancetype)makeWithId:(NSInteger)id { + FGMPlatformRegisteredMapBitmap *pigeonResult = [[FGMPlatformRegisteredMapBitmap alloc] init]; + pigeonResult.id = id; + return pigeonResult; +} ++ (FGMPlatformRegisteredMapBitmap *)fromList:(NSArray *)list { + FGMPlatformRegisteredMapBitmap *pigeonResult = [[FGMPlatformRegisteredMapBitmap alloc] init]; + pigeonResult.id = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FGMPlatformRegisteredMapBitmap *)nullableFromList:(NSArray *)list { + return (list) ? [FGMPlatformRegisteredMapBitmap fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.id), + ]; +} +@end + @interface FGMMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FGMMessagesPigeonCodecReader @@ -1638,6 +1665,8 @@ - (nullable id)readValueOfType:(UInt8)type { return [FGMPlatformBitmapAssetMap fromList:[self readValue]]; case 170: return [FGMPlatformBitmapBytesMap fromList:[self readValue]]; + case 171: + return [FGMPlatformRegisteredMapBitmap fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -1778,6 +1807,9 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FGMPlatformBitmapBytesMap class]]) { [self writeByte:170]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FGMPlatformRegisteredMapBitmap class]]) { + [self writeByte:171]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -3121,4 +3153,114 @@ void SetUpFGMMapsInspectorApiWithSuffix(id binaryMesseng [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"MapsInspectorApi.hasRegisteredMapBitmap", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(hasRegisteredMapBitmapId:error:)], + @"FGMMapsInspectorApi api (%@) doesn't respond to " + @"@selector(hasRegisteredMapBitmapId:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_id = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + NSNumber *output = [api hasRegisteredMapBitmapId:arg_id error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +void SetUpFGMImageRegistryApi(id binaryMessenger, + NSObject *api) { + SetUpFGMImageRegistryApiWithSuffix(binaryMessenger, api, @""); +} + +void SetUpFGMImageRegistryApiWithSuffix(id binaryMessenger, + NSObject *api, + NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 + ? [NSString stringWithFormat:@".%@", messageChannelSuffix] + : @""; + /// Adds a bitmap to the cache. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"ImageRegistryApi.addBitmapToCache", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(addBitmapToCacheId:bitmap:error:)], + @"FGMImageRegistryApi api (%@) doesn't respond to " + @"@selector(addBitmapToCacheId:bitmap:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_id = [GetNullableObjectAtIndex(args, 0) integerValue]; + FGMPlatformBitmap *arg_bitmap = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api addBitmapToCacheId:arg_id bitmap:arg_bitmap error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Removes a bitmap from the cache. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"ImageRegistryApi.removeBitmapFromCache", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(removeBitmapFromCacheId:error:)], + @"FGMImageRegistryApi api (%@) doesn't respond to " + @"@selector(removeBitmapFromCacheId:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_id = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api removeBitmapFromCacheId:arg_id error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Clears the bitmap cache. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"ImageRegistryApi.clearBitmapCache", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(clearBitmapCacheWithError:)], + @"FGMImageRegistryApi api (%@) doesn't respond to @selector(clearBitmapCacheWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + [api clearBitmapCacheWithError:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart index 823e210ce73..745b8b59038 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart @@ -141,4 +141,12 @@ class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { GoogleMapsFlutterIOS.clusterFromPlatformCluster(cluster)) .toList(); } + + @override + Future hasRegisteredMapBitmap({ + required int mapId, + required int bitmapId, + }) { + return _inspectorProvider(mapId)!.hasRegisteredMapBitmap(bitmapId); + } } 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 89c6b90ddc6..9ee480109b9 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 @@ -441,6 +441,23 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { return _hostApi(mapId).getLastStyleError(); } + @override + Future registerBitmap(int id, MapBitmap bitmap) { + final PlatformBitmap platformBitmap = + platformBitmapFromBitmapDescriptor(bitmap); + return ImageRegistryApi().addBitmapToCache(id, platformBitmap); + } + + @override + Future unregisterBitmap(int id) { + return ImageRegistryApi().removeBitmapFromCache(id); + } + + @override + Future clearBitmapCache() { + return ImageRegistryApi().clearBitmapCache(); + } + Widget _buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -811,6 +828,10 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { imagePixelRatio: bytes.imagePixelRatio, width: bytes.width, height: bytes.height)); + case final RegisteredMapBitmap registeredBitmap: + return PlatformBitmap( + bitmap: PlatformRegisteredMapBitmap(id: registeredBitmap.id), + ); default: throw ArgumentError( 'Unrecognized type of bitmap ${bitmap.runtimeType}', 'bitmap'); 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 30a3e4e138d..040b9b89d26 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.6.1), 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 @@ -1250,8 +1250,7 @@ class PlatformBitmap { } } -/// Pigeon equivalent of [DefaultMarker]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#defaultMarker(float) +/// Pigeon equivalent of [DefaultMarker]. class PlatformBitmapDefaultMarker { PlatformBitmapDefaultMarker({ this.hue, @@ -1273,8 +1272,7 @@ class PlatformBitmapDefaultMarker { } } -/// Pigeon equivalent of [BytesBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#fromBitmap(android.graphics.Bitmap) +/// Pigeon equivalent of [BytesBitmap]. class PlatformBitmapBytes { PlatformBitmapBytes({ required this.byteData, @@ -1301,8 +1299,7 @@ class PlatformBitmapBytes { } } -/// Pigeon equivalent of [AssetBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetBitmap]. class PlatformBitmapAsset { PlatformBitmapAsset({ required this.name, @@ -1329,8 +1326,7 @@ class PlatformBitmapAsset { } } -/// Pigeon equivalent of [AssetImageBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetImageBitmap]. class PlatformBitmapAssetImage { PlatformBitmapAssetImage({ required this.name, @@ -1362,8 +1358,7 @@ class PlatformBitmapAssetImage { } } -/// Pigeon equivalent of [AssetMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-fromasset-string-assetname +/// Pigeon equivalent of [AssetMapBitmap]. class PlatformBitmapAssetMap { PlatformBitmapAssetMap({ required this.assetName, @@ -1405,8 +1400,7 @@ class PlatformBitmapAssetMap { } } -/// Pigeon equivalent of [BytesMapBitmap]. See -/// https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/model/BitmapDescriptorFactory#public-static-bitmapdescriptor-frombitmap-bitmap-image +/// Pigeon equivalent of [BytesMapBitmap]. class PlatformBitmapBytesMap { PlatformBitmapBytesMap({ required this.byteData, @@ -1448,6 +1442,28 @@ class PlatformBitmapBytesMap { } } +/// Pigeon equivalent of a registered bitmap. +class PlatformRegisteredMapBitmap { + PlatformRegisteredMapBitmap({ + required this.id, + }); + + int id; + + Object encode() { + return [ + id, + ]; + } + + static PlatformRegisteredMapBitmap decode(Object result) { + result as List; + return PlatformRegisteredMapBitmap( + id: result[0]! as int, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -1581,6 +1597,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformBitmapBytesMap) { buffer.putUint8(170); writeValue(buffer, value.encode()); + } else if (value is PlatformRegisteredMapBitmap) { + buffer.putUint8(171); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1677,6 +1696,8 @@ class _PigeonCodec extends StandardMessageCodec { return PlatformBitmapAssetMap.decode(readValue(buffer)!); case 170: return PlatformBitmapBytesMap.decode(readValue(buffer)!); + case 171: + return PlatformRegisteredMapBitmap.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -3194,4 +3215,125 @@ class MapsInspectorApi { .cast(); } } + + Future hasRegisteredMapBitmap(int id) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.MapsInspectorApi.hasRegisteredMapBitmap$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([id]) 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?)!; + } + } +} + +/// API for interacting with the image registry. +class ImageRegistryApi { + /// Constructor for [ImageRegistryApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImageRegistryApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Adds a bitmap to the cache. + Future addBitmapToCache(int id, PlatformBitmap bitmap) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.ImageRegistryApi.addBitmapToCache$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([id, bitmap]) 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 { + return; + } + } + + /// Removes a bitmap from the cache. + Future removeBitmapFromCache(int id) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.ImageRegistryApi.removeBitmapFromCache$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([id]) 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 { + return; + } + } + + /// Clears the bitmap cache. + Future clearBitmapCache() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.ImageRegistryApi.clearBitmapCache$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 { + return; + } + } } 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 342bd9ea801..266b131aea3 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 @@ -540,6 +540,15 @@ class PlatformBitmapBytesMap { final double? height; } +/// Pigeon equivalent of a registered bitmap. +class PlatformRegisteredMapBitmap { + PlatformRegisteredMapBitmap({ + required this.id, + }); + + final int id; +} + /// Pigeon equivalent of [MapBitmapScaling]. enum PlatformMapBitmapScaling { auto, @@ -752,4 +761,18 @@ abstract class MapsInspectorApi { PlatformZoomRange getZoomRange(); @ObjCSelector('clustersWithIdentifier:') List getClusters(String clusterManagerId); + bool hasRegisteredMapBitmap(int id); +} + +/// API for interacting with the image registry. +@HostApi() +abstract class ImageRegistryApi { + /// Adds a bitmap to the cache. + void addBitmapToCache(int id, PlatformBitmap bitmap); + + /// Removes a bitmap from the cache. + void removeBitmapFromCache(int id); + + /// Clears the bitmap cache. + void clearBitmapCache(); } 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 2020ec8940e..385d5e8bd56 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: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}} 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 a8f8e6d8b32..402e564035b 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 @@ -400,6 +400,23 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { return null; } + /// Registers [bitmap] with the bitmap registry. + Future registerBitmap(int id, MapBitmap bitmap) { + throw UnimplementedError('addBitmapToCache() has not been implemented.'); + } + + /// Unregisters the bitmap with the given [id]. + Future unregisterBitmap(int id) { + throw UnimplementedError( + 'removeBitmapFromCache() has not been implemented.', + ); + } + + /// Clears the bitmap cache. + Future clearBitmapCache() { + throw UnimplementedError('clearBitmapCache() has not been implemented.'); + } + /// 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/platform_interface/google_maps_inspector_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart index 8bf6f6f89ba..a38baace66d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_inspector_platform.dart @@ -137,4 +137,15 @@ abstract class GoogleMapsInspectorPlatform extends PlatformInterface { {required int mapId, required ClusterManagerId clusterManagerId}) { throw UnimplementedError('getClusters() has not been implemented.'); } + + /// Returns true if the bitmap with the given [id] is registered in the + /// bitmap registry. + Future hasRegisteredMapBitmap({ + required int mapId, + required int bitmapId, + }) { + throw UnimplementedError( + 'hasRegisteredMapBitmap() has not been implemented.', + ); + } } 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..d09e3fcb56d 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 @@ -496,6 +496,43 @@ abstract class MapBitmap extends BitmapDescriptor { final double? height; } +/// Represents a [BitmapDescriptor] that was previously registered with the +/// bitmap registry. +/// +/// The [id] is the unique ID of the registered bitmap. The bitmap itself is +/// registered using the bitmap registry and the ID is returned when the bitmap +/// is registered. +/// +/// Following example demonstrates how to register a bitmap using the bitmap +/// registry and create a [RegisteredMapBitmap] instance: +/// +/// ```dart +/// final AssetMapBitmap bitmap = AssetMapBitmap( +/// 'assets/images/map_icon.png', +/// imagePixelRatio: MediaQuery.maybeDevicePixelRatioOf(context), +/// ); +/// final int registeredBitmapId = +/// await GoogleMapBitmapRegistry.instance.register(bitmap); +/// final registeredMapBitmap = RegisteredMapBitmap(id: registeredBitmapId); +/// ``` +class RegisteredMapBitmap extends BitmapDescriptor { + /// Creates a [RegisteredMapBitmap] with the given [id]. + const RegisteredMapBitmap({ + required this.id, + }) : super._(); + + /// The id of the registered bitmap. + final int id; + + @override + Object toJson() { + return [ + 'registered', + id, + ]; + } +} + /// Represents a [BitmapDescriptor] that is created from an asset image. /// /// This class extends [BitmapDescriptor] to support loading images from assets 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 b0e48fd474b..5aee1711175 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 @@ -121,6 +121,32 @@ void main() { expect(await platform.getStyleError(mapId: 0), null); }, ); + + test('default implementation of `registerBitmap` throws UnimplementedError', + () { + expect( + () => BuildViewGoogleMapsFlutterPlatform().clearBitmapCache(), + throwsUnimplementedError, + ); + }); + + test( + 'default implementation of `unregisterBitmap` throws UnimplementedError', + () { + expect( + () => BuildViewGoogleMapsFlutterPlatform().clearBitmapCache(), + throwsUnimplementedError, + ); + }); + + test( + 'default implementation of `clearBitmapCache` throws UnimplementedError', + () { + expect( + () => BuildViewGoogleMapsFlutterPlatform().clearBitmapCache(), + throwsUnimplementedError, + ); + }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/bitmap_registry_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/bitmap_registry_test.dart new file mode 100644 index 00000000000..30d2ce39a12 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/bitmap_registry_test.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + GoogleMapsFlutterPlatform.instance.enableDebugInspection(); + + testWidgets('Add bitmap to cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + }); + + testWidgets('Remove bitmap from cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + + await GoogleMapsFlutterPlatform.instance.unregisterBitmap(1); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isFalse, + ); + }); + + testWidgets('Clear bitmap cache', (WidgetTester tester) async { + final AssetMapBitmap bitmap = AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + ); + + final int mapId = await _pumpMap(tester); + await GoogleMapsFlutterPlatform.instance.clearBitmapCache(); + await GoogleMapsFlutterPlatform.instance.registerBitmap(1, bitmap); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isTrue, + ); + + await GoogleMapsFlutterPlatform.instance.clearBitmapCache(); + expect( + await GoogleMapsInspectorPlatform.instance!.hasRegisteredMapBitmap( + mapId: mapId, + bitmapId: 1, + ), + isFalse, + ); + }); +} + +// Pump a map and return the map ID. +Future _pumpMap(WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(0, 0)), + onMapCreated: (GoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + return controller.mapId; +} 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 e4c0cfabb96..1b3d48b3a9c 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,17 @@ 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_web: path: ../ + google_maps_flutter: + path: ../../../../packages/google_maps_flutter/google_maps_flutter + google_maps_flutter_platform_interface: + path: ../../../../packages/google_maps_flutter/google_maps_flutter_platform_interface 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 f56e0b02bd1..4d49dd1bf08 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 @@ -24,6 +24,7 @@ import 'package:web/web.dart'; import 'src/dom_window_extension.dart'; import 'src/google_maps_inspector_web.dart'; +import 'src/image_registry.dart'; import 'src/map_styler.dart'; import 'src/marker_clustering.dart'; import 'src/third_party/to_screen_location/to_screen_location.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 f6af32046cc..e2d405b0455 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 @@ -369,8 +369,8 @@ void _cleanUpBitmapConversionCaches() { _bitmapBlobUrlCache.clear(); } -// Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. -Future _gmIconFromBitmapDescriptor( +/// Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. +Future gmIconFromBitmapDescriptor( BitmapDescriptor bitmapDescriptor) async { gmaps.Icon? icon; @@ -401,6 +401,12 @@ Future _gmIconFromBitmapDescriptor( return icon; } + if (bitmapDescriptor is RegisteredMapBitmap) { + final gmaps.Icon? registeredIcon = + ImageRegistry.instance.getBitmap(bitmapDescriptor.id); + return registeredIcon; + } + // The following code is for the deprecated BitmapDescriptor.fromBytes // and BitmapDescriptor.fromAssetImage. final List iconConfig = bitmapDescriptor.toJson() as List; @@ -441,6 +447,7 @@ Future _gmIconFromBitmapDescriptor( ..scaledSize = size; } } + return icon; } @@ -460,7 +467,7 @@ Future _markerOptionsFromMarker( ..visible = marker.visible ..opacity = marker.alpha ..draggable = marker.draggable - ..icon = await _gmIconFromBitmapDescriptor(marker.icon); + ..icon = await gmIconFromBitmapDescriptor(marker.icon); // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. // Flat and Rotation are not supported directly on the web. } 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 c49b5ed6739..f39d7a8e3b8 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 @@ -306,6 +306,21 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { return _map(mapId).lastStyleError; } + @override + Future registerBitmap(int id, MapBitmap bitmap) async { + return ImageRegistry.instance.addBitmapToCache(id, bitmap); + } + + @override + Future unregisterBitmap(int id) async { + return ImageRegistry.instance.removeBitmapFromCache(id); + } + + @override + Future clearBitmapCache() async { + return ImageRegistry.instance.clearBitmapCache(); + } + /// 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 98b47430958..2b8d77f2b17 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 @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'image_registry.dart'; import 'marker_clustering.dart'; /// Function that gets the [MapConfiguration] for a given `mapId`. @@ -103,4 +104,12 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { ?.getClusters(clusterManagerId) ?? []; } + + @override + Future hasRegisteredMapBitmap({ + required int mapId, + required int bitmapId, + }) async { + return ImageRegistry.instance.getBitmap(bitmapId) != null; + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/image_registry.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/image_registry.dart new file mode 100644 index 00000000000..f9a15476bf4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/image_registry.dart @@ -0,0 +1,39 @@ +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import '../google_maps_flutter_web.dart'; + +/// Manages mapping between registered bitmap IDs and [gmaps.Icon] objects. +class ImageRegistry { + /// Default constructor. + ImageRegistry._(); + + /// The singleton instance of [ImageRegistry]. + static final ImageRegistry instance = ImageRegistry._(); + + final Map _registry = {}; + + /// Registers a [bitmap] with the given [id]. + Future addBitmapToCache(int id, MapBitmap bitmap) async { + final gmaps.Icon? convertedBitmap = + await gmIconFromBitmapDescriptor(bitmap); + if (convertedBitmap != null) { + _registry[id] = convertedBitmap; + } + } + + /// Unregisters a bitmap with the given [id]. + void removeBitmapFromCache(int id) { + _registry.remove(id); + } + + /// Unregisters all previously registered bitmaps and clears the cache. + void clearBitmapCache() { + _registry.clear(); + } + + /// Returns the [gmaps.Icon] object associated with the given [id]. + gmaps.Icon? getBitmap(int id) { + return _registry[id]; + } +} 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 7ed3d99fed5..00f80d55e0c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -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: ../../../packages/google_maps_flutter/google_maps_flutter_platform_interface}}