From dcd8586ca7347d4108df212d77e226596ad702e8 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Tue, 4 Jun 2024 23:13:25 +0300 Subject: [PATCH] [google_maps_flutter] Custom marker size improvements - platform impls (#6826) Platform implementations portion of : https://github.com/flutter/packages/pull/4055 Adds platform handling for new BitmapDescriptor classes `AssetMapBitmap` and `BytesMapBitmap` introduced in https://github.com/flutter/packages/pull/6687 Containing only changes to packages * `google_maps_flutter_android` * `google_maps_flutter_ios` * `google_maps_flutter_web` Follow up PR will hold the app-facing plugin implementations. Linked issue: https://github.com/flutter/flutter/issues/34657 --- .../google_maps_flutter_android/CHANGELOG.md | 4 + .../flutter/plugins/googlemaps/Convert.java | 320 ++++++++++++++- .../googlemaps/GoogleMapController.java | 7 +- .../plugins/googlemaps/MarkersController.java | 16 +- .../googlemaps/PolylinesController.java | 10 +- .../ClusterManagersControllerTest.java | 10 +- .../plugins/googlemaps/ConvertTest.java | 288 ++++++++++++++ .../googlemaps/MarkersControllerTest.java | 9 +- .../integration_test/google_maps_tests.dart | 122 +++++- .../integration_test/resources/icon_image.png | Bin 0 -> 1257 bytes .../resources/icon_image_base64.dart | 49 +++ .../example/lib/custom_marker_icon.dart | 56 +++ .../example/lib/marker_icons.dart | 307 +++++++++++++-- .../example/lib/place_marker.dart | 28 +- .../example/pubspec.yaml | 2 +- .../google_maps_flutter_android/pubspec.yaml | 4 +- .../google_maps_flutter_ios/AUTHORS | 1 + .../google_maps_flutter_ios/CHANGELOG.md | 4 + .../integration_test/google_maps_test.dart | 135 ++++++- .../integration_test/resources/icon_image.png | Bin 0 -> 1257 bytes .../resources/icon_image_base64.dart | 49 +++ .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../RunnerTests/ExtractIconFromDataTests.m | 371 ++++++++++++++++++ .../example/ios14/pubspec.yaml | 2 +- .../lib/custom_marker_icon.dart | 56 +++ .../maps_example_dart/lib/marker_icons.dart | 307 +++++++++++++-- .../maps_example_dart/lib/place_marker.dart | 28 +- .../shared/maps_example_dart/pubspec.yaml | 2 +- .../ios/Classes/GoogleMapMarkerController.m | 233 ++++++++++- .../Classes/GoogleMapMarkerController_Test.h | 29 ++ .../Classes/google_maps_flutter_ios.modulemap | 1 + .../google_maps_flutter_ios/pubspec.yaml | 4 +- .../google_maps_flutter_web/CHANGELOG.md | 4 + .../example/assets/red_square.png | Bin 0 -> 195 bytes .../example/build.excerpt.yaml | 15 + .../google_maps_controller_test.dart | 12 +- .../google_maps_controller_test.mocks.dart | 18 +- .../google_maps_plugin_test.mocks.dart | 8 +- .../integration_test/markers_test.dart | 210 +++++++++- .../example/pubspec.yaml | 6 +- .../lib/src/convert.dart | 140 ++++++- .../lib/src/google_maps_controller.dart | 7 +- .../lib/src/google_maps_flutter_web.dart | 2 +- .../lib/src/markers.dart | 20 +- .../google_maps_flutter_web/pubspec.yaml | 4 +- 45 files changed, 2664 insertions(+), 240 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image.png create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image_base64.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/example/lib/custom_marker_icon.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image.png create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image_base64.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/custom_marker_icon.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController_Test.h create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/example/assets/red_square.png create mode 100644 packages/google_maps_flutter/google_maps_flutter_web/example/build.excerpt.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 4c003ec9eaae..f2fcc3cc80e4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.9.0 + +* Adds support for BitmapDescriptor classes `AssetMapBitmap` and `BytesMapBitmap`. + ## 2.8.1 * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. 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 ccf474ab1e2c..8b0796207d3c 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 @@ -4,9 +4,11 @@ package io.flutter.plugins.googlemaps; +import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Point; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; @@ -27,6 +29,8 @@ import com.google.android.gms.maps.model.Tile; import com.google.maps.android.clustering.Cluster; import io.flutter.FlutterInjector; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -37,45 +41,67 @@ /** Conversions between JSON-like values and GoogleMaps data types. */ class Convert { - private static BitmapDescriptor toBitmapDescriptor(Object o) { + private static BitmapDescriptor toBitmapDescriptor( + Object o, AssetManager assetManager, float density) { final List data = toList(o); - switch (toString(data.get(0))) { + final String descriptorType = toString(data.get(0)); + switch (descriptorType) { case "defaultMarker": if (data.size() == 1) { return BitmapDescriptorFactory.defaultMarker(); } else { - return BitmapDescriptorFactory.defaultMarker(toFloat(data.get(1))); + final float hue = toFloat(data.get(1)); + return BitmapDescriptorFactory.defaultMarker(hue); } case "fromAsset": + final String assetPath = toString(data.get(1)); if (data.size() == 2) { return BitmapDescriptorFactory.fromAsset( - FlutterInjector.instance() - .flutterLoader() - .getLookupKeyForAsset(toString(data.get(1)))); + FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(assetPath)); } else { + final String assetPackage = toString(data.get(2)); return BitmapDescriptorFactory.fromAsset( FlutterInjector.instance() .flutterLoader() - .getLookupKeyForAsset(toString(data.get(1)), toString(data.get(2)))); + .getLookupKeyForAsset(assetPath, assetPackage)); } case "fromAssetImage": + final String assetImagePath = toString(data.get(1)); if (data.size() == 3) { return BitmapDescriptorFactory.fromAsset( - FlutterInjector.instance() - .flutterLoader() - .getLookupKeyForAsset(toString(data.get(1)))); + FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(assetImagePath)); } else { throw new IllegalArgumentException( "'fromAssetImage' Expected exactly 3 arguments, got: " + data.size()); } case "fromBytes": - return getBitmapFromBytes(data); + return getBitmapFromBytesLegacy(data); + case "asset": + if (!(data.get(1) instanceof Map)) { + throw new IllegalArgumentException("'asset' expected a map as the second parameter"); + } + final Map assetData = toMap(data.get(1)); + return getBitmapFromAsset( + assetData, + assetManager, + density, + new BitmapDescriptorFactoryWrapper(), + new FlutterInjectorWrapper()); + case "bytes": + if (!(data.get(1) instanceof Map)) { + throw new IllegalArgumentException("'bytes' expected a map as the second parameter"); + } + final Map byteData = toMap(data.get(1)); + return getBitmapFromBytes(byteData, density, new BitmapDescriptorFactoryWrapper()); default: throw new IllegalArgumentException("Cannot interpret " + o + " as BitmapDescriptor"); } } - private static BitmapDescriptor getBitmapFromBytes(List data) { + // Used for deprecated fromBytes bitmap descriptor. + // Can be removed after support for "fromBytes" bitmap descriptor type is + // removed. + private static BitmapDescriptor getBitmapFromBytesLegacy(List data) { if (data.size() == 2) { try { Bitmap bitmap = toBitmap(data.get(1)); @@ -90,6 +116,185 @@ private static BitmapDescriptor getBitmapFromBytes(List data) { } } + /** + * Creates a BitmapDescriptor object from bytes data. + * + *

This method requires the `byteData` map to contain specific keys: 'byteData' for image + * bytes, 'bitmapScaling' for scaling mode, and 'imagePixelRatio' for scale ratio. It may + * optionally include 'width' and/or 'height' for explicit image dimensions. + * + * @param byteData a map containing the byte data and scaling instructions. Expected keys are: + * 'byteData': the actual bytes of the image, 'bitmapScaling': the scaling mode, either 'auto' + * or 'none', 'imagePixelRatio': used with 'auto' bitmapScaling if width or height are not + * provided, 'width' (optional): the desired width, which affects scaling if 'height' is not + * provided, 'height' (optional): the desired height, which affects scaling if 'width' is not + * provided + * @param density the density of the display, used to calculate pixel dimensions. + * @param bitmapDescriptorFactory is an instance of the BitmapDescriptorFactoryWrapper. + * @return BitmapDescriptor object from bytes data. + * @throws IllegalArgumentException if any required keys are missing in `byteData` or if the byte + * data cannot be interpreted as a valid image. + */ + @VisibleForTesting + public static BitmapDescriptor getBitmapFromBytes( + Map byteData, float density, BitmapDescriptorFactoryWrapper bitmapDescriptorFactory) { + + final String byteDataKey = "byteData"; + final String bitmapScalingKey = "bitmapScaling"; + final String imagePixelRatioKey = "imagePixelRatio"; + + if (!byteData.containsKey(byteDataKey)) { + throw new IllegalArgumentException("'bytes' requires '" + byteDataKey + "' key."); + } + if (!byteData.containsKey(bitmapScalingKey)) { + throw new IllegalArgumentException("'bytes' requires '" + bitmapScalingKey + "' key."); + } + if (!byteData.containsKey(imagePixelRatioKey)) { + throw new IllegalArgumentException("'bytes' requires '" + imagePixelRatioKey + "' key."); + } + + try { + Bitmap bitmap = toBitmap(byteData.get(byteDataKey)); + String scalingMode = toString(byteData.get(bitmapScalingKey)); + switch (scalingMode) { + case "auto": + final String widthKey = "width"; + final String heightKey = "height"; + + final Double width = + byteData.containsKey(widthKey) ? toDouble(byteData.get(widthKey)) : null; + final Double height = + byteData.containsKey(heightKey) ? toDouble(byteData.get(heightKey)) : null; + + if (width != null || height != null) { + int targetWidth = width != null ? toInt(width * density) : bitmap.getWidth(); + int targetHeight = height != null ? toInt(height * density) : bitmap.getHeight(); + + if (width != null && height == null) { + // If only width is provided, calculate height based on aspect ratio. + double aspectRatio = (double) bitmap.getHeight() / bitmap.getWidth(); + targetHeight = (int) (targetWidth * aspectRatio); + } else if (height != null && width == null) { + // If only height is provided, calculate width based on aspect ratio. + double aspectRatio = (double) bitmap.getWidth() / bitmap.getHeight(); + targetWidth = (int) (targetHeight * aspectRatio); + } + return bitmapDescriptorFactory.fromBitmap( + toScaledBitmap(bitmap, targetWidth, targetHeight)); + } else { + // Scale image using given scale ratio + final float scale = density / toFloat(byteData.get(imagePixelRatioKey)); + return bitmapDescriptorFactory.fromBitmap(toScaledBitmap(bitmap, scale)); + } + case "none": + break; + } + return bitmapDescriptorFactory.fromBitmap(bitmap); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to interpret bytes as a valid image.", e); + } + } + + /** + * Creates a BitmapDescriptor object from asset, using given details and density. + * + *

This method processes an asset specified by name and applies scaling based on the provided + * parameters. The `assetDetails` map must contain the keys 'assetName', 'bitmapScaling', and + * 'imagePixelRatio', and may optionally include 'width' and/or 'height' to explicitly set the + * dimensions of the output image. + * + * @param assetDetails a map containing the asset details and scaling instructions, with keys + * 'assetName': the name of the asset file, 'bitmapScaling': the scaling mode, either 'auto' + * or 'none', 'imagePixelRatio': used with 'auto' scaling to compute the scale ratio, 'width' + * (optional): the desired width, which affects scaling if 'height' is not provided, 'height' + * (optional): the desired height, which affects scaling if 'width' is not provided + * @param assetManager assetManager An instance of Android's AssetManager, which provides access + * to any raw asset files stored in the application's assets directory. + * @param density density the density of the display, used to calculate pixel dimensions. + * @param bitmapDescriptorFactory is an instance of the BitmapDescriptorFactoryWrapper. + * @param flutterInjector An instance of the FlutterInjectorWrapper class. + * @return BitmapDescriptor object from asset. + * @throws IllegalArgumentException if any required keys are missing in `assetDetails` or if the + * asset cannot be opened or processed as a valid image. + */ + @VisibleForTesting + public static BitmapDescriptor getBitmapFromAsset( + Map assetDetails, + AssetManager assetManager, + float density, + BitmapDescriptorFactoryWrapper bitmapDescriptorFactory, + FlutterInjectorWrapper flutterInjector) { + + final String assetNameKey = "assetName"; + final String bitmapScalingKey = "bitmapScaling"; + final String imagePixelRatioKey = "imagePixelRatio"; + + if (!assetDetails.containsKey(assetNameKey)) { + throw new IllegalArgumentException("'asset' requires '" + assetNameKey + "' key."); + } + if (!assetDetails.containsKey(bitmapScalingKey)) { + throw new IllegalArgumentException("'asset' requires '" + bitmapScalingKey + "' key."); + } + if (!assetDetails.containsKey(imagePixelRatioKey)) { + throw new IllegalArgumentException("'asset' requires '" + imagePixelRatioKey + "' key."); + } + + final String assetName = toString(assetDetails.get(assetNameKey)); + final String assetKey = flutterInjector.getLookupKeyForAsset(assetName); + + String scalingMode = toString(assetDetails.get(bitmapScalingKey)); + switch (scalingMode) { + case "auto": + final String widthKey = "width"; + final String heightKey = "height"; + + final Double width = + assetDetails.containsKey(widthKey) ? toDouble(assetDetails.get(widthKey)) : null; + final Double height = + assetDetails.containsKey(heightKey) ? toDouble(assetDetails.get(heightKey)) : null; + InputStream inputStream = null; + try { + inputStream = assetManager.open(assetKey); + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + + if (width != null || height != null) { + int targetWidth = width != null ? toInt(width * density) : bitmap.getWidth(); + int targetHeight = height != null ? toInt(height * density) : bitmap.getHeight(); + + if (width != null && height == null) { + // If only width is provided, calculate height based on aspect ratio. + double aspectRatio = (double) bitmap.getHeight() / bitmap.getWidth(); + targetHeight = (int) (targetWidth * aspectRatio); + } else if (height != null && width == null) { + // If only height is provided, calculate width based on aspect ratio. + double aspectRatio = (double) bitmap.getWidth() / bitmap.getHeight(); + targetWidth = (int) (targetHeight * aspectRatio); + } + return bitmapDescriptorFactory.fromBitmap( + toScaledBitmap(bitmap, targetWidth, targetHeight)); + } else { + // Scale image using given scale. + final float scale = density / toFloat(assetDetails.get(imagePixelRatioKey)); + return bitmapDescriptorFactory.fromBitmap(toScaledBitmap(bitmap, scale)); + } + } catch (Exception e) { + throw new IllegalArgumentException("'asset' cannot open asset: " + assetName, e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + case "none": + break; + } + + return bitmapDescriptorFactory.fromAsset(assetKey); + } + private static boolean toBoolean(Object o) { return (Boolean) o; } @@ -242,7 +447,8 @@ static Object clusterToJson(String clusterManagerId, Cluster clus String[] markerIds = new String[clusterSize]; MarkerBuilder[] markerBuilders = cluster.getItems().toArray(new MarkerBuilder[clusterSize]); - // Loops though cluster items and reads markers position for the LatLngBounds builder + // Loops though cluster items and reads markers position for the LatLngBounds + // builder // and also builds list of marker ids on the cluster. for (int i = 0; i < clusterSize; i++) { MarkerBuilder markerBuilder = markerBuilders[i]; @@ -328,6 +534,25 @@ private static Bitmap toBitmap(Object o) { } } + private static Bitmap toScaledBitmap(Bitmap bitmap, float scale) { + // Threshold to check if scaling is necessary. + final float scalingThreshold = 0.001f; + + if (Math.abs(scale - 1) > scalingThreshold && scale > 0) { + final int newWidth = (int) (bitmap.getWidth() * scale); + final int newHeight = (int) (bitmap.getHeight() * scale); + return toScaledBitmap(bitmap, newWidth, newHeight); + } + return bitmap; + } + + private static Bitmap toScaledBitmap(Bitmap bitmap, int width, int height) { + if (width > 0 && height > 0 && (bitmap.getWidth() != width || bitmap.getHeight() != height)) { + return Bitmap.createScaledBitmap(bitmap, width, height, true); + } + return bitmap; + } + private static Point toPoint(Object o, float density) { final List data = toList(o); return new Point(toPixels(data.get(0), density), toPixels(data.get(1), density)); @@ -427,7 +652,8 @@ static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { } /** Set the options in the given object to marker options sink. */ - static void interpretMarkerOptions(Object o, MarkerOptionsSink sink) { + static void interpretMarkerOptions( + Object o, MarkerOptionsSink sink, AssetManager assetManager, float density) { final Map data = toMap(o); final Object alpha = data.get("alpha"); if (alpha != null) { @@ -452,7 +678,7 @@ static void interpretMarkerOptions(Object o, MarkerOptionsSink sink) { } final Object icon = data.get("icon"); if (icon != null) { - sink.setIcon(toBitmapDescriptor(icon)); + sink.setIcon(toBitmapDescriptor(icon, assetManager, density)); } final Object infoWindow = data.get("infoWindow"); @@ -538,7 +764,8 @@ static String interpretPolygonOptions(Object o, PolygonOptionsSink sink) { } } - static String interpretPolylineOptions(Object o, PolylineOptionsSink sink) { + static String interpretPolylineOptions( + Object o, PolylineOptionsSink sink, AssetManager assetManager, float density) { final Map data = toMap(o); final Object consumeTapEvents = data.get("consumeTapEvents"); if (consumeTapEvents != null) { @@ -550,7 +777,7 @@ static String interpretPolylineOptions(Object o, PolylineOptionsSink sink) { } final Object endCap = data.get("endCap"); if (endCap != null) { - sink.setEndCap(toCap(endCap)); + sink.setEndCap(toCap(endCap, assetManager, density)); } final Object geodesic = data.get("geodesic"); if (geodesic != null) { @@ -562,7 +789,7 @@ static String interpretPolylineOptions(Object o, PolylineOptionsSink sink) { } final Object startCap = data.get("startCap"); if (startCap != null) { - sink.setStartCap(toCap(startCap)); + sink.setStartCap(toCap(startCap, assetManager, density)); } final Object visible = data.get("visible"); if (visible != null) { @@ -685,7 +912,7 @@ private static List toPattern(Object o) { return pattern; } - private static Cap toCap(Object o) { + private static Cap toCap(Object o, AssetManager assetManager, float density) { final List data = toList(o); switch (toString(data.get(0))) { case "buttCap": @@ -696,9 +923,10 @@ private static Cap toCap(Object o) { return new SquareCap(); case "customCap": if (data.size() == 2) { - return new CustomCap(toBitmapDescriptor(data.get(1))); + return new CustomCap(toBitmapDescriptor(data.get(1), assetManager, density)); } else { - return new CustomCap(toBitmapDescriptor(data.get(1)), toFloat(data.get(2))); + return new CustomCap( + toBitmapDescriptor(data.get(1), assetManager, density), toFloat(data.get(2))); } default: throw new IllegalArgumentException("Cannot interpret " + o + " as Cap"); @@ -739,4 +967,54 @@ static Tile interpretTile(Map data) { } return new Tile(width, height, dataArray); } + + @VisibleForTesting + static class BitmapDescriptorFactoryWrapper { + /** + * Creates a BitmapDescriptor from the provided asset key using the {@link + * BitmapDescriptorFactory}. + * + *

This method is visible for testing purposes only and should never be used outside Convert + * class. + * + * @param assetKey the key of the asset. + * @return a new instance of the {@link BitmapDescriptor}. + */ + @VisibleForTesting + public BitmapDescriptor fromAsset(String assetKey) { + return BitmapDescriptorFactory.fromAsset(assetKey); + } + + /** + * Creates a BitmapDescriptor from the provided bitmap using the {@link + * BitmapDescriptorFactory}. + * + *

This method is visible for testing purposes only and should never be used outside Convert + * class. + * + * @param bitmap the bitmap to convert. + * @return a new instance of the {@link BitmapDescriptor}. + */ + @VisibleForTesting + public BitmapDescriptor fromBitmap(Bitmap bitmap) { + return BitmapDescriptorFactory.fromBitmap(bitmap); + } + } + + @VisibleForTesting + static class FlutterInjectorWrapper { + /** + * Retrieves the lookup key for a given asset name using the {@link FlutterInjector}. + * + *

This method is visible for testing purposes only and should never be used outside Convert + * class. + * + * @param assetName the name of the asset. + * @return the lookup key for the asset. + */ + @VisibleForTesting + public String getLookupKeyForAsset(@NonNull String assetName) { + return FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(assetName); + } + } } 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 9a077ece3809..24d66fed24c5 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 @@ -8,6 +8,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; +import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.SurfaceTexture; @@ -115,11 +116,13 @@ class GoogleMapController methodChannel = new MethodChannel(binaryMessenger, "plugins.flutter.dev/google_maps_android_" + id); methodChannel.setMethodCallHandler(this); + AssetManager assetManager = context.getAssets(); this.lifecycleProvider = lifecycleProvider; this.clusterManagersController = new ClusterManagersController(methodChannel, context); - this.markersController = new MarkersController(methodChannel, clusterManagersController); + this.markersController = + new MarkersController(methodChannel, clusterManagersController, assetManager, density); this.polygonsController = new PolygonsController(methodChannel, density); - this.polylinesController = new PolylinesController(methodChannel, density); + this.polylinesController = new PolylinesController(methodChannel, assetManager, density); this.circlesController = new CirclesController(methodChannel, density); this.tileOverlaysController = new TileOverlaysController(methodChannel); } 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 fa88310e0eab..a95f2f34482c 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 @@ -4,6 +4,7 @@ package io.flutter.plugins.googlemaps; +import android.content.res.AssetManager; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; @@ -21,14 +22,21 @@ class MarkersController { private final MethodChannel methodChannel; private MarkerManager.Collection markerCollection; private final ClusterManagersController clusterManagersController; + private final AssetManager assetManager; + private final float density; MarkersController( - MethodChannel methodChannel, ClusterManagersController clusterManagersController) { + MethodChannel methodChannel, + ClusterManagersController clusterManagersController, + AssetManager assetManager, + float density) { this.markerIdToMarkerBuilder = new HashMap<>(); this.markerIdToController = new HashMap<>(); this.googleMapsMarkerIdToDartMarkerId = new HashMap<>(); this.methodChannel = methodChannel; this.clusterManagersController = clusterManagersController; + this.assetManager = assetManager; + this.density = density; } void setCollection(MarkerManager.Collection markerCollection) { @@ -192,7 +200,7 @@ private void addMarker(Object marker) { } String clusterManagerId = getClusterManagerId(marker); MarkerBuilder markerBuilder = new MarkerBuilder(markerId, clusterManagerId); - Convert.interpretMarkerOptions(marker, markerBuilder); + Convert.interpretMarkerOptions(marker, markerBuilder, assetManager, density); addMarker(markerBuilder); } @@ -251,12 +259,12 @@ private void changeMarker(Object marker) { } // Update marker builder. - Convert.interpretMarkerOptions(marker, markerBuilder); + Convert.interpretMarkerOptions(marker, markerBuilder, assetManager, density); // Update existing marker on map. MarkerController markerController = markerIdToController.get(markerId); if (markerController != null) { - Convert.interpretMarkerOptions(marker, markerController); + Convert.interpretMarkerOptions(marker, markerController, assetManager, density); } } 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 399634933dc9..2dbad98fcfea 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 @@ -4,6 +4,7 @@ package io.flutter.plugins.googlemaps; +import android.content.res.AssetManager; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.Polyline; import com.google.android.gms.maps.model.PolylineOptions; @@ -19,8 +20,10 @@ class PolylinesController { private final MethodChannel methodChannel; private GoogleMap googleMap; private final float density; + private final AssetManager assetManager; - PolylinesController(MethodChannel methodChannel, float density) { + PolylinesController(MethodChannel methodChannel, AssetManager assetManager, float density) { + this.assetManager = assetManager; this.polylineIdToController = new HashMap<>(); this.googleMapsPolylineIdToDartPolylineId = new HashMap<>(); this.methodChannel = methodChannel; @@ -82,7 +85,8 @@ private void addPolyline(Object polyline) { return; } PolylineBuilder polylineBuilder = new PolylineBuilder(density); - String polylineId = Convert.interpretPolylineOptions(polyline, polylineBuilder); + String polylineId = + Convert.interpretPolylineOptions(polyline, polylineBuilder, assetManager, density); PolylineOptions options = polylineBuilder.build(); addPolyline(polylineId, options, polylineBuilder.consumeTapEvents()); } @@ -102,7 +106,7 @@ private void changePolyline(Object polyline) { String polylineId = getPolylineId(polyline); PolylineController polylineController = polylineIdToController.get(polylineId); if (polylineController != null) { - Convert.interpretPolylineOptions(polyline, polylineController); + Convert.interpretPolylineOptions(polyline, polylineController, assetManager, 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 565c11f2dfd3..b218c29ad7c8 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 @@ -14,6 +14,7 @@ import static org.mockito.Mockito.when; import android.content.Context; +import android.content.res.AssetManager; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.maps.GoogleMap; @@ -34,6 +35,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -46,10 +48,14 @@ public class ClusterManagersControllerTest { private GoogleMap googleMap; private MarkerManager markerManager; private MarkerManager.Collection markerCollection; + private AssetManager assetManager; + private final float density = 1; @Before public void setUp() { + MockitoAnnotations.openMocks(this); context = ApplicationProvider.getApplicationContext(); + assetManager = context.getAssets(); methodChannel = spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); controller = spy(new ClusterManagersController(methodChannel, context)); @@ -93,8 +99,8 @@ public void AddClusterManagersAndMarkers() throws InterruptedException { final Map markerData2 = createMarkerData(markerId2, location2, clusterManagerId); - Convert.interpretMarkerOptions(markerData1, markerBuilder1); - Convert.interpretMarkerOptions(markerData2, markerBuilder2); + Convert.interpretMarkerOptions(markerData1, markerBuilder1, assetManager, density); + Convert.interpretMarkerOptions(markerData2, markerBuilder2, assetManager, density); 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 8f6c806838cc..abb7681e436b 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 @@ -4,18 +4,69 @@ package io.flutter.plugins.googlemaps; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +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 com.google.android.gms.maps.model.LatLng; import com.google.maps.android.clustering.Cluster; import com.google.maps.android.clustering.algo.StaticCluster; +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 java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +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.P) public class ConvertTest { + @Mock private AssetManager assetManager; + + @Mock private BitmapDescriptorFactoryWrapper bitmapDescriptorFactoryWrapper; + + @Mock private BitmapDescriptor mockBitmapDescriptor; + + @Mock private FlutterInjectorWrapper flutterInjectorWrapper; + + AutoCloseable mockCloseable; + + // A 1x1 pixel (#8080ff) PNG image encoded in base64 + private String base64Image = generateBase64Image(); + + @Before + public void before() { + mockCloseable = MockitoAnnotations.openMocks(this); + } + + @After + public void tearDown() throws Exception { + mockCloseable.close(); + } @Test public void ConvertToPointsConvertsThePointsWithFullPrecision() { @@ -87,4 +138,241 @@ public void ConvertClustersToJsonReturnsCorrectData() { Assert.assertEquals(marker1.markerId(), markerIdList.get(0)); Assert.assertEquals(marker2.markerId(), markerIdList.get(1)); } + + @Test + public void GetBitmapFromAssetAuto() throws Exception { + String fakeAssetName = "fake_asset_name"; + String fakeAssetKey = "fake_asset_key"; + Map assetDetails = new HashMap<>(); + assetDetails.put("assetName", fakeAssetName); + assetDetails.put("bitmapScaling", "auto"); + assetDetails.put("width", 15.0f); + assetDetails.put("height", 15.0f); + assetDetails.put("imagePixelRatio", 2.0f); + + when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); + + when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + BitmapDescriptor result = + Convert.getBitmapFromAsset( + assetDetails, + assetManager, + 1.0f, + bitmapDescriptorFactoryWrapper, + flutterInjectorWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test + public void GetBitmapFromAssetAutoAndWidth() throws Exception { + String fakeAssetName = "fake_asset_name"; + String fakeAssetKey = "fake_asset_key"; + + Map assetDetails = new HashMap<>(); + assetDetails.put("assetName", fakeAssetName); + assetDetails.put("bitmapScaling", "auto"); + assetDetails.put("width", 15.0f); + assetDetails.put("imagePixelRatio", 2.0f); + + when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); + + when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + BitmapDescriptor result = + Convert.getBitmapFromAsset( + assetDetails, + assetManager, + 1.0f, + bitmapDescriptorFactoryWrapper, + flutterInjectorWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test + public void GetBitmapFromAssetAutoAndHeight() throws Exception { + String fakeAssetName = "fake_asset_name"; + String fakeAssetKey = "fake_asset_key"; + + Map assetDetails = new HashMap<>(); + assetDetails.put("assetName", fakeAssetName); + assetDetails.put("bitmapScaling", "auto"); + assetDetails.put("height", 15.0f); + assetDetails.put("imagePixelRatio", 2.0f); + + when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); + + when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + BitmapDescriptor result = + Convert.getBitmapFromAsset( + assetDetails, + assetManager, + 1.0f, + bitmapDescriptorFactoryWrapper, + flutterInjectorWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test + public void GetBitmapFromAssetNoScaling() throws Exception { + String fakeAssetName = "fake_asset_name"; + String fakeAssetKey = "fake_asset_key"; + + Map assetDetails = new HashMap<>(); + assetDetails.put("assetName", fakeAssetName); + assetDetails.put("bitmapScaling", "noScaling"); + assetDetails.put("imagePixelRatio", 2.0f); + + when(flutterInjectorWrapper.getLookupKeyForAsset(fakeAssetName)).thenReturn(fakeAssetKey); + + when(assetManager.open(fakeAssetKey)).thenReturn(buildImageInputStream()); + + when(bitmapDescriptorFactoryWrapper.fromAsset(any())).thenReturn(mockBitmapDescriptor); + + verify(bitmapDescriptorFactoryWrapper, never()).fromBitmap(any()); + + BitmapDescriptor result = + Convert.getBitmapFromAsset( + assetDetails, + assetManager, + 1.0f, + bitmapDescriptorFactoryWrapper, + flutterInjectorWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test + public void GetBitmapFromBytesAuto() throws Exception { + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + + Map assetDetails = new HashMap<>(); + assetDetails.put("byteData", bmpData); + assetDetails.put("bitmapScaling", "auto"); + assetDetails.put("imagePixelRatio", 2.0f); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + BitmapDescriptor result = + Convert.getBitmapFromBytes(assetDetails, 1f, bitmapDescriptorFactoryWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test + public void GetBitmapFromBytesAutoAndWidth() throws Exception { + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + + Map assetDetails = new HashMap<>(); + assetDetails.put("byteData", bmpData); + assetDetails.put("bitmapScaling", "auto"); + assetDetails.put("imagePixelRatio", 2.0f); + assetDetails.put("width", 15.0f); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + BitmapDescriptor result = + Convert.getBitmapFromBytes(assetDetails, 1f, bitmapDescriptorFactoryWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test + public void GetBitmapFromBytesAutoAndHeight() throws Exception { + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + + Map assetDetails = new HashMap<>(); + assetDetails.put("byteData", bmpData); + assetDetails.put("bitmapScaling", "auto"); + assetDetails.put("imagePixelRatio", 2.0f); + assetDetails.put("height", 15.0f); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + BitmapDescriptor result = + Convert.getBitmapFromBytes(assetDetails, 1f, bitmapDescriptorFactoryWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test + public void GetBitmapFromBytesNoScaling() throws Exception { + byte[] bmpData = Base64.decode(base64Image, Base64.DEFAULT); + + Map assetDetails = new HashMap<>(); + assetDetails.put("byteData", bmpData); + assetDetails.put("bitmapScaling", "noScaling"); + assetDetails.put("imagePixelRatio", 2.0f); + + when(bitmapDescriptorFactoryWrapper.fromBitmap(any())).thenReturn(mockBitmapDescriptor); + + BitmapDescriptor result = + Convert.getBitmapFromBytes(assetDetails, 1f, bitmapDescriptorFactoryWrapper); + + Assert.assertEquals(mockBitmapDescriptor, result); + } + + @Test(expected = IllegalArgumentException.class) // Expecting an IllegalArgumentException + public void GetBitmapFromBytesThrowsErrorIfInvalidImageData() throws Exception { + String invalidBase64Image = "not valid image data"; + byte[] bmpData = Base64.decode(invalidBase64Image, Base64.DEFAULT); + + Map assetDetails = new HashMap<>(); + assetDetails.put("byteData", bmpData); + assetDetails.put("bitmapScaling", "noScaling"); + assetDetails.put("imagePixelRatio", 2.0f); + + verify(bitmapDescriptorFactoryWrapper, never()).fromBitmap(any()); + + try { + Convert.getBitmapFromBytes(assetDetails, 1f, bitmapDescriptorFactoryWrapper); + } catch (IllegalArgumentException e) { + Assert.assertEquals(e.getMessage(), "Unable to interpret bytes as a valid image."); + throw e; // rethrow the exception + } + + fail("Expected an IllegalArgumentException to be thrown"); + } + + 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(); + InputStream fakeStream = new ByteArrayInputStream(byteArray); + return fakeStream; + } + + // Helper method to generate 1x1 pixel base64 encoded png test image + private String generateBase64Image() { + 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.parseColor("#FF8080FF")); + 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 + String base64Image = Base64.encodeToString(pngBytes, Base64.DEFAULT); + + return base64Image; + } } 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 581a0dbfc5c6..116f8381bce3 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 @@ -12,6 +12,7 @@ import static org.mockito.Mockito.when; import android.content.Context; +import android.content.res.AssetManager; import android.os.Build; import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.maps.GoogleMap; @@ -32,6 +33,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -45,14 +47,19 @@ public class MarkersControllerTest { private GoogleMap googleMap; private MarkerManager markerManager; private MarkerManager.Collection markerCollection; + private AssetManager assetManager; + private final float density = 1; @Before public void setUp() { + MockitoAnnotations.openMocks(this); + assetManager = ApplicationProvider.getApplicationContext().getAssets(); context = ApplicationProvider.getApplicationContext(); methodChannel = spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); clusterManagersController = spy(new ClusterManagersController(methodChannel, context)); - controller = new MarkersController(methodChannel, clusterManagersController); + controller = + new MarkersController(methodChannel, clusterManagersController, assetManager, density); googleMap = mock(GoogleMap.class); markerManager = new MarkerManager(googleMap); markerCollection = markerManager.newCollection(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart index 858d1fbc44a1..277010adfeba 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -13,6 +14,8 @@ import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; import 'package:google_maps_flutter_example/example_google_map.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'resources/icon_image_base64.dart'; + const LatLng _kInitialMapCenter = LatLng(0, 0); const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = @@ -967,19 +970,6 @@ void googleMapsTests() { expect(iwVisibleStatus, false); }); - testWidgets('fromAssetImage', (WidgetTester tester) async { - const double pixelRatio = 2; - const ImageConfiguration imageConfiguration = - ImageConfiguration(devicePixelRatio: pixelRatio); - final BitmapDescriptor mip = await BitmapDescriptor.fromAssetImage( - imageConfiguration, 'red_square.png'); - final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( - imageConfiguration, 'red_square.png', - mipmaps: false); - expect((mip.toJson() as List)[2], 1); - expect((scaled.toJson() as List)[2], 2); - }); - testWidgets('testTakeSnapshot', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -1346,6 +1336,112 @@ void googleMapsTests() { final String? error = await controller.getStyleError(); expect(error, isNull); }); + + testWidgets('markerWithAssetMapBitmap', (WidgetTester tester) async { + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + )), + }; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + }); + + testWidgets('markerWithAssetMapBitmapCreate', (WidgetTester tester) async { + final ImageConfiguration imageConfiguration = ImageConfiguration( + devicePixelRatio: tester.view.devicePixelRatio, + ); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: await AssetMapBitmap.create( + imageConfiguration, + 'assets/red_square.png', + )), + }; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + }); + + testWidgets('markerWithBytesMapBitmap', (WidgetTester tester) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: tester.view.devicePixelRatio, + ), + ), + }; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + }); + + testWidgets('markerWithLegacyAsset', (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + final ImageConfiguration imageConfiguration = ImageConfiguration( + devicePixelRatio: tester.view.devicePixelRatio, + size: const Size(100, 100), + ); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: await BitmapDescriptor.fromAssetImage( + imageConfiguration, + 'assets/red_square.png', + )), + }; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + + await tester.pumpAndSettle(); + }); + + testWidgets('markerWithLegacyBytes', (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: BitmapDescriptor.fromBytes( + bytes, + size: const Size(100, 100), + )), + }; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + + await tester.pumpAndSettle(); + }); } class _DebugTileProvider implements TileProvider { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image.png b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image.png new file mode 100644 index 0000000000000000000000000000000000000000..920b93f74d7875578cfddd9fb3a86ffc91cc66aa GIT binary patch literal 1257 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|T2doC(|mmy zw18|523AHP24;{FAY@>aVqgWc85q16rQz%#Mh&PMCI*J~Oa>OHnkXO*0v4nJa0`PlBg3pY5xV%QuQiw3qZOUY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb09I0xZL1XF8=&Bv zUzDm~s%N5Spk&9TprBw=l#*r@P?Wt5Z@Sn2DRmzV368|&p4rRy77T3YHG z80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{w8U0P31kr*K-^i9 znTD__uNdkrpa=CqGWv#k2KsQbfm&@qqE`MznW;dVLFU^T+JIG}h(YbK(Fa+MGhS)(5hysGVj@1S=_(gPCCO{ zqhwks#Vc``|8 z#o#O*d;N2|BC{jvU{z}(n^^Vap!a1%`y(*n{sOkJ|#X3oe;O!Zo?Dnhr)@GQ`z`?mdD=yHSxr< zDy_rs-x{jCabub6U?S+*S@!Va>4+T*Hmzz(*!gva0^=QR?pv-qbGCN6d|Si4{*vsJ zlH0zk1sd5rv=;3+XYfL0!@WaC`Ahb=eYxm+=guaBQ~enqLO(hNXMfPrI^{pDhK z-4_e9Y*sb;Jj~0Qs$sV`^@y=`QNlJK=FPXKOL(O1`}w`Gn*UDUbd&Ixz5e<2va`xH z41-N}zw;G7BCl8SdS-H5ozdgaGryVtGld%!O3p1V7HRwR?9co=RqwyAbPax;#ec&s zvsY)SyxMWE30Fc-y!yt-7}jeScd5ucNzCQ_c1QOYYo)m6PP)2@Gs?hW+Rx4&FMFT6 zT8Ccju)dun6cb3O7sF#-G(!!nzha!-kf?C|8-&R&#t@} QJ5YZ2boFyt=akR{0O*p@82|tP literal 0 HcmV?d00001 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image_base64.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image_base64.dart new file mode 100644 index 000000000000..1bfc791ca385 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image_base64.dart @@ -0,0 +1,49 @@ +// 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. + +/// This constant holds the base64-encoded data of a 16x16 PNG image of the +/// Flutter logo. +/// +/// See `icon_image.png` source in the same directory. +/// +/// To create or update this image, follow these steps: +/// 1. Create or update a 16x16 PNG image. +/// 2. Convert the image to a base64 string using a script below. +/// 3. Replace the existing base64 string below with the new one. +/// +/// Example of converting an image to base64 in Dart: +/// ```dart +/// import 'dart:convert'; +/// import 'dart:io'; +/// +/// void main() async { +/// final bytes = await File('icon_image.png').readAsBytes(); +/// final base64String = base64Encode(bytes); +/// print(base64String); +/// } +/// ``` +const String iconImageBase64 = + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIRlWElmTU' + '0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIA' + 'AIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQ' + 'AAABCgAwAEAAAAAQAAABAAAAAAx28c8QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1M' + 'OmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIH' + 'g6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8v' + 'd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcm' + 'lwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFk' + 'b2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk' + '9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6' + 'eG1wbWV0YT4KTMInWQAAAplJREFUOBF1k01ME1EQx2fe7tIPoGgTE6AJgQQSPaiH9oAtkFbsgX' + 'jygFcT0XjSkxcTDxtPJh6MR28ePMHBBA8cNLSIony0oBhEMVETP058tE132+7uG3cW24DAXN57' + '2fn9/zPz3iIcEdEl0nIxtNLr1IlVeoMadkubKmoL+u2SzAV8IjV5Ekt4GN+A8+VOUPwLarOI2G' + 'Vpqq0i4JQorwQxPtWHVZ1IKP8LNGDXGaSyqARFxDGo7MJBy4XVf3AyQ+qTHnTEXoF9cFUy3OkY' + '0oWxmWFtD5xNoc1sQ6AOn1+hCNTkkhKow8KFZV77tVs2O9dhFvBm0IA/U0RhZ7/ocEx23oUDlh' + 'h8HkNjZIN8Lb3gOU8gOp7AKJHCB2/aNZkTftHumNzzbtl2CBPZHqxw8mHhVZBeoz6w5DvhE2FZ' + 'lQYPjKdd2/qRyKZ6KsPv7TEk7EYEk0A0EUmJduHRy1i4oLKqgmC59ZggAdwrC9pFuWy1iUT2rA' + 'uv0h2UdNtNqxCBBkgqorjOMOgksN7CxQ90vEb00U3c3LIwyo9o8FXxQVNr8Coqyk+S5EPBXnjt' + 'xRmc4TegI7qWbvBkeeUbGMnTCd4nZnYeDOWIEtlC6cKK/JJepY3hZSvN33jovO6L0XFqPKqBTO' + 'FuapUoPr1lxDM7cmC2TAOz25cYSGa++feBew/cjpc0V+mNT29/HZp3KDFTNLvuTRPEHy5065lj' + 'Xn4y41XM+wP/AlcycRmdc3MUhvLm/J/ceu/3qUVT62oP2EZpjSylHybHSpDUVcjq9gEBVo0+Xt' + 'JyN2IWRO+3QUforRoKnZLVsglaMECW+YmMSj9M3SrC6Lg71CMiqWfUrJ6ywzefhnZ+G69BaKdB' + 'WhXQAn6wzDUpfUPw7MrmX/WhbfmKblw+AAAAAElFTkSuQmCC'; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/custom_marker_icon.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/custom_marker_icon.dart new file mode 100644 index 000000000000..8940762f02e4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/custom_marker_icon.dart @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +/// Returns a generated png image in [ByteData] format with the requested size. +Future createCustomMarkerIconImage({required Size size}) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final _MarkerPainter painter = _MarkerPainter(); + + painter.paint(canvas, size); + + final ui.Image image = await recorder + .endRecording() + .toImage(size.width.floor(), size.height.floor()); + + final ByteData? bytes = + await image.toByteData(format: ui.ImageByteFormat.png); + return bytes!; +} + +class _MarkerPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + const RadialGradient gradient = RadialGradient( + colors: [Colors.yellow, Colors.red], + stops: [0.4, 1.0], + ); + + // Draw radial gradient + canvas.drawRect( + rect, + Paint()..shader = gradient.createShader(rect), + ); + + // Draw diagonal black line + canvas.drawLine( + Offset.zero, + Offset(size.width, size.height), + Paint() + ..color = Colors.black + ..strokeWidth = 1, + ); + } + + @override + bool shouldRepaint(_MarkerPainter oldDelegate) => false; + @override + bool shouldRebuildSemantics(_MarkerPainter oldDelegate) => false; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart index 174055613a9e..df4f79205e82 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/marker_icons.dart @@ -5,9 +5,13 @@ // ignore_for_file: public_member_api_docs // ignore_for_file: unawaited_futures +import 'dart:async'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'custom_marker_icon.dart'; import 'example_google_map.dart'; import 'page.dart'; @@ -30,66 +34,303 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); +enum _MarkerSizeOption { + original, + width30, + height40, + size30x60, + size120x60, +} + class MarkerIconsBodyState extends State { + final Size _markerAssetImageSize = const Size(48, 48); + _MarkerSizeOption _currentSizeOption = _MarkerSizeOption.original; + Set _markers = {}; + bool _scalingEnabled = true; + bool _mipMapsEnabled = true; ExampleGoogleMapController? controller; - BitmapDescriptor? _markerIcon; + AssetMapBitmap? _markerIconAsset; + BytesMapBitmap? _markerIconBytes; + final int _markersAmountPerType = 15; + bool get _customSizeEnabled => + _currentSizeOption != _MarkerSizeOption.original; @override Widget build(BuildContext context) { - _createMarkerImageFromAsset(context); + _createCustomMarkerIconImages(context); + final Size referenceSize = _getMarkerReferenceSize(); return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Center( - child: SizedBox( - width: 350.0, - height: 300.0, - child: ExampleGoogleMap( - initialCameraPosition: const CameraPosition( - target: _kMapCenter, - zoom: 7.0, + Column(children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + markers: _markers, + onMapCreated: _onMapCreated, + ), + ), + ), + TextButton( + onPressed: () => _toggleScaling(context), + child: Text(_scalingEnabled + ? 'Disable auto scaling' + : 'Enable auto scaling'), + ), + if (_scalingEnabled) ...[ + Container( + width: referenceSize.width, + height: referenceSize.height, + decoration: BoxDecoration( + border: Border.all(), ), - markers: {_createMarker()}, - onMapCreated: _onMapCreated, ), + Text( + 'Reference box with size of ${referenceSize.width} x ${referenceSize.height} in logical pixels.'), + const SizedBox(height: 10), + Image.asset( + 'assets/red_square.png', + scale: _mipMapsEnabled ? null : 1.0, + ), + const Text('Asset image rendered with flutter'), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Marker size:'), + const SizedBox(width: 10), + DropdownButton<_MarkerSizeOption>( + value: _currentSizeOption, + onChanged: (_MarkerSizeOption? newValue) { + if (newValue != null) { + setState(() { + _currentSizeOption = newValue; + _updateMarkerImages(context); + }); + } + }, + items: + _MarkerSizeOption.values.map((_MarkerSizeOption option) { + return DropdownMenuItem<_MarkerSizeOption>( + value: option, + child: Text(_getMarkerSizeOptionName(option)), + ); + }).toList(), + ) + ], + ), + ], + TextButton( + onPressed: () => _toggleMipMaps(context), + child: Text(_mipMapsEnabled ? 'Disable mipmaps' : 'Enable mipmaps'), ), - ) + ]) ], ); } - Marker _createMarker() { - if (_markerIcon != null) { - return Marker( - markerId: const MarkerId('marker_1'), - position: _kMapCenter, - icon: _markerIcon!, - ); + String _getMarkerSizeOptionName(_MarkerSizeOption option) { + switch (option) { + case _MarkerSizeOption.original: + return 'Original'; + case _MarkerSizeOption.width30: + return 'Width 30'; + case _MarkerSizeOption.height40: + return 'Height 40'; + case _MarkerSizeOption.size30x60: + return '30x60'; + case _MarkerSizeOption.size120x60: + return '120x60'; + } + } + + (double? width, double? height) _getCurrentMarkerSize() { + if (_scalingEnabled) { + switch (_currentSizeOption) { + case _MarkerSizeOption.width30: + return (30, null); + case _MarkerSizeOption.height40: + return (null, 40); + case _MarkerSizeOption.size30x60: + return (30, 60); + case _MarkerSizeOption.size120x60: + return (120, 60); + case _MarkerSizeOption.original: + return (_markerAssetImageSize.width, _markerAssetImageSize.height); + } } else { - return const Marker( - markerId: MarkerId('marker_1'), - position: _kMapCenter, - ); + return (_markerAssetImageSize.width, _markerAssetImageSize.height); } } - Future _createMarkerImageFromAsset(BuildContext context) async { - if (_markerIcon == null) { - final ImageConfiguration imageConfiguration = - createLocalImageConfiguration(context, size: const Size.square(48)); - BitmapDescriptor.fromAssetImage( - imageConfiguration, 'assets/red_square.png') - .then(_updateBitmap); + // Helper method to calculate reference size for custom marker size. + Size _getMarkerReferenceSize() { + final (double? width, double? height) = _getCurrentMarkerSize(); + + // Calculates reference size using _markerAssetImageSize aspect ration: + + if (width != null && height != null) { + return Size(width, height); + } else if (width != null) { + return Size(width, + width * _markerAssetImageSize.height / _markerAssetImageSize.width); + } else if (height != null) { + return Size( + height * _markerAssetImageSize.width / _markerAssetImageSize.height, + height); + } else { + return _markerAssetImageSize; } } - void _updateBitmap(BitmapDescriptor bitmap) { + void _toggleMipMaps(BuildContext context) { + _mipMapsEnabled = !_mipMapsEnabled; + _updateMarkerImages(context); + } + + void _toggleScaling(BuildContext context) { + _scalingEnabled = !_scalingEnabled; + _updateMarkerImages(context); + } + + void _updateMarkerImages(BuildContext context) { + _updateMarkerAssetImage(context); + _updateMarkerBytesImage(context); + _updateMarkers(); + } + + Marker _createAssetMarker(int index) { + final LatLng position = + LatLng(_kMapCenter.latitude - (index * 0.5), _kMapCenter.longitude - 1); + + return Marker( + markerId: MarkerId('marker_asset_$index'), + position: position, + icon: _markerIconAsset!, + ); + } + + Marker _createBytesMarker(int index) { + final LatLng position = + LatLng(_kMapCenter.latitude - (index * 0.5), _kMapCenter.longitude + 1); + + return Marker( + markerId: MarkerId('marker_bytes_$index'), + position: position, + icon: _markerIconBytes!, + ); + } + + void _updateMarkers() { + final Set markers = {}; + for (int i = 0; i < _markersAmountPerType; i++) { + if (_markerIconAsset != null) { + markers.add(_createAssetMarker(i)); + } + if (_markerIconBytes != null) { + markers.add(_createBytesMarker(i)); + } + } setState(() { - _markerIcon = bitmap; + _markers = markers; }); } + Future _updateMarkerAssetImage(BuildContext context) async { + // Width and height are used only for custom size. + final (double? width, double? height) = + _scalingEnabled && _customSizeEnabled + ? _getCurrentMarkerSize() + : (null, null); + + AssetMapBitmap assetMapBitmap; + if (_mipMapsEnabled) { + final ImageConfiguration imageConfiguration = + createLocalImageConfiguration( + context, + ); + + assetMapBitmap = await AssetMapBitmap.create( + imageConfiguration, + 'assets/red_square.png', + width: width, + height: height, + bitmapScaling: + _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none, + ); + } else { + // Uses hardcoded asset path + // This bypasses the asset resolving logic and allows to load the asset + // with precise path. + assetMapBitmap = AssetMapBitmap( + 'assets/red_square.png', + width: width, + height: height, + bitmapScaling: + _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none, + ); + } + + _updateAssetBitmap(assetMapBitmap); + } + + Future _updateMarkerBytesImage(BuildContext context) async { + final double? devicePixelRatio = + MediaQuery.maybeDevicePixelRatioOf(context); + + final Size bitmapLogicalSize = _getMarkerReferenceSize(); + final double? imagePixelRatio = _scalingEnabled ? devicePixelRatio : null; + + // Create canvasSize with physical marker size + final Size canvasSize = Size( + bitmapLogicalSize.width * (imagePixelRatio ?? 1.0), + bitmapLogicalSize.height * (imagePixelRatio ?? 1.0)); + + final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); + + // Width and height are used only for custom size. + final (double? width, double? height) = + _scalingEnabled && _customSizeEnabled + ? _getCurrentMarkerSize() + : (null, null); + + final BytesMapBitmap bitmap = BytesMapBitmap(bytes.buffer.asUint8List(), + imagePixelRatio: imagePixelRatio, + width: width, + height: height, + bitmapScaling: + _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none); + + _updateBytesBitmap(bitmap); + } + + void _updateAssetBitmap(AssetMapBitmap bitmap) { + _markerIconAsset = bitmap; + _updateMarkers(); + } + + void _updateBytesBitmap(BytesMapBitmap bitmap) { + _markerIconBytes = bitmap; + _updateMarkers(); + } + + void _createCustomMarkerIconImages(BuildContext context) { + if (_markerIconAsset == null) { + _updateMarkerAssetImage(context); + } + + if (_markerIconBytes == null) { + _updateMarkerBytesImage(context); + } + } + void _onMapCreated(ExampleGoogleMapController controllerParam) { setState(() { controller = controllerParam; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart index 9cba4975d40f..d475787c92fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart @@ -7,11 +7,11 @@ import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'custom_marker_icon.dart'; import 'example_google_map.dart'; import 'page.dart'; @@ -267,26 +267,10 @@ class PlaceMarkerBodyState extends State { }); } - Future _getAssetIcon(BuildContext context) async { - final Completer bitmapIcon = - Completer(); - final ImageConfiguration config = createLocalImageConfiguration(context); - - const AssetImage('assets/red_square.png') - .resolve(config) - .addListener(ImageStreamListener((ImageInfo image, bool sync) async { - final ByteData? bytes = - await image.image.toByteData(format: ImageByteFormat.png); - if (bytes == null) { - bitmapIcon.completeError(Exception('Unable to encode icon')); - return; - } - final BitmapDescriptor bitmap = - BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); - bitmapIcon.complete(bitmap); - })); - - return bitmapIcon.future; + Future _getMarkerIcon(BuildContext context) async { + const Size canvasSize = Size(48, 48); + final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); + return BytesMapBitmap(bytes.buffer.asUint8List()); } @override @@ -383,7 +367,7 @@ class PlaceMarkerBodyState extends State { onPressed: selectedId == null ? null : () { - _getAssetIcon(context).then( + _getMarkerIcon(context).then( (BitmapDescriptor icon) { _setMarkerIcon(selectedId, icon); }, 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 62c94d4d2fc5..ddac3d7b231c 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 @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_maps_flutter_platform_interface: ^2.6.0 + google_maps_flutter_platform_interface: ^2.7.0 dev_dependencies: build_runner: ^2.1.10 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 73625d794219..f14b973f0d51 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.8.1 +version: 2.9.0 environment: sdk: ^3.4.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.6.0 + google_maps_flutter_platform_interface: ^2.7.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_ios/AUTHORS index 9f1b53ee2667..4fc3ace39f0f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter_ios/AUTHORS @@ -65,3 +65,4 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Taha Tesser +Joonas Kerttula diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md index 3a0195b6ca0b..a43cf9c5d5f0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.7.0 + +* Adds support for BitmapDescriptor classes `AssetMapBitmap` and `BytesMapBitmap`. + ## 2.6.1 * Adds support for patterns in polylines. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart index e49c451d08ee..db8b39567635 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -12,6 +13,8 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:integration_test/integration_test.dart'; import 'package:maps_example_dart/example_google_map.dart'; +import 'resources/icon_image_base64.dart'; + const LatLng _kInitialMapCenter = LatLng(0, 0); const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = @@ -818,19 +821,6 @@ void main() { expect(iwVisibleStatus, false); }); - testWidgets('fromAssetImage', (WidgetTester tester) async { - const double pixelRatio = 2; - const ImageConfiguration imageConfiguration = - ImageConfiguration(devicePixelRatio: pixelRatio); - final BitmapDescriptor mip = await BitmapDescriptor.fromAssetImage( - imageConfiguration, 'red_square.png'); - final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( - imageConfiguration, 'red_square.png', - mipmaps: false); - expect((mip.toJson() as List)[2], 1); - expect((scaled.toJson() as List)[2], 2); - }); - testWidgets('testTakeSnapshot', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -1100,6 +1090,125 @@ void main() { final String? error = await controller.getStyleError(); expect(error, isNull); }); + + testWidgets('markerWithAssetMapBitmap', (WidgetTester tester) async { + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + )), + }; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + + await tester.pumpAndSettle(); + }); + + testWidgets('markerWithAssetMapBitmapCreate', (WidgetTester tester) async { + final ImageConfiguration imageConfiguration = ImageConfiguration( + devicePixelRatio: tester.view.devicePixelRatio, + ); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: await AssetMapBitmap.create( + imageConfiguration, + 'assets/red_square.png', + )), + }; + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + + await tester.pumpAndSettle(); + }); + + testWidgets('markerWithBytesMapBitmap', (WidgetTester tester) async { + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: tester.view.devicePixelRatio, + ), + ), + }; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + ), + )); + + await tester.pumpAndSettle(); + }); + + testWidgets('markerWithLegacyAsset', (WidgetTester tester) async { + //tester.view.devicePixelRatio = 2.0; + const ImageConfiguration imageConfiguration = ImageConfiguration( + devicePixelRatio: 2.0, + size: Size(100, 100), + ); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: await BitmapDescriptor.fromAssetImage( + imageConfiguration, + 'assets/red_square.png', + )), + }; + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + onMapCreated: (ExampleGoogleMapController controller) => + controllerCompleter.complete(controller), + ), + )); + + await controllerCompleter.future; + }); + + testWidgets('markerWithLegacyBytes', (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final BitmapDescriptor icon = BitmapDescriptor.fromBytes( + bytes, + ); + + final Set markers = { + Marker(markerId: const MarkerId('1'), icon: icon), + }; + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + onMapCreated: (ExampleGoogleMapController controller) => + controllerCompleter.complete(controller), + ), + )); + await controllerCompleter.future; + }); } class _DebugTileProvider implements TileProvider { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image.png new file mode 100644 index 0000000000000000000000000000000000000000..920b93f74d7875578cfddd9fb3a86ffc91cc66aa GIT binary patch literal 1257 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|T2doC(|mmy zw18|523AHP24;{FAY@>aVqgWc85q16rQz%#Mh&PMCI*J~Oa>OHnkXO*0v4nJa0`PlBg3pY5xV%QuQiw3qZOUY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb09I0xZL1XF8=&Bv zUzDm~s%N5Spk&9TprBw=l#*r@P?Wt5Z@Sn2DRmzV368|&p4rRy77T3YHG z80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{w8U0P31kr*K-^i9 znTD__uNdkrpa=CqGWv#k2KsQbfm&@qqE`MznW;dVLFU^T+JIG}h(YbK(Fa+MGhS)(5hysGVj@1S=_(gPCCO{ zqhwks#Vc``|8 z#o#O*d;N2|BC{jvU{z}(n^^Vap!a1%`y(*n{sOkJ|#X3oe;O!Zo?Dnhr)@GQ`z`?mdD=yHSxr< zDy_rs-x{jCabub6U?S+*S@!Va>4+T*Hmzz(*!gva0^=QR?pv-qbGCN6d|Si4{*vsJ zlH0zk1sd5rv=;3+XYfL0!@WaC`Ahb=eYxm+=guaBQ~enqLO(hNXMfPrI^{pDhK z-4_e9Y*sb;Jj~0Qs$sV`^@y=`QNlJK=FPXKOL(O1`}w`Gn*UDUbd&Ixz5e<2va`xH z41-N}zw;G7BCl8SdS-H5ozdgaGryVtGld%!O3p1V7HRwR?9co=RqwyAbPax;#ec&s zvsY)SyxMWE30Fc-y!yt-7}jeScd5ucNzCQ_c1QOYYo)m6PP)2@Gs?hW+Rx4&FMFT6 zT8Ccju)dun6cb3O7sF#-G(!!nzha!-kf?C|8-&R&#t@} QJ5YZ2boFyt=akR{0O*p@82|tP literal 0 HcmV?d00001 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image_base64.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image_base64.dart new file mode 100644 index 000000000000..1bfc791ca385 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image_base64.dart @@ -0,0 +1,49 @@ +// 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. + +/// This constant holds the base64-encoded data of a 16x16 PNG image of the +/// Flutter logo. +/// +/// See `icon_image.png` source in the same directory. +/// +/// To create or update this image, follow these steps: +/// 1. Create or update a 16x16 PNG image. +/// 2. Convert the image to a base64 string using a script below. +/// 3. Replace the existing base64 string below with the new one. +/// +/// Example of converting an image to base64 in Dart: +/// ```dart +/// import 'dart:convert'; +/// import 'dart:io'; +/// +/// void main() async { +/// final bytes = await File('icon_image.png').readAsBytes(); +/// final base64String = base64Encode(bytes); +/// print(base64String); +/// } +/// ``` +const String iconImageBase64 = + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIRlWElmTU' + '0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIA' + 'AIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQ' + 'AAABCgAwAEAAAAAQAAABAAAAAAx28c8QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1M' + 'OmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIH' + 'g6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8v' + 'd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcm' + 'lwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFk' + 'b2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk' + '9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6' + 'eG1wbWV0YT4KTMInWQAAAplJREFUOBF1k01ME1EQx2fe7tIPoGgTE6AJgQQSPaiH9oAtkFbsgX' + 'jygFcT0XjSkxcTDxtPJh6MR28ePMHBBA8cNLSIony0oBhEMVETP058tE132+7uG3cW24DAXN57' + '2fn9/zPz3iIcEdEl0nIxtNLr1IlVeoMadkubKmoL+u2SzAV8IjV5Ekt4GN+A8+VOUPwLarOI2G' + 'Vpqq0i4JQorwQxPtWHVZ1IKP8LNGDXGaSyqARFxDGo7MJBy4XVf3AyQ+qTHnTEXoF9cFUy3OkY' + '0oWxmWFtD5xNoc1sQ6AOn1+hCNTkkhKow8KFZV77tVs2O9dhFvBm0IA/U0RhZ7/ocEx23oUDlh' + 'h8HkNjZIN8Lb3gOU8gOp7AKJHCB2/aNZkTftHumNzzbtl2CBPZHqxw8mHhVZBeoz6w5DvhE2FZ' + 'lQYPjKdd2/qRyKZ6KsPv7TEk7EYEk0A0EUmJduHRy1i4oLKqgmC59ZggAdwrC9pFuWy1iUT2rA' + 'uv0h2UdNtNqxCBBkgqorjOMOgksN7CxQ90vEb00U3c3LIwyo9o8FXxQVNr8Coqyk+S5EPBXnjt' + 'xRmc4TegI7qWbvBkeeUbGMnTCd4nZnYeDOWIEtlC6cKK/JJepY3hZSvN33jovO6L0XFqPKqBTO' + 'FuapUoPr1lxDM7cmC2TAOz25cYSGa++feBew/cjpc0V+mNT29/HZp3KDFTNLvuTRPEHy5065lj' + 'Xn4y41XM+wP/AlcycRmdc3MUhvLm/J/ceu/3qUVT62oP2EZpjSylHybHSpDUVcjq9gEBVo0+Xt' + 'JyN2IWRO+3QUforRoKnZLVsglaMECW+YmMSj9M3SrC6Lg71CMiqWfUrJ6ywzefhnZ+G69BaKdB' + 'WhXQAn6wzDUpfUPw7MrmX/WhbfmKblw+AAAAAElFTkSuQmCC'; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj index 660b466a2fdf..10545d750ed3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; 478116522BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */; }; 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */; }; + 521AB0032B876A76005F460D /* ExtractIconFromDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 521AB0022B876A76005F460D /* ExtractIconFromDataTests.m */; }; 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68E472692836FF0C00BDDDAC /* MapKit.framework */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; @@ -63,6 +64,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsPolylinesControllerTests.m; sourceTree = ""; }; 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTGoogleMapJSONConversionsConversionTests.m; sourceTree = ""; }; + 521AB0022B876A76005F460D /* ExtractIconFromDataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExtractIconFromDataTests.m; sourceTree = ""; }; 68E472692836FF0C00BDDDAC /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.0.sdk/System/iOSSupport/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -204,6 +206,7 @@ children = ( F269303A2BB389BF00BF17C4 /* assets */, 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */, + 521AB0022B876A76005F460D /* ExtractIconFromDataTests.m */, F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, 478116512BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m */, 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */, @@ -472,6 +475,7 @@ 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */, 478116522BEF8F47002F593E /* GoogleMapsPolylinesControllerTests.m in Sources */, 0DD7B6C32B744EEF00E857FD /* FLTTileProviderControllerTests.m in Sources */, + 521AB0032B876A76005F460D /* ExtractIconFromDataTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m new file mode 100644 index 000000000000..c3d6a363e0c4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/ExtractIconFromDataTests.m @@ -0,0 +1,371 @@ +// 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 google_maps_flutter_ios; +@import google_maps_flutter_ios.Test; +@import XCTest; +#import +#import + +@interface ExtractIconFromDataTests : XCTestCase +- (UIImage *)createOnePixelImage; +@end + +@implementation ExtractIconFromDataTests + +- (void)testExtractIconFromDataAssetAuto { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + id mockImageClass = OCMClassMock([UIImage class]); + UIImage *testImage = [self createOnePixelImage]; + OCMStub([mockRegistrar lookupKeyForAsset:@"fakeImageNameKey"]).andReturn(@"fakeAssetKey"); + OCMStub(ClassMethod([mockImageClass imageNamed:@"fakeAssetKey"])).andReturn(testImage); + + NSDictionary *assetData = + @{@"assetName" : @"fakeImageNameKey", @"bitmapScaling" : @"auto", @"imagePixelRatio" : @1}; + + NSArray *iconData = @[ @"asset", assetData ]; + + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, 1.0); + XCTAssertEqual(resultImage.size.width, 1.0); + XCTAssertEqual(resultImage.size.height, 1.0); +} + +- (void)testExtractIconFromDataAssetAutoWithScale { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + id mockImageClass = OCMClassMock([UIImage class]); + UIImage *testImage = [self createOnePixelImage]; + + OCMStub([mockRegistrar lookupKeyForAsset:@"fakeImageNameKey"]).andReturn(@"fakeAssetKey"); + OCMStub(ClassMethod([mockImageClass imageNamed:@"fakeAssetKey"])).andReturn(testImage); + + NSDictionary *assetData = + @{@"assetName" : @"fakeImageNameKey", @"bitmapScaling" : @"auto", @"imagePixelRatio" : @10}; + + NSArray *iconData = @[ @"asset", assetData ]; + + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, 10); + XCTAssertEqual(resultImage.size.width, 0.1); + XCTAssertEqual(resultImage.size.height, 0.1); +} + +- (void)testExtractIconFromDataAssetAutoAndSizeWithSameAspectRatio { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + id mockImageClass = OCMClassMock([UIImage class]); + UIImage *testImage = [self createOnePixelImage]; + XCTAssertEqual(testImage.scale, 1.0); + + OCMStub([mockRegistrar lookupKeyForAsset:@"fakeImageNameKey"]).andReturn(@"fakeAssetKey"); + OCMStub(ClassMethod([mockImageClass imageNamed:@"fakeAssetKey"])).andReturn(testImage); + + NSDictionary *assetData = @{ + @"assetName" : @"fakeImageNameKey", + @"bitmapScaling" : @"auto", + @"imagePixelRatio" : @1, + @"width" : @15.0 + }; // Target height + + NSArray *iconData = @[ @"asset", assetData ]; + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + XCTAssertNotNil(resultImage); + XCTAssertEqual(testImage.scale, 1.0); + + // As image has same aspect ratio as the original image, + // only image scale has been changed to match the target size. + CGFloat targetScale = testImage.scale * (testImage.size.width / 15.0); + const CGFloat accuracy = 0.001; + XCTAssertEqualWithAccuracy(resultImage.scale, targetScale, accuracy); + XCTAssertEqual(resultImage.size.width, 15.0); + XCTAssertEqual(resultImage.size.height, 15.0); +} + +- (void)testExtractIconFromDataAssetAutoAndSizeWithDifferentAspectRatio { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + id mockImageClass = OCMClassMock([UIImage class]); + UIImage *testImage = [self createOnePixelImage]; + + OCMStub([mockRegistrar lookupKeyForAsset:@"fakeImageNameKey"]).andReturn(@"fakeAssetKey"); + OCMStub(ClassMethod([mockImageClass imageNamed:@"fakeAssetKey"])).andReturn(testImage); + + NSDictionary *assetData = @{ + @"assetName" : @"fakeImageNameKey", + @"bitmapScaling" : @"auto", + @"imagePixelRatio" : @1, + @"width" : @15.0, + @"height" : @45.0 + }; + + NSArray *iconData = @[ @"asset", assetData ]; + + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, screenScale); + XCTAssertEqual(resultImage.size.width, 15.0); + XCTAssertEqual(resultImage.size.height, 45.0); +} + +- (void)testExtractIconFromDataAssetNoScaling { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + id mockImageClass = OCMClassMock([UIImage class]); + UIImage *testImage = [self createOnePixelImage]; + + OCMStub([mockRegistrar lookupKeyForAsset:@"fakeImageNameKey"]).andReturn(@"fakeAssetKey"); + OCMStub(ClassMethod([mockImageClass imageNamed:@"fakeAssetKey"])).andReturn(testImage); + + NSDictionary *assetData = + @{@"assetName" : @"fakeImageNameKey", @"bitmapScaling" : @"none", @"imagePixelRatio" : @10}; + + NSArray *iconData = @[ @"asset", assetData ]; + + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, 1.0); + XCTAssertEqual(resultImage.size.width, 1.0); + XCTAssertEqual(resultImage.size.height, 1.0); +} + +- (void)testExtractIconFromDataBytesAuto { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + UIImage *testImage = [self createOnePixelImage]; + NSData *pngData = UIImagePNGRepresentation(testImage); + XCTAssertNotNil(pngData); + + FlutterStandardTypedData *typedData = [FlutterStandardTypedData typedDataWithBytes:pngData]; + + NSDictionary *bytesData = + @{@"byteData" : typedData, @"bitmapScaling" : @"auto", @"imagePixelRatio" : @1}; + + NSArray *iconData = @[ @"bytes", bytesData ]; + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, 1.0); + XCTAssertEqual(resultImage.size.width, 1.0); + XCTAssertEqual(resultImage.size.height, 1.0); +} + +- (void)testExtractIconFromDataBytesAutoWithScaling { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + UIImage *testImage = [self createOnePixelImage]; + NSData *pngData = UIImagePNGRepresentation(testImage); + XCTAssertNotNil(pngData); + + FlutterStandardTypedData *typedData = [FlutterStandardTypedData typedDataWithBytes:pngData]; + + NSDictionary *bytesData = + @{@"byteData" : typedData, @"bitmapScaling" : @"auto", @"imagePixelRatio" : @10}; + + NSArray *iconData = @[ @"bytes", bytesData ]; + + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, 10); + XCTAssertEqual(resultImage.size.width, 0.1); + XCTAssertEqual(resultImage.size.height, 0.1); +} + +- (void)testExtractIconFromDataBytesAutoAndSizeWithSameAspectRatio { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + UIImage *testImage = [self createOnePixelImage]; + NSData *pngData = UIImagePNGRepresentation(testImage); + XCTAssertNotNil(pngData); + + FlutterStandardTypedData *typedData = [FlutterStandardTypedData typedDataWithBytes:pngData]; + + NSDictionary *bytesData = @{ + @"byteData" : typedData, + @"bitmapScaling" : @"auto", + @"imagePixelRatio" : @1, + @"width" : @15.0, + @"height" : @15.0 + }; + + NSArray *iconData = @[ @"bytes", bytesData ]; + + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + + XCTAssertNotNil(resultImage); + XCTAssertEqual(testImage.scale, 1.0); + + // As image has same aspect ratio as the original image, + // only image scale has been changed to match the target size. + CGFloat targetScale = testImage.scale * (testImage.size.width / 15.0); + const CGFloat accuracy = 0.001; + XCTAssertEqualWithAccuracy(resultImage.scale, targetScale, accuracy); + XCTAssertEqual(resultImage.size.width, 15.0); + XCTAssertEqual(resultImage.size.height, 15.0); +} + +- (void)testExtractIconFromDataBytesAutoAndSizeWithDifferentAspectRatio { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + UIImage *testImage = [self createOnePixelImage]; + NSData *pngData = UIImagePNGRepresentation(testImage); + XCTAssertNotNil(pngData); + + FlutterStandardTypedData *typedData = [FlutterStandardTypedData typedDataWithBytes:pngData]; + + NSDictionary *bytesData = @{ + @"byteData" : typedData, + @"bitmapScaling" : @"auto", + @"imagePixelRatio" : @1, + @"width" : @15.0, + @"height" : @45.0 + }; + + NSArray *iconData = @[ @"bytes", bytesData ]; + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, screenScale); + XCTAssertEqual(resultImage.size.width, 15.0); + XCTAssertEqual(resultImage.size.height, 45.0); +} + +- (void)testExtractIconFromDataBytesNoScaling { + FLTGoogleMapMarkerController *instance = [[FLTGoogleMapMarkerController alloc] init]; + NSObject *mockRegistrar = + OCMStrictProtocolMock(@protocol(FlutterPluginRegistrar)); + UIImage *testImage = [self createOnePixelImage]; + NSData *pngData = UIImagePNGRepresentation(testImage); + XCTAssertNotNil(pngData); + + FlutterStandardTypedData *typedData = [FlutterStandardTypedData typedDataWithBytes:pngData]; + + NSDictionary *bytesData = + @{@"byteData" : typedData, @"bitmapScaling" : @"none", @"imagePixelRatio" : @1}; + + NSArray *iconData = @[ @"bytes", bytesData ]; + CGFloat screenScale = 3.0; + + UIImage *resultImage = [instance extractIconFromData:iconData + registrar:mockRegistrar + screenScale:screenScale]; + XCTAssertNotNil(resultImage); + XCTAssertEqual(resultImage.scale, 1.0); + XCTAssertEqual(resultImage.size.width, 1.0); + XCTAssertEqual(resultImage.size.height, 1.0); +} + +- (void)testIsScalableWithScaleFactorFromSize100x100to10x100 { + CGSize originalSize = CGSizeMake(100.0, 100.0); + CGSize targetSize = CGSizeMake(10.0, 100.0); + XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize + toSize:targetSize]); +} + +- (void)testIsScalableWithScaleFactorFromSize100x100to10x10 { + CGSize originalSize = CGSizeMake(100.0, 100.0); + CGSize targetSize = CGSizeMake(10.0, 10.0); + XCTAssertTrue([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize + toSize:targetSize]); +} + +- (void)testIsScalableWithScaleFactorFromSize233x200to23x20 { + CGSize originalSize = CGSizeMake(233.0, 200.0); + CGSize targetSize = CGSizeMake(23.0, 20.0); + XCTAssertTrue([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize + toSize:targetSize]); +} + +- (void)testIsScalableWithScaleFactorFromSize233x200to22x20 { + CGSize originalSize = CGSizeMake(233.0, 200.0); + CGSize targetSize = CGSizeMake(22.0, 20.0); + XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize + toSize:targetSize]); +} + +- (void)testIsScalableWithScaleFactorFromSize200x233to20x23 { + CGSize originalSize = CGSizeMake(200.0, 233.0); + CGSize targetSize = CGSizeMake(20.0, 23.0); + XCTAssertTrue([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize + toSize:targetSize]); +} + +- (void)testIsScalableWithScaleFactorFromSize200x233to20x22 { + CGSize originalSize = CGSizeMake(200.0, 233.0); + CGSize targetSize = CGSizeMake(20.0, 22.0); + XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize + toSize:targetSize]); +} + +- (void)testIsScalableWithScaleFactorFromSize1024x768to500x250 { + CGSize originalSize = CGSizeMake(1024.0, 768.0); + CGSize targetSize = CGSizeMake(500.0, 250.0); + XCTAssertFalse([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalSize + toSize:targetSize]); +} + +- (UIImage *)createOnePixelImage { + CGSize size = CGSizeMake(1, 1); + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = 1.0; + format.opaque = YES; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size + format:format]; + UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) { + [[UIColor whiteColor] setFill]; + [context fillRect:CGRectMake(0, 0, size.width, size.height)]; + }]; + return image; +} + +@end 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 a0b172a65e1c..0c50df87f12e 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 @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../ - google_maps_flutter_platform_interface: ^2.5.0 + google_maps_flutter_platform_interface: ^2.7.0 maps_example_dart: path: ../shared/maps_example_dart/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/custom_marker_icon.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/custom_marker_icon.dart new file mode 100644 index 000000000000..8940762f02e4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/custom_marker_icon.dart @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +/// Returns a generated png image in [ByteData] format with the requested size. +Future createCustomMarkerIconImage({required Size size}) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final _MarkerPainter painter = _MarkerPainter(); + + painter.paint(canvas, size); + + final ui.Image image = await recorder + .endRecording() + .toImage(size.width.floor(), size.height.floor()); + + final ByteData? bytes = + await image.toByteData(format: ui.ImageByteFormat.png); + return bytes!; +} + +class _MarkerPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Rect rect = Offset.zero & size; + const RadialGradient gradient = RadialGradient( + colors: [Colors.yellow, Colors.red], + stops: [0.4, 1.0], + ); + + // Draw radial gradient + canvas.drawRect( + rect, + Paint()..shader = gradient.createShader(rect), + ); + + // Draw diagonal black line + canvas.drawLine( + Offset.zero, + Offset(size.width, size.height), + Paint() + ..color = Colors.black + ..strokeWidth = 1, + ); + } + + @override + bool shouldRepaint(_MarkerPainter oldDelegate) => false; + @override + bool shouldRebuildSemantics(_MarkerPainter oldDelegate) => false; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart index 174055613a9e..df4f79205e82 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart @@ -5,9 +5,13 @@ // ignore_for_file: public_member_api_docs // ignore_for_file: unawaited_futures +import 'dart:async'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'custom_marker_icon.dart'; import 'example_google_map.dart'; import 'page.dart'; @@ -30,66 +34,303 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); +enum _MarkerSizeOption { + original, + width30, + height40, + size30x60, + size120x60, +} + class MarkerIconsBodyState extends State { + final Size _markerAssetImageSize = const Size(48, 48); + _MarkerSizeOption _currentSizeOption = _MarkerSizeOption.original; + Set _markers = {}; + bool _scalingEnabled = true; + bool _mipMapsEnabled = true; ExampleGoogleMapController? controller; - BitmapDescriptor? _markerIcon; + AssetMapBitmap? _markerIconAsset; + BytesMapBitmap? _markerIconBytes; + final int _markersAmountPerType = 15; + bool get _customSizeEnabled => + _currentSizeOption != _MarkerSizeOption.original; @override Widget build(BuildContext context) { - _createMarkerImageFromAsset(context); + _createCustomMarkerIconImages(context); + final Size referenceSize = _getMarkerReferenceSize(); return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Center( - child: SizedBox( - width: 350.0, - height: 300.0, - child: ExampleGoogleMap( - initialCameraPosition: const CameraPosition( - target: _kMapCenter, - zoom: 7.0, + Column(children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: ExampleGoogleMap( + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + markers: _markers, + onMapCreated: _onMapCreated, + ), + ), + ), + TextButton( + onPressed: () => _toggleScaling(context), + child: Text(_scalingEnabled + ? 'Disable auto scaling' + : 'Enable auto scaling'), + ), + if (_scalingEnabled) ...[ + Container( + width: referenceSize.width, + height: referenceSize.height, + decoration: BoxDecoration( + border: Border.all(), ), - markers: {_createMarker()}, - onMapCreated: _onMapCreated, ), + Text( + 'Reference box with size of ${referenceSize.width} x ${referenceSize.height} in logical pixels.'), + const SizedBox(height: 10), + Image.asset( + 'assets/red_square.png', + scale: _mipMapsEnabled ? null : 1.0, + ), + const Text('Asset image rendered with flutter'), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Marker size:'), + const SizedBox(width: 10), + DropdownButton<_MarkerSizeOption>( + value: _currentSizeOption, + onChanged: (_MarkerSizeOption? newValue) { + if (newValue != null) { + setState(() { + _currentSizeOption = newValue; + _updateMarkerImages(context); + }); + } + }, + items: + _MarkerSizeOption.values.map((_MarkerSizeOption option) { + return DropdownMenuItem<_MarkerSizeOption>( + value: option, + child: Text(_getMarkerSizeOptionName(option)), + ); + }).toList(), + ) + ], + ), + ], + TextButton( + onPressed: () => _toggleMipMaps(context), + child: Text(_mipMapsEnabled ? 'Disable mipmaps' : 'Enable mipmaps'), ), - ) + ]) ], ); } - Marker _createMarker() { - if (_markerIcon != null) { - return Marker( - markerId: const MarkerId('marker_1'), - position: _kMapCenter, - icon: _markerIcon!, - ); + String _getMarkerSizeOptionName(_MarkerSizeOption option) { + switch (option) { + case _MarkerSizeOption.original: + return 'Original'; + case _MarkerSizeOption.width30: + return 'Width 30'; + case _MarkerSizeOption.height40: + return 'Height 40'; + case _MarkerSizeOption.size30x60: + return '30x60'; + case _MarkerSizeOption.size120x60: + return '120x60'; + } + } + + (double? width, double? height) _getCurrentMarkerSize() { + if (_scalingEnabled) { + switch (_currentSizeOption) { + case _MarkerSizeOption.width30: + return (30, null); + case _MarkerSizeOption.height40: + return (null, 40); + case _MarkerSizeOption.size30x60: + return (30, 60); + case _MarkerSizeOption.size120x60: + return (120, 60); + case _MarkerSizeOption.original: + return (_markerAssetImageSize.width, _markerAssetImageSize.height); + } } else { - return const Marker( - markerId: MarkerId('marker_1'), - position: _kMapCenter, - ); + return (_markerAssetImageSize.width, _markerAssetImageSize.height); } } - Future _createMarkerImageFromAsset(BuildContext context) async { - if (_markerIcon == null) { - final ImageConfiguration imageConfiguration = - createLocalImageConfiguration(context, size: const Size.square(48)); - BitmapDescriptor.fromAssetImage( - imageConfiguration, 'assets/red_square.png') - .then(_updateBitmap); + // Helper method to calculate reference size for custom marker size. + Size _getMarkerReferenceSize() { + final (double? width, double? height) = _getCurrentMarkerSize(); + + // Calculates reference size using _markerAssetImageSize aspect ration: + + if (width != null && height != null) { + return Size(width, height); + } else if (width != null) { + return Size(width, + width * _markerAssetImageSize.height / _markerAssetImageSize.width); + } else if (height != null) { + return Size( + height * _markerAssetImageSize.width / _markerAssetImageSize.height, + height); + } else { + return _markerAssetImageSize; } } - void _updateBitmap(BitmapDescriptor bitmap) { + void _toggleMipMaps(BuildContext context) { + _mipMapsEnabled = !_mipMapsEnabled; + _updateMarkerImages(context); + } + + void _toggleScaling(BuildContext context) { + _scalingEnabled = !_scalingEnabled; + _updateMarkerImages(context); + } + + void _updateMarkerImages(BuildContext context) { + _updateMarkerAssetImage(context); + _updateMarkerBytesImage(context); + _updateMarkers(); + } + + Marker _createAssetMarker(int index) { + final LatLng position = + LatLng(_kMapCenter.latitude - (index * 0.5), _kMapCenter.longitude - 1); + + return Marker( + markerId: MarkerId('marker_asset_$index'), + position: position, + icon: _markerIconAsset!, + ); + } + + Marker _createBytesMarker(int index) { + final LatLng position = + LatLng(_kMapCenter.latitude - (index * 0.5), _kMapCenter.longitude + 1); + + return Marker( + markerId: MarkerId('marker_bytes_$index'), + position: position, + icon: _markerIconBytes!, + ); + } + + void _updateMarkers() { + final Set markers = {}; + for (int i = 0; i < _markersAmountPerType; i++) { + if (_markerIconAsset != null) { + markers.add(_createAssetMarker(i)); + } + if (_markerIconBytes != null) { + markers.add(_createBytesMarker(i)); + } + } setState(() { - _markerIcon = bitmap; + _markers = markers; }); } + Future _updateMarkerAssetImage(BuildContext context) async { + // Width and height are used only for custom size. + final (double? width, double? height) = + _scalingEnabled && _customSizeEnabled + ? _getCurrentMarkerSize() + : (null, null); + + AssetMapBitmap assetMapBitmap; + if (_mipMapsEnabled) { + final ImageConfiguration imageConfiguration = + createLocalImageConfiguration( + context, + ); + + assetMapBitmap = await AssetMapBitmap.create( + imageConfiguration, + 'assets/red_square.png', + width: width, + height: height, + bitmapScaling: + _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none, + ); + } else { + // Uses hardcoded asset path + // This bypasses the asset resolving logic and allows to load the asset + // with precise path. + assetMapBitmap = AssetMapBitmap( + 'assets/red_square.png', + width: width, + height: height, + bitmapScaling: + _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none, + ); + } + + _updateAssetBitmap(assetMapBitmap); + } + + Future _updateMarkerBytesImage(BuildContext context) async { + final double? devicePixelRatio = + MediaQuery.maybeDevicePixelRatioOf(context); + + final Size bitmapLogicalSize = _getMarkerReferenceSize(); + final double? imagePixelRatio = _scalingEnabled ? devicePixelRatio : null; + + // Create canvasSize with physical marker size + final Size canvasSize = Size( + bitmapLogicalSize.width * (imagePixelRatio ?? 1.0), + bitmapLogicalSize.height * (imagePixelRatio ?? 1.0)); + + final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); + + // Width and height are used only for custom size. + final (double? width, double? height) = + _scalingEnabled && _customSizeEnabled + ? _getCurrentMarkerSize() + : (null, null); + + final BytesMapBitmap bitmap = BytesMapBitmap(bytes.buffer.asUint8List(), + imagePixelRatio: imagePixelRatio, + width: width, + height: height, + bitmapScaling: + _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none); + + _updateBytesBitmap(bitmap); + } + + void _updateAssetBitmap(AssetMapBitmap bitmap) { + _markerIconAsset = bitmap; + _updateMarkers(); + } + + void _updateBytesBitmap(BytesMapBitmap bitmap) { + _markerIconBytes = bitmap; + _updateMarkers(); + } + + void _createCustomMarkerIconImages(BuildContext context) { + if (_markerIconAsset == null) { + _updateMarkerAssetImage(context); + } + + if (_markerIconBytes == null) { + _updateMarkerBytesImage(context); + } + } + void _onMapCreated(ExampleGoogleMapController controllerParam) { setState(() { controller = controllerParam; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart index 1dd1f6f35dd8..d282602d8dc9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart @@ -7,11 +7,11 @@ import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'custom_marker_icon.dart'; import 'example_google_map.dart'; import 'page.dart'; @@ -286,26 +286,10 @@ class PlaceMarkerBodyState extends State { }); } - Future _getAssetIcon(BuildContext context) async { - final Completer bitmapIcon = - Completer(); - final ImageConfiguration config = createLocalImageConfiguration(context); - - const AssetImage('assets/red_square.png') - .resolve(config) - .addListener(ImageStreamListener((ImageInfo image, bool sync) async { - final ByteData? bytes = - await image.image.toByteData(format: ImageByteFormat.png); - if (bytes == null) { - bitmapIcon.completeError(Exception('Unable to encode icon')); - return; - } - final BitmapDescriptor bitmap = - BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); - bitmapIcon.complete(bitmap); - })); - - return bitmapIcon.future; + Future _getMarkerIcon(BuildContext context) async { + const Size canvasSize = Size(48, 48); + final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); + return BytesMapBitmap(bytes.buffer.asUint8List()); } @override @@ -402,7 +386,7 @@ class PlaceMarkerBodyState extends State { onPressed: selectedId == null ? null : () { - _getAssetIcon(context).then( + _getMarkerIcon(context).then( (BitmapDescriptor icon) { _setMarkerIcon(selectedId, icon); }, 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 77063ec00061..810adc8ee21f 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 @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../../ - google_maps_flutter_platform_interface: ^2.5.0 + google_maps_flutter_platform_interface: ^2.7.0 dev_dependencies: flutter_test: 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 dd07e791a888..b816e7ee9144 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 @@ -91,7 +91,8 @@ - (void)setZIndex:(int)zIndex { } - (void)interpretMarkerOptions:(NSDictionary *)data - registrar:(NSObject *)registrar { + registrar:(NSObject *)registrar + screenScale:(CGFloat)screenScale { NSNumber *alpha = data[@"alpha"]; if (alpha && alpha != (id)[NSNull null]) { [self setAlpha:[alpha floatValue]]; @@ -106,7 +107,7 @@ - (void)interpretMarkerOptions:(NSDictionary *)data } NSArray *icon = data[@"icon"]; if (icon && icon != (id)[NSNull null]) { - UIImage *image = [self extractIconFromData:icon registrar:registrar]; + UIImage *image = [self extractIconFromData:icon registrar:registrar screenScale:screenScale]; [self setIcon:image]; } NSNumber *flat = data[@"flat"]; @@ -152,7 +153,9 @@ - (void)interpretInfoWindow:(NSDictionary *)data { } - (UIImage *)extractIconFromData:(NSArray *)iconData - registrar:(NSObject *)registrar { + registrar:(NSObject *)registrar + screenScale:(CGFloat)screenScale { + NSAssert(screenScale > 0, @"Screen scale must be greater than 0"); UIImage *image; if ([iconData.firstObject isEqualToString:@"defaultMarker"]) { CGFloat hue = (iconData.count == 1) ? 0.0f : [iconData[1] doubleValue]; @@ -161,6 +164,8 @@ - (UIImage *)extractIconFromData:(NSArray *)iconData brightness:0.7 alpha:1.0]]; } else if ([iconData.firstObject isEqualToString:@"fromAsset"]) { + // Deprecated: This message handling for 'fromAsset' has been replaced by 'asset'. + // Refer to the flutter google_maps_flutter_platform_interface package for details. if (iconData.count == 2) { image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; } else { @@ -168,6 +173,8 @@ - (UIImage *)extractIconFromData:(NSArray *)iconData fromPackage:iconData[2]]]; } } else if ([iconData.firstObject isEqualToString:@"fromAssetImage"]) { + // Deprecated: This message handling for 'fromAssetImage' has been replaced by 'asset'. + // Refer to the flutter google_maps_flutter_platform_interface package for details. if (iconData.count == 3) { image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; id scaleParam = iconData[2]; @@ -182,11 +189,13 @@ - (UIImage *)extractIconFromData:(NSArray *)iconData @throw exception; } } else if ([iconData[0] isEqualToString:@"fromBytes"]) { + // Deprecated: This message handling for 'fromBytes' has been replaced by 'bytes'. + // Refer to the flutter google_maps_flutter_platform_interface package for details. if (iconData.count == 2) { @try { FlutterStandardTypedData *byteData = iconData[1]; - CGFloat screenScale = [[UIScreen mainScreen] scale]; - image = [UIImage imageWithData:[byteData data] scale:screenScale]; + CGFloat mainScreenScale = [[UIScreen mainScreen] scale]; + image = [UIImage imageWithData:[byteData data] scale:mainScreenScale]; } @catch (NSException *exception) { @throw [NSException exceptionWithName:@"InvalidByteDescriptor" reason:@"Unable to interpret bytes as a valid image." @@ -201,11 +210,86 @@ - (UIImage *)extractIconFromData:(NSArray *)iconData userInfo:nil]; @throw exception; } + } else if ([iconData.firstObject isEqualToString:@"asset"]) { + NSDictionary *assetData = iconData[1]; + if (![assetData isKindOfClass:[NSDictionary class]]) { + NSException *exception = + [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:@"Unable to interpret asset, expected a dictionary as the " + @"second parameter." + userInfo:nil]; + @throw exception; + } + + NSString *assetName = assetData[@"assetName"]; + NSString *scalingMode = assetData[@"bitmapScaling"]; + + image = [UIImage imageNamed:[registrar lookupKeyForAsset:assetName]]; + + if ([scalingMode isEqualToString:@"auto"]) { + NSNumber *width = assetData[@"width"]; + NSNumber *height = assetData[@"height"]; + CGFloat imagePixelRatio = [assetData[@"imagePixelRatio"] doubleValue]; + + if (width || height) { + image = [FLTGoogleMapMarkerController scaledImage:image withScale:screenScale]; + image = [FLTGoogleMapMarkerController scaledImage:image + withWidth:width + height:height + screenScale:screenScale]; + } else { + image = [FLTGoogleMapMarkerController scaledImage:image withScale:imagePixelRatio]; + } + } + } else if ([iconData[0] isEqualToString:@"bytes"]) { + NSDictionary *byteData = iconData[1]; + if (![byteData isKindOfClass:[NSDictionary class]]) { + NSException *exception = + [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:@"Unable to interpret bytes, expected a dictionary as the " + @"second parameter." + userInfo:nil]; + @throw exception; + } + + FlutterStandardTypedData *bytes = byteData[@"byteData"]; + NSString *scalingMode = byteData[@"bitmapScaling"]; + + @try { + image = [UIImage imageWithData:[bytes data] scale:screenScale]; + if ([scalingMode isEqualToString:@"auto"]) { + NSNumber *width = byteData[@"width"]; + NSNumber *height = byteData[@"height"]; + CGFloat imagePixelRatio = [byteData[@"imagePixelRatio"] doubleValue]; + + if (width || height) { + // Before scaling the image, image must be in screenScale + image = [FLTGoogleMapMarkerController scaledImage:image withScale:screenScale]; + image = [FLTGoogleMapMarkerController scaledImage:image + withWidth:width + height:height + screenScale:screenScale]; + } else { + image = [FLTGoogleMapMarkerController scaledImage:image withScale:imagePixelRatio]; + } + } else { + // No scaling, load image from bytes without scale parameter. + image = [UIImage imageWithData:[bytes data]]; + } + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:@"Unable to interpret bytes as a valid image." + userInfo:nil]; + } } return image; } +/// This method is deprecated within the context of `BitmapDescriptor.fromBytes` handling in the +/// 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:(id)scaleParam { double scale = 1.0; if ([scaleParam isKindOfClass:[NSNumber class]]) { @@ -219,6 +303,127 @@ - (UIImage *)scaleImage:(UIImage *)image by:(id)scaleParam { return image; } +/// Creates a scaled version of the provided UIImage based on a specified scale factor. If the +/// scale factor differs from the image's current scale by more than a small epsilon-delta (to +/// account for minor floating-point inaccuracies), a new UIImage object is created with the +/// specified scale. Otherwise, the original image is returned. +/// +/// @param image The UIImage to scale. +/// @param scale The factor by which to scale the image. +/// @return UIImage Returns the scaled UIImage. ++ (UIImage *)scaledImage:(UIImage *)image withScale:(CGFloat)scale { + if (fabs(scale - image.scale) > DBL_EPSILON) { + return [UIImage imageWithCGImage:[image CGImage] + scale:scale + orientation:(image.imageOrientation)]; + } + return image; +} + +/// Scales an input UIImage to a specified size. If the aspect ratio of the input image +/// closely matches the target size, indicated by a small epsilon-delta, the image's scale +/// property is updated instead of resizing the image. If the aspect ratios differ beyond this +/// threshold, the method redraws the image at the target size. +/// +/// @param image The UIImage to scale. +/// @param size The target CGSize to scale the image to. +/// @return UIImage Returns the scaled UIImage. ++ (UIImage *)scaledImage:(UIImage *)image withSize:(CGSize)size { + CGFloat originalPixelWidth = image.size.width * image.scale; + CGFloat originalPixelHeight = image.size.height * image.scale; + + // Return original image if either original image size or target size is so small that + // image cannot be resized or displayed. + if (originalPixelWidth <= 0 || originalPixelHeight <= 0 || size.width <= 0 || size.height <= 0) { + return image; + } + + // Check if the image's size, accounting for scale, matches the target size. + if (fabs(originalPixelWidth - size.width) <= DBL_EPSILON && + fabs(originalPixelHeight - size.height) <= DBL_EPSILON) { + // No need for resizing, return the original image + return image; + } + + // Check if the aspect ratios are approximately equal. + CGSize originalPixelSize = CGSizeMake(originalPixelWidth, originalPixelHeight); + if ([FLTGoogleMapMarkerController isScalableWithScaleFactorFromSize:originalPixelSize + toSize:size]) { + // Scaled image has close to same aspect ratio, + // updating image scale instead of resizing image. + CGFloat factor = originalPixelWidth / size.width; + return [FLTGoogleMapMarkerController scaledImage:image withScale:(image.scale * factor)]; + } else { + // Aspect ratios differ significantly, resize the image. + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = 1.0; + format.opaque = NO; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size + format:format]; + UIImage *newImage = + [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) { + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + }]; + + // Return image with proper scaling. + return [FLTGoogleMapMarkerController scaledImage:newImage withScale:image.scale]; + } +} + +/// Scales an input UIImage to a specified width and height preserving aspect ratio if both +/// widht and height are not given.. +/// +/// @param image The UIImage to scale. +/// @param width The target width to scale the image to. +/// @param height The target height to scale the image to. +/// @param screenScale The current screen scale. +/// @return UIImage Returns the scaled UIImage. ++ (UIImage *)scaledImage:(UIImage *)image + withWidth:(NSNumber *)width + height:(NSNumber *)height + screenScale:(CGFloat)screenScale { + if (!width && !height) { + return image; + } + + CGFloat targetWidth = width ? width.doubleValue : image.size.width; + CGFloat targetHeight = height ? height.doubleValue : image.size.height; + + if (width && !height) { + // Calculate height based on aspect ratio if only width is provided. + double aspectRatio = image.size.height / image.size.width; + targetHeight = round(targetWidth * aspectRatio); + } else if (!width && height) { + // Calculate width based on aspect ratio if only height is provided. + double aspectRatio = image.size.width / image.size.height; + targetWidth = round(targetHeight * aspectRatio); + } + + CGSize targetSize = + CGSizeMake(round(targetWidth * screenScale), round(targetHeight * screenScale)); + return [FLTGoogleMapMarkerController scaledImage:image withSize:targetSize]; +} + ++ (BOOL)isScalableWithScaleFactorFromSize:(CGSize)originalSize toSize:(CGSize)targetSize { + // Select the scaling factor based on the longer side to have good precision. + CGFloat scaleFactor = (originalSize.width > originalSize.height) + ? (targetSize.width / originalSize.width) + : (targetSize.height / originalSize.height); + + // Calculate the scaled dimensions. + CGFloat scaledWidth = originalSize.width * scaleFactor; + CGFloat scaledHeight = originalSize.height * scaleFactor; + + // Check if the scaled dimensions are within a one-pixel + // threshold of the target dimensions. + BOOL widthWithinThreshold = fabs(scaledWidth - targetSize.width) <= 1.0; + BOOL heightWithinThreshold = fabs(scaledHeight - targetSize.height) <= 1.0; + + // The image is considered scalable with scale factor + // if both dimensions are within the threshold. + return widthWithinThreshold && heightWithinThreshold; +} + @end @interface FLTMarkersController () @@ -253,7 +458,9 @@ - (void)addMarkers:(NSArray *)markersToAdd { [[FLTGoogleMapMarkerController alloc] initMarkerWithPosition:position identifier:identifier mapView:self.mapView]; - [controller interpretMarkerOptions:marker registrar:self.registrar]; + [controller interpretMarkerOptions:marker + registrar:self.registrar + screenScale:[self getScreenScale]]; self.markerIdentifierToController[identifier] = controller; } } @@ -265,7 +472,9 @@ - (void)changeMarkers:(NSArray *)markersToChange { if (!controller) { continue; } - [controller interpretMarkerOptions:marker registrar:self.registrar]; + [controller interpretMarkerOptions:marker + registrar:self.registrar + screenScale:[self getScreenScale]]; } } @@ -379,6 +588,16 @@ - (void)isInfoWindowShownForMarkerWithIdentifier:(NSString *)identifier } } +- (CGFloat)getScreenScale { + // TODO(jokerttu): This method is called on marker creation, which, for initial markers, is done + // before the view is added to the view hierarchy. This means that the traitCollection values may + // not be matching the right display where the map is finally shown. The solution should be + // revisited after the proper way to fetch the display scale is resolved for platform views. This + // should be done under the context of the following issue: + // https://github.com/flutter/flutter/issues/125496. + return self.mapView.traitCollection.displayScale; +} + + (CLLocationCoordinate2D)getPosition:(NSDictionary *)marker { NSArray *position = marker[@"position"]; return [FLTGoogleMapJSONConversions locationFromLatLong:position]; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController_Test.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController_Test.h new file mode 100644 index 000000000000..84e6d0937a28 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapMarkerController_Test.h @@ -0,0 +1,29 @@ +// 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 "GoogleMapMarkerController.h" + +@interface FLTGoogleMapMarkerController (Test) + +/// Extracts an icon image from the iconData array. +/// +/// @param iconData An array containing the data for the icon image. +/// @param registrar A Flutter plugin registrar. +/// @param screenScale Screen scale factor for scaling bitmaps. Must be greater than 0. +/// @return A UIImage object created from the icon data. +/// @note Assert unless screenScale is greater than 0. +- (UIImage *)extractIconFromData:(NSArray *)iconData + registrar:(NSObject *)registrar + screenScale:(CGFloat)screenScale; + +/// Checks if an image can be scaled from an original size to a target size using a scale factor +/// while maintaining the aspect ratio. +/// +/// @param originalSize The original size of the image. +/// @param targetSize The desired target size to scale the image to. +/// @return A BOOL indicating whether the image can be scaled to the target size with scale +/// factor. ++ (BOOL)isScalableWithScaleFactorFromSize:(CGSize)originalSize toSize:(CGSize)targetSize; + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios.modulemap b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios.modulemap index 699e6753db38..ad6fe6735b1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios.modulemap +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/google_maps_flutter_ios.modulemap @@ -6,5 +6,6 @@ framework module google_maps_flutter_ios { explicit module Test { header "GoogleMapController_Test.h" + header "GoogleMapMarkerController_Test.h" } } 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 df74168cb104..e84115146296 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_ios description: iOS implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.6.1 +version: 2.7.0 environment: sdk: ^3.2.3 @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.5.0 + google_maps_flutter_platform_interface: ^2.7.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 77f222b11936..a519cfc7ada0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.8 + +* Adds support for BitmapDescriptor classes `AssetMapBitmap` and `BytesMapBitmap`. + ## 0.5.7 * Adds support for marker clustering. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/assets/red_square.png b/packages/google_maps_flutter/google_maps_flutter_web/example/assets/red_square.png new file mode 100644 index 0000000000000000000000000000000000000000..650a2dee711d0d404163de8d0e479d68e31d1662 GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=CqbAk63)r1AkM7~$#S7?R=q_WVZP1_OZu2ihZzoWI>~S6qbP0l+XkKvphU~ literal 0 HcmV?d00001 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/build.excerpt.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/build.excerpt.yaml new file mode 100644 index 000000000000..e317efa11cb3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + builders: + code_excerpter|code_excerpter: + enabled: true diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index aa7e0ae4fe41..d7c9ae288f4d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -181,14 +181,12 @@ void main() { (WidgetTester tester) async { controller.dispose(); - expect(() { - controller.updateMarkers( - MarkerUpdates.from( + await expectLater( + controller.updateMarkers(MarkerUpdates.from( const {}, const {}, - ), - ); - }, throwsAssertionError); + )), + throwsAssertionError); expect(() { controller.showInfoWindow(const MarkerId('any')); @@ -674,7 +672,7 @@ void main() { const Marker(markerId: MarkerId('to-be-added')), }; - controller.updateMarkers(MarkerUpdates.from(previous, current)); + await controller.updateMarkers(MarkerUpdates.from(previous, current)); verify(mock.removeMarkers({ const MarkerId('to-be-removed'), diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index ee6ffe02a420..aef35cc40466 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -3,6 +3,8 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + import 'package:google_maps/google_maps.dart' as _i2; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i4; @@ -338,21 +340,25 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); @override - void addMarkers(Set<_i4.Marker>? markersToAdd) => super.noSuchMethod( + _i5.Future addMarkers(Set<_i4.Marker>? markersToAdd) => + (super.noSuchMethod( Invocation.method( #addMarkers, [markersToAdd], ), - returnValueForMissingStub: null, - ); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - void changeMarkers(Set<_i4.Marker>? markersToChange) => super.noSuchMethod( + _i5.Future changeMarkers(Set<_i4.Marker>? markersToChange) => + (super.noSuchMethod( Invocation.method( #changeMarkers, [markersToChange], ), - returnValueForMissingStub: null, - ); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) => super.noSuchMethod( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index 582d6ddc1dee..30d7b46859e0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -291,13 +291,15 @@ class MockGoogleMapController extends _i1.Mock returnValueForMissingStub: null, ); @override - void updateMarkers(_i2.MarkerUpdates? updates) => super.noSuchMethod( + _i3.Future updateMarkers(_i2.MarkerUpdates? updates) => + (super.noSuchMethod( Invocation.method( #updateMarkers, [updates], ), - returnValueForMissingStub: null, - ); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override void updateClusterManagers(_i2.ClusterManagerUpdates? updates) => super.noSuchMethod( diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart index 81e1eb265574..f068ec8db307 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps/google_maps.dart' as gmaps; @@ -46,7 +45,7 @@ void main() { const Marker(markerId: MarkerId('2')), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 2); expect(controller.markers, contains(const MarkerId('1'))); @@ -61,7 +60,7 @@ void main() { final Set markers = { const Marker(markerId: MarkerId('1')), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); marker = controller.markers[const MarkerId('1')]?.marker; expect(marker, isNotNull); @@ -81,7 +80,7 @@ void main() { position: LatLng(42, 54), ), }; - controller.changeMarkers(updatedMarkers); + await controller.changeMarkers(updatedMarkers); expect(controller.markers.length, 1); marker = controller.markers[const MarkerId('1')]?.marker; @@ -106,7 +105,7 @@ void main() { position: LatLng(42, 54), ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); marker = controller.markers[const MarkerId('1')]?.marker; expect(marker, isNotNull); @@ -124,7 +123,7 @@ void main() { draggable: true, ), }; - controller.changeMarkers(updatedMarkers); + await controller.changeMarkers(updatedMarkers); expect(controller.markers.length, 1); marker = controller.markers[const MarkerId('1')]?.marker; @@ -144,7 +143,7 @@ void main() { const Marker(markerId: MarkerId('3')), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 3); @@ -170,7 +169,7 @@ void main() { ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); @@ -196,7 +195,7 @@ void main() { infoWindow: InfoWindow(title: 'Title', snippet: 'Snippet'), ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isFalse); @@ -212,18 +211,149 @@ void main() { expect(controller.markers[const MarkerId('2')]?.infoWindowShown, isTrue); }); + testWidgets('markers with custom asset icon work', + (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 1.0, + )), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final gmaps.Icon? icon = + controller.markers[const MarkerId('1')]?.marker?.icon as gmaps.Icon?; + expect(icon, isNotNull); + + final String assetUrl = icon!.url!; + expect(assetUrl, startsWith('assets')); + + final gmaps.Size size = icon.size!; + final gmaps.Size scaledSize = icon.scaledSize!; + + // asset size is 48x48 physical pixels + expect(size.width, 48); + expect(size.height, 48); + expect(scaledSize.width, 48); + expect(scaledSize.height, 48); + }); + + testWidgets('markers with custom asset icon and pixelratio work', + (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 2.0, + )), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final gmaps.Icon? icon = + controller.markers[const MarkerId('1')]?.marker?.icon as gmaps.Icon?; + expect(icon, isNotNull); + + final String assetUrl = icon!.url!; + expect(assetUrl, startsWith('assets')); + + final gmaps.Size size = icon.size!; + final gmaps.Size scaledSize = icon.scaledSize!; + + // Asset size is 48x48 physical pixels, and with pixel ratio 2.0 it + // should be drawn with size 24x24 logical pixels. + expect(size.width, 24); + expect(size.height, 24); + expect(scaledSize.width, 24); + expect(scaledSize.height, 24); + }); + testWidgets('markers with custom asset icon with width and height work', + (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/red_square.png', + imagePixelRatio: 2.0, + width: 64, + height: 64, + )), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final gmaps.Icon? icon = + controller.markers[const MarkerId('1')]?.marker?.icon as gmaps.Icon?; + expect(icon, isNotNull); + + final String assetUrl = icon!.url!; + expect(assetUrl, startsWith('assets')); + + final gmaps.Size size = icon.size!; + final gmaps.Size scaledSize = icon.scaledSize!; + + // Asset size is 48x48 physical pixels, + // and scaled to requested 64x64 size. + expect(size.width, 64); + expect(size.height, 64); + expect(scaledSize.width, 64); + expect(scaledSize.height, 64); + }); + + testWidgets('markers with missing asset icon should not set size', + (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: AssetMapBitmap( + 'assets/broken_asset_name.png', + imagePixelRatio: 2.0, + )), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final gmaps.Icon? icon = + controller.markers[const MarkerId('1')]?.marker?.icon as gmaps.Icon?; + expect(icon, isNotNull); + + final String assetUrl = icon!.url!; + expect(assetUrl, startsWith('assets')); + + // For invalid assets, the size and scaledSize should be null. + expect(icon.size, null); + expect(icon.scaledSize, null); + }); + // https://github.com/flutter/flutter/issues/66622 testWidgets('markers with custom bitmap icon work', (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); final Set markers = { Marker( markerId: const MarkerId('1'), - icon: BitmapDescriptor.fromBytes(bytes), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: tester.view.devicePixelRatio, + ), ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final gmaps.Icon? icon = @@ -237,20 +367,70 @@ void main() { expect(response.bodyBytes, bytes, reason: 'Bytes from the Icon blob must match bytes used to create Marker'); + + final gmaps.Size size = icon.size!; + final gmaps.Size scaledSize = icon.scaledSize!; + + // Icon size is 16x16 pixels, this should be automatically read from the + // bitmap and set to the icon size scaled to 8x8 using the + // given imagePixelRatio. + final int expectedSize = 16 ~/ tester.view.devicePixelRatio; + expect(size.width, expectedSize); + expect(size.height, expectedSize); + expect(scaledSize.width, expectedSize); + expect(scaledSize.height, expectedSize); + }); + + testWidgets('markers with custom bitmap icon and pixelratio work', + (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; + final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); + final Set markers = { + Marker( + markerId: const MarkerId('1'), + icon: BytesMapBitmap( + bytes, + imagePixelRatio: 1, + ), + ), + }; + + await controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final gmaps.Icon? icon = + controller.markers[const MarkerId('1')]?.marker?.icon as gmaps.Icon?; + expect(icon, isNotNull); + + final gmaps.Size size = icon!.size!; + final gmaps.Size scaledSize = icon.scaledSize!; + + // Icon size is 16x16 pixels, this should be automatically read from the + // bitmap and set to the icon size and should not be changed as + // image pixel ratio is set to 1.0. + expect(size.width, 16); + expect(size.height, 16); + expect(scaledSize.width, 16); + expect(scaledSize.height, 16); }); // https://github.com/flutter/flutter/issues/73789 testWidgets('markers with custom bitmap icon pass size to sdk', (WidgetTester tester) async { + tester.view.devicePixelRatio = 2.0; final Uint8List bytes = const Base64Decoder().convert(iconImageBase64); final Set markers = { Marker( markerId: const MarkerId('1'), - icon: BitmapDescriptor.fromBytes(bytes, size: const Size(20, 30)), + icon: BytesMapBitmap( + bytes, + width: 20, + height: 30, + ), ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final gmaps.Icon? icon = @@ -279,7 +459,7 @@ void main() { ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final HTMLElement? content = controller @@ -304,7 +484,7 @@ void main() { ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final HTMLElement? content = controller 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 a7222dfb12ab..85a89ed5bf96 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 @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.6.0 + google_maps_flutter_platform_interface: ^2.7.0 google_maps_flutter_web: path: ../ web: ^0.5.0 @@ -25,6 +25,10 @@ dev_dependencies: sdk: flutter mockito: 5.4.4 +flutter: + assets: + - assets/ + dependency_overrides: # Override the google_maps_flutter dependency on google_maps_flutter_web. # TODO(ditman): Unwind the circular dependency. This will create problems 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 1822a2fb4779..ee9100ddff01 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 @@ -12,6 +12,16 @@ final gmaps.LatLngBounds _nullGmapsLatLngBounds = // The TrustedType Policy used by this plugin. Used to sanitize InfoWindow contents. TrustedTypePolicy? _gmapsTrustedTypePolicy; +// A cache for image size Futures to reduce redundant image fetch requests. +// This cache should be always cleaned up after marker updates are processed. +final Map> _bitmapSizeFutureCache = + >{}; + +// A cache for blob URLs of bitmaps to avoid creating a new blob URL for the +// same bitmap instances. This cache should be always cleaned up after marker +// updates are processed. +final Map _bitmapBlobUrlCache = {}; + // Converts a [Color] into a valid CSS value #RRGGBB. String _getCssColor(Color color) { return '#${color.value.toRadixString(16).padLeft(8, '0').substring(2)}'; @@ -197,7 +207,6 @@ CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) { // Convert plugin objects to gmaps.Options objects // TODO(ditman): Move to their appropriate objects, maybe make them copy constructors? // Marker.fromMarker(anotherMarker, moreOptions); - gmaps.InfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { final String markerTitle = marker.infoWindow.title ?? ''; final String markerSnippet = marker.infoWindow.snippet ?? ''; @@ -264,20 +273,132 @@ gmaps.Size? _gmSizeFromIconConfig(List iconConfig, int sizeIndex) { final List? rawIconSize = iconConfig[sizeIndex] as List?; if (rawIconSize != null) { size = gmaps.Size( - rawIconSize[0] as num?, - rawIconSize[1] as num?, + rawIconSize[0]! as double, + rawIconSize[1]! as double, ); } } return size; } -// Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. -gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { - final List iconConfig = bitmapDescriptor.toJson() as List; +/// Sets the size of the Google Maps icon. +void _setIconSize({ + required Size size, + required gmaps.Icon icon, +}) { + final gmaps.Size gmapsSize = gmaps.Size(size.width, size.height); + icon.size = gmapsSize; + icon.scaledSize = gmapsSize; +} + +/// Determines the appropriate size for a bitmap based on its descriptor. +/// +/// This method returns the icon's size based on the provided [width] and +/// [height]. If both dimensions are null, the size is calculated using the +/// [imagePixelRatio] based on the actual size of the image fetched from the +/// [url]. If only one of the dimensions is provided, the other is calculated to +/// maintain the image's original aspect ratio. +Future _getBitmapSize(MapBitmap mapBitmap, String url) async { + final double? width = mapBitmap.width; + final double? height = mapBitmap.height; + if (width != null && height != null) { + // If both, width and height are set, return the provided dimensions. + return Size(width, height); + } else { + assert( + url.isNotEmpty, 'URL must not be empty when calculating dimensions.'); + + final Size? bitmapSize = await _bitmapSizeFutureCache.putIfAbsent(url, () { + return _fetchBitmapSize(url); + }); + + if (bitmapSize == null) { + // If bitmap size is null, the image is invalid, + // and the icon size cannot be calculated. + return null; + } + + double targetWidth = bitmapSize.width; + double targetHeight = bitmapSize.height; + if (width == null && height == null) { + // Width and height are not provided, so the imagePixelRatio is used to + // calculate the target size. + targetWidth /= mapBitmap.imagePixelRatio; + targetHeight /= mapBitmap.imagePixelRatio; + } else { + final double aspectRatio = bitmapSize.width / bitmapSize.height; + targetWidth = width ?? (height ?? bitmapSize.height) * aspectRatio; + targetHeight = height ?? (width ?? bitmapSize.width) / aspectRatio; + } + + // Return the calculated size. + return Size(targetWidth, targetHeight); + } +} +/// Fetches the size of the bitmap from a given URL and caches the result. +/// +/// This method attempts to fetch the image size for a given [url]. +Future _fetchBitmapSize(String url) async { + final HTMLImageElement image = HTMLImageElement()..src = url; + + // Wait for the onLoad or onError event. + await Future.any(>[image.onLoad.first, image.onError.first]); + + if (image.width == 0 || image.height == 0) { + // Complete with null for invalid images. + return null; + } + + // Complete with the image size for valid images. + return Size(image.width.toDouble(), image.height.toDouble()); +} + +/// Cleans up the caches used for bitmap size conversion and URL storage. +/// +/// This method should be called after marker updates are processed to ensure +/// that memory usage is optimized by removing completed or outdated cache +/// entries. +void _cleanUpBitmapConversionCaches() { + _bitmapSizeFutureCache.clear(); + _bitmapBlobUrlCache.clear(); +} + +// Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. +Future _gmIconFromBitmapDescriptor( + BitmapDescriptor bitmapDescriptor) async { gmaps.Icon? icon; + if (bitmapDescriptor is MapBitmap) { + final String url = switch (bitmapDescriptor) { + (final BytesMapBitmap bytesMapBitmap) => + _bitmapBlobUrlCache.putIfAbsent(bytesMapBitmap.byteData.hashCode, () { + final Blob blob = + Blob([bytesMapBitmap.byteData.toJS].toJS); + return URL.createObjectURL(blob as JSObject); + }), + (final AssetMapBitmap assetMapBitmap) => + ui_web.assetManager.getAssetUrl(assetMapBitmap.assetName), + _ => throw UnimplementedError(), + }; + + icon = gmaps.Icon()..url = url; + + switch (bitmapDescriptor.bitmapScaling) { + case MapBitmapScaling.auto: + final Size? size = await _getBitmapSize(bitmapDescriptor, url); + if (size != null) { + _setIconSize(size: size, icon: icon); + } + case MapBitmapScaling.none: + break; + } + return icon; + } + + // The following code is for the deprecated BitmapDescriptor.fromBytes + // and BitmapDescriptor.fromAssetImage. + final List iconConfig = bitmapDescriptor.toJson() as List; if (iconConfig[0] == 'fromAssetImage') { assert(iconConfig.length >= 2); // iconConfig[2] contains the DPIs of the screen, but that information is @@ -315,16 +436,15 @@ gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { ..scaledSize = size; } } - return icon; } // Computes the options for a new [gmaps.Marker] from an incoming set of options // [marker], and the existing marker registered with the map: [currentMarker]. -gmaps.MarkerOptions _markerOptionsFromMarker( +Future _markerOptionsFromMarker( Marker marker, gmaps.Marker? currentMarker, -) { +) async { return gmaps.MarkerOptions() ..position = gmaps.LatLng( marker.position.latitude, @@ -335,7 +455,7 @@ gmaps.MarkerOptions _markerOptionsFromMarker( ..visible = marker.visible ..opacity = marker.alpha ..draggable = marker.draggable - ..icon = _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_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index c2fe750de89f..37c1e8a2ffff 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -464,12 +464,13 @@ class GoogleMapController { } /// Applies [MarkerUpdates] to the currently managed markers. - void updateMarkers(MarkerUpdates updates) { + Future updateMarkers(MarkerUpdates updates) async { assert( _markersController != null, 'Cannot update markers after dispose().'); - _markersController?.addMarkers(updates.markersToAdd); - _markersController?.changeMarkers(updates.markersToChange); + await _markersController?.addMarkers(updates.markersToAdd); + await _markersController?.changeMarkers(updates.markersToChange); _markersController?.removeMarkers(updates.markerIdsToRemove); + _cleanUpBitmapConversionCaches(); } /// Applies [ClusterManagerUpdates] to the currently managed cluster managers. 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 f71818a43805..16618b59a6e8 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 @@ -60,7 +60,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { MarkerUpdates markerUpdates, { required int mapId, }) async { - _map(mapId).updateMarkers(markerUpdates); + await _map(mapId).updateMarkers(markerUpdates); } /// Applies the passed in `polygonUpdates` to the `mapId`. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart index 962422f752a8..8bcf8b7f942d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -29,11 +29,11 @@ class MarkersController extends GeometryController { /// Adds a set of [Marker] objects to the cache. /// /// Wraps each [Marker] into its corresponding [MarkerController]. - void addMarkers(Set markersToAdd) { - markersToAdd.forEach(_addMarker); + Future addMarkers(Set markersToAdd) async { + await Future.wait(markersToAdd.map(_addMarker)); } - void _addMarker(Marker marker) { + Future _addMarker(Marker marker) async { final gmaps.InfoWindowOptions? infoWindowOptions = _infoWindowOptionsFromMarker(marker); gmaps.InfoWindow? gmInfoWindow; @@ -56,7 +56,7 @@ class MarkersController extends GeometryController { _markerIdToController[marker.markerId]?.marker; final gmaps.MarkerOptions markerOptions = - _markerOptionsFromMarker(marker, currentMarker); + await _markerOptionsFromMarker(marker, currentMarker); final gmaps.Marker gmMarker = gmaps.Marker(markerOptions); @@ -67,6 +67,7 @@ class MarkersController extends GeometryController { } else { gmMarker.map = googleMap; } + final MarkerController controller = MarkerController( marker: gmMarker, clusterManagerId: marker.clusterManagerId, @@ -90,11 +91,11 @@ class MarkersController extends GeometryController { } /// Updates a set of [Marker] objects with new options. - void changeMarkers(Set markersToChange) { - markersToChange.forEach(_changeMarker); + Future changeMarkers(Set markersToChange) async { + await Future.wait(markersToChange.map(_changeMarker)); } - void _changeMarker(Marker marker) { + Future _changeMarker(Marker marker) async { final MarkerController? markerController = _markerIdToController[marker.markerId]; if (markerController != null) { @@ -105,9 +106,10 @@ class MarkersController extends GeometryController { if (oldClusterManagerId != newClusterManagerId) { // If clusterManagerId changes. Remove existing marker and create new one. _removeMarker(marker.markerId); - _addMarker(marker); + await _addMarker(marker); } else { - final gmaps.MarkerOptions markerOptions = _markerOptionsFromMarker( + final gmaps.MarkerOptions markerOptions = + await _markerOptionsFromMarker( marker, markerController.marker, ); 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 3447e8b1026f..76707dd7c1e9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.5.7 +version: 0.5.8 environment: sdk: ^3.3.0 @@ -23,7 +23,7 @@ dependencies: flutter_web_plugins: sdk: flutter google_maps: ^7.1.0 - google_maps_flutter_platform_interface: ^2.6.0 + google_maps_flutter_platform_interface: ^2.7.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 web: ^0.5.1