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 000000000000..920b93f74d78
Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/resources/icon_image.png differ
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 000000000000..920b93f74d78
Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/resources/icon_image.png differ
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 000000000000..650a2dee711d
Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_web/example/assets/red_square.png differ
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