diff --git a/packages/google_maps_flutter/google_maps_flutter/AUTHORS b/packages/google_maps_flutter/google_maps_flutter/AUTHORS index 9f1b53ee2667..4fc3ace39f0f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter/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/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index bab8412142d9..c4d3c4472f04 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 2.3.0 +* Adds better support for marker size and scaling behaviour with `BitmapDescriptor.createFromAsset` and `BitmapDescriptor.createFromBytes`. +* Deprecates `BitmapDescriptor.fromAssetImage` in favor of `BitmapDescriptor.createFromAsset` +* Deprecates `BitmapDescriptor.fromBytes` in favor of `BitmapDescriptor.createFromBytes` * Updates minimum Flutter version to 3.0. ## 2.2.3 diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart index 38a02ea0d8f1..7bfeb238d0dc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -940,17 +940,21 @@ void main() { expect(iwVisibleStatus, false); }); - testWidgets('fromAssetImage', (WidgetTester tester) async { + testWidgets('createFromAsset', (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); + final BitmapDescriptor mip = await BitmapDescriptor.createFromAsset( + imageConfiguration, + 'red_square.png', + ); + final BitmapDescriptor scaled = await BitmapDescriptor.createFromAsset( + imageConfiguration, + 'red_square.png', + mipmaps: false, + ); + expect((mip.toJson() as List)[3], 1.0); + expect((scaled.toJson() as List)[3], 2); }); testWidgets('testTakeSnapshot', (WidgetTester tester) async { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/custom_marker_icon.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/custom_marker_icon.dart new file mode 100644 index 000000000000..0dff2e1263af --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/custom_marker_icon.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +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], + ); + canvas.drawRect( + rect, + Paint()..shader = gradient.createShader(rect), + ); + } + + @override + bool shouldRepaint(_MarkerPainter oldDelegate) => false; + @override + bool shouldRebuildSemantics(_MarkerPainter oldDelegate) => false; +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart index 58d266c95d1d..261621496217 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart @@ -5,9 +5,14 @@ // ignore_for_file: public_member_api_docs // ignore_for_file: unawaited_futures +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'custom_marker_icon.dart'; import 'page.dart'; class MarkerIconsPage extends GoogleMapExampleAppPage { @@ -30,65 +35,235 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { + final double _markerAssetImageSize = 48; + final Size _customSize = const Size(55, 30); + Set _markers = {}; + double _markerScale = 1.0; + bool _scalingEnabled = true; + bool _customSizeEnabled = false; + bool _mipMapsEnabled = true; GoogleMapController? controller; - BitmapDescriptor? _markerIcon; + BitmapDescriptor? _markerIconAsset; + BitmapDescriptor? _markerIconBytes; + final int _markersAmountPerType = 15; @override Widget build(BuildContext context) { - _createMarkerImageFromAsset(context); + _createCustomMarkerIconImages(context); + final Size size = getCurrentMarkerSize(); return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Center( - child: SizedBox( - width: 350.0, - height: 300.0, - child: GoogleMap( - initialCameraPosition: const CameraPosition( - target: _kMapCenter, - zoom: 7.0, + Column(children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: GoogleMap( + initialCameraPosition: const CameraPosition( + target: _kMapCenter, + zoom: 7.0, + ), + markers: _markers, + onMapCreated: _onMapCreated, ), - markers: {_createMarker()}, - onMapCreated: _onMapCreated, ), ), - ) + TextButton( + onPressed: () => _toggleScaling(context), + child: Text(_scalingEnabled + ? 'Disable auto scaling' + : 'Enable auto scaling'), + ), + if (_scalingEnabled) ...[ + Container( + width: size.width, + height: size.height, + color: Colors.red, + ), + Text( + 'Reference box with size of ${size.width} x ${size.height} in logical pixels.'), + const SizedBox(height: 10), + TextButton( + onPressed: () => _toggleCustomSize(context), + child: Text(_customSizeEnabled + ? 'Disable custom size' + : 'Enable custom size'), + ), + if (_customSizeEnabled) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: _markerScale <= 0.5 + ? null + : () => _decreaseScale(context), + child: const Text( + '-', + textScaleFactor: 2, + ), + ), + Text('scale ${_markerScale}x'), + TextButton( + onPressed: _markerScale >= 2.5 + ? null + : () => _increaseScale(context), + child: const Text( + '+', + textScaleFactor: 2, + ), + ), + ], + ), + ], + 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!, - ); - } else { - return const Marker( - markerId: MarkerId('marker_1'), - position: _kMapCenter, - ); - } + Size getCurrentMarkerSize() { + return _scalingEnabled && _customSizeEnabled + ? _customSize * _markerScale + : Size(_markerAssetImageSize, _markerAssetImageSize); } - 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); - } + void _toggleMipMaps(BuildContext context) { + _mipMapsEnabled = !_mipMapsEnabled; + _updateMarkerImages(context); + } + + void _toggleScaling(BuildContext context) { + _scalingEnabled = !_scalingEnabled; + _updateMarkerImages(context); + } + + void _toggleCustomSize(BuildContext context) { + _customSizeEnabled = !_customSizeEnabled; + _updateMarkerImages(context); + } + + void _decreaseScale(BuildContext context) { + _markerScale = max(_markerScale - 0.5, 0.0); + _updateMarkerImages(context); + } + + void _increaseScale(BuildContext context) { + _markerScale = min(_markerScale + 0.5, 2.5); + _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 _updateBitmap(BitmapDescriptor bitmap) { + 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 { + final Size? size = + _scalingEnabled && _customSizeEnabled ? getCurrentMarkerSize() : null; + final ImageConfiguration imageConfiguration = createLocalImageConfiguration( + context, + size: size, + ); + BitmapDescriptor.createFromAsset( + imageConfiguration, 'assets/red_square.png', + mipmaps: _mipMapsEnabled, + imagePixelRatio: _mipMapsEnabled ? null : 1.0, + bitmapScaling: + _scalingEnabled ? BitmapScaling.auto : BitmapScaling.noScaling) + .then(_updateAssetBitmap); + } + + Future _updateMarkerBytesImage(BuildContext context) async { + final double devicePixelRatio = + WidgetsBinding.instance.window.devicePixelRatio; + + final Size markerSize = getCurrentMarkerSize(); + + final double? imagePixelRatio = _scalingEnabled ? devicePixelRatio : null; + + // Create canvasSize with physical marker size + final Size canvasSize = Size(markerSize.width * (imagePixelRatio ?? 1.0), + markerSize.height * (imagePixelRatio ?? 1.0)); + + final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); + + // Size is used only for custom size + final Size? size = + _scalingEnabled && _customSizeEnabled ? getCurrentMarkerSize() : null; + + final BitmapDescriptor bitmap = BitmapDescriptor.createFromBytes( + bytes.buffer.asUint8List(), + imagePixelRatio: _customSizeEnabled ? null : imagePixelRatio, + size: size, + bitmapScaling: + _scalingEnabled ? BitmapScaling.auto : BitmapScaling.noScaling); + + _updateBytesBitmap(bitmap); + } + + void _updateAssetBitmap(BitmapDescriptor bitmap) { + _markerIconAsset = bitmap; + _updateMarkers(); + } + + void _updateBytesBitmap(BitmapDescriptor bitmap) { + _markerIconBytes = bitmap; + _updateMarkers(); + } + + void _createCustomMarkerIconImages(BuildContext context) { + if (_markerIconAsset == null) { + _updateMarkerAssetImage(context); + } + + if (_markerIconBytes == null) { + _updateMarkerBytesImage(context); + } + } + void _onMapCreated(GoogleMapController controllerParam) { setState(() { controller = controllerParam; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 8fde95016f51..2337f6e7bc19 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -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/google_maps_flutter.dart'; +import 'custom_marker_icon.dart'; import 'page.dart'; class PlaceMarkerPage extends GoogleMapExampleAppPage { @@ -266,26 +266,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 BitmapDescriptor.createFromBytes(bytes.buffer.asUint8List()); } @override @@ -382,7 +366,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/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 5813d42e617e..a65019c387a6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -35,3 +35,13 @@ flutter: uses-material-design: true assets: - assets/ + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_android: + path: ../../../google_maps_flutter/google_maps_flutter_android + google_maps_flutter_ios: + path: ../../../google_maps_flutter/google_maps_flutter_ios + google_maps_flutter_platform_interface: + path: ../../../google_maps_flutter/google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index a4be120b2117..48e848740586 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -21,6 +21,7 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf ArgumentCallbacks, ArgumentCallback, BitmapDescriptor, + BitmapScaling, CameraPosition, CameraPositionCallback, CameraTargetBounds, diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 0771314b9e44..85a42d1e4c65 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.2.3 +version: 2.3.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -28,3 +28,13 @@ dev_dependencies: sdk: flutter plugin_platform_interface: ^2.0.0 stream_transform: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_android: + path: ../../google_maps_flutter/google_maps_flutter_android + google_maps_flutter_ios: + path: ../../google_maps_flutter/google_maps_flutter_ios + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter/google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_android/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_android/AUTHORS index 9f1b53ee2667..4fc3ace39f0f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter_android/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_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 6e596c135f38..046d8b3c48e8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.5.0 +* Adds support for BitmapDescriptors created with `BitmapDescriptor.createFromAsset` or `BitmapDescriptor.createFromBytes`. * Updates minimum Flutter version to 3.0. ## 2.4.3 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 72c6959fe55e..a4645bc40c99 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,6 +4,7 @@ package io.flutter.plugins.googlemaps; +import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Point; @@ -24,7 +25,8 @@ import com.google.android.gms.maps.model.RoundCap; import com.google.android.gms.maps.model.SquareCap; import com.google.android.gms.maps.model.Tile; -import io.flutter.view.FlutterMain; +import io.flutter.FlutterInjector; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -34,10 +36,8 @@ /** Conversions between JSON-like values and GoogleMaps data types. */ class Convert { - // TODO(hamdikahloun): FlutterMain has been deprecated and should be replaced with FlutterLoader - // when it's available in Stable channel: https://github.com/flutter/flutter/issues/70923. - @SuppressWarnings("deprecation") - 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))) { case "defaultMarker": @@ -49,27 +49,40 @@ private static BitmapDescriptor toBitmapDescriptor(Object o) { case "fromAsset": if (data.size() == 2) { return BitmapDescriptorFactory.fromAsset( - FlutterMain.getLookupKeyForAsset(toString(data.get(1)))); + FlutterInjector.instance() + .flutterLoader() + .getLookupKeyForAsset(toString(data.get(1)))); } else { return BitmapDescriptorFactory.fromAsset( - FlutterMain.getLookupKeyForAsset(toString(data.get(1)), toString(data.get(2)))); + FlutterInjector.instance() + .flutterLoader() + .getLookupKeyForAsset(toString(data.get(1)), toString(data.get(2)))); } case "fromAssetImage": if (data.size() == 3) { return BitmapDescriptorFactory.fromAsset( - FlutterMain.getLookupKeyForAsset(toString(data.get(1)))); + FlutterInjector.instance() + .flutterLoader() + .getLookupKeyForAsset(toString(data.get(1)))); } else { throw new IllegalArgumentException( "'fromAssetImage' Expected exactly 3 arguments, got: " + data.size()); } case "fromBytes": - return getBitmapFromBytes(data); + return getBitmapFromBytesLegacy(data); + case "asset": + return getBitmapFromAsset(data, assetManager, density); + case "bytes": + return getBitmapFromBytes(data, density); 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)); @@ -84,6 +97,81 @@ private static BitmapDescriptor getBitmapFromBytes(List data) { } } + private static BitmapDescriptor getBitmapFromBytes(List data, float density) { + if (data.size() == 4 || data.size() == 5) { + try { + Bitmap bitmap = toBitmap(data.get(1)); + switch (toString(data.get(2))) { + case "auto": + if (data.size() == 4) { + // Scales image using given scale ratio + final float scale = density / toFloat(data.get(3)); + return BitmapDescriptorFactory.fromBitmap(toScaledBitmap(bitmap, scale)); + } else if (data.size() == 5) { + // Scales image using physical size parameter + final List size = toList(data.get(4)); + final int width = toInt((double) size.get(0) * density); + final int height = toInt((double) size.get(1) * density); + return BitmapDescriptorFactory.fromBitmap( + toScaledBitmap(bitmap, toInt(width), toInt(height))); + } + break; + case "noScaling": + break; + } + return BitmapDescriptorFactory.fromBitmap(bitmap); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to interpret bytes as a valid image.", e); + } + } else { + throw new IllegalArgumentException( + "bytes should have exactly 4 or 5 arguments, the bytes, scaling type, scale and size. Got: " + + data.size()); + } + } + + private static BitmapDescriptor getBitmapFromAsset( + List data, AssetManager assetManager, float density) { + if (data.size() != 4 && data.size() != 5) { + throw new IllegalArgumentException( + "'asset' Expected exactly 4 or 5 arguments, got: " + data.size()); + } + + String asset = + FlutterInjector.instance().flutterLoader().getLookupKeyForAsset(toString(data.get(1))); + + switch (toString(data.get(2))) { + case "auto": + float scale = density / toFloat(data.get(3)); + // Scale image if size is given or scale is other than 1.0 + if (data.size() == 5 || Math.abs(scale - 1) > 0.001) { + try { + InputStream inputStream = assetManager.open(asset); + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + inputStream.close(); + if (data.size() == 5) { + // Scales asset image to exact size. + final List size = toList(data.get(4)); + final int width = toInt((double) size.get(0) * density); + final int height = toInt((double) size.get(1) * density); + return BitmapDescriptorFactory.fromBitmap(toScaledBitmap(bitmap, width, height)); + } else { + // Scales asset image to using given scale. + return BitmapDescriptorFactory.fromBitmap(toScaledBitmap(bitmap, scale)); + } + } catch (Exception e) { + throw new IllegalArgumentException( + "'asset' cannot open asset: " + toString(data.get(1))); + } + } + break; + case "noScaling": + break; + } + + return BitmapDescriptorFactory.fromAsset(asset); + } + private static boolean toBoolean(Object o) { return (Boolean) o; } @@ -284,6 +372,18 @@ private static Bitmap toBitmap(Object o) { } } + private static Bitmap toScaledBitmap(Bitmap bitmap, float scale) { + return toScaledBitmap( + bitmap, (int) (bitmap.getWidth() * scale), (int) (bitmap.getHeight() * scale)); + } + + private static Bitmap toScaledBitmap(Bitmap bitmap, int width, int height) { + if (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)); @@ -379,7 +479,8 @@ static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { } /** Returns the dartMarkerId of the interpreted marker. */ - static String interpretMarkerOptions(Object o, MarkerOptionsSink sink) { + static String interpretMarkerOptions( + Object o, MarkerOptionsSink sink, AssetManager assetManager, float density) { final Map data = toMap(o); final Object alpha = data.get("alpha"); if (alpha != null) { @@ -404,7 +505,7 @@ static String 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"); @@ -496,7 +597,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) { @@ -508,7 +610,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) { @@ -520,7 +622,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) { @@ -642,7 +744,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": @@ -653,9 +755,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"); 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 66d3e283b8df..5526d8ec28ab 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.os.Bundle; @@ -99,10 +100,11 @@ final class GoogleMapController methodChannel = new MethodChannel(binaryMessenger, "plugins.flutter.dev/google_maps_android_" + id); methodChannel.setMethodCallHandler(this); + AssetManager assetManager = context.getAssets(); this.lifecycleProvider = lifecycleProvider; - this.markersController = new MarkersController(methodChannel); + this.markersController = new MarkersController(methodChannel, 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 47ffe9b857d6..29a7107f51dd 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.GoogleMap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; @@ -19,11 +20,15 @@ class MarkersController { private final Map googleMapsMarkerIdToDartMarkerId; private final MethodChannel methodChannel; private GoogleMap googleMap; + private final AssetManager assetManager; + private final float density; - MarkersController(MethodChannel methodChannel) { + MarkersController(MethodChannel methodChannel, AssetManager assetManager, float density) { + this.assetManager = assetManager; this.markerIdToController = new HashMap<>(); this.googleMapsMarkerIdToDartMarkerId = new HashMap<>(); this.methodChannel = methodChannel; + this.density = density; } void setGoogleMap(GoogleMap googleMap) { @@ -151,7 +156,7 @@ private void addMarker(Object marker) { return; } MarkerBuilder markerBuilder = new MarkerBuilder(); - String markerId = Convert.interpretMarkerOptions(marker, markerBuilder); + String markerId = Convert.interpretMarkerOptions(marker, markerBuilder, assetManager, density); MarkerOptions options = markerBuilder.build(); addMarker(markerId, options, markerBuilder.consumeTapEvents()); } @@ -170,7 +175,7 @@ private void changeMarker(Object marker) { String markerId = getMarkerId(marker); 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/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java index 3ca78e7674d7..e467863053bc 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 @@ -9,6 +9,9 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.content.res.AssetManager; +import android.os.Build; +import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; @@ -21,16 +24,32 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.P) public class MarkersControllerTest { + private AssetManager assetManager; + private final float density = 1; + + @Before + public void before() { + MockitoAnnotations.openMocks(this); + assetManager = ApplicationProvider.getApplicationContext().getAssets(); + } @Test public void controller_OnMarkerDragStart() { final MethodChannel methodChannel = spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); - final MarkersController controller = new MarkersController(methodChannel); + final MarkersController controller = + new MarkersController(methodChannel, assetManager, density); final GoogleMap googleMap = mock(GoogleMap.class); controller.setGoogleMap(googleMap); @@ -63,7 +82,8 @@ public void controller_OnMarkerDragStart() { public void controller_OnMarkerDragEnd() { final MethodChannel methodChannel = spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); - final MarkersController controller = new MarkersController(methodChannel); + final MarkersController controller = + new MarkersController(methodChannel, assetManager, density); final GoogleMap googleMap = mock(GoogleMap.class); controller.setGoogleMap(googleMap); @@ -96,7 +116,8 @@ public void controller_OnMarkerDragEnd() { public void controller_OnMarkerDrag() { final MethodChannel methodChannel = spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); - final MarkersController controller = new MarkersController(methodChannel); + final MarkersController controller = + new MarkersController(methodChannel, assetManager, density); final GoogleMap googleMap = mock(GoogleMap.class); controller.setGoogleMap(googleMap); 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 bd72b7ba52d2..1e9ee1893916 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 @@ -956,16 +956,20 @@ void googleMapsTests() { expect(iwVisibleStatus, false); }); - testWidgets('fromAssetImage', (WidgetTester tester) async { + testWidgets('createFromAsset', (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); + final BitmapDescriptor mip = await BitmapDescriptor.createFromAsset( + imageConfiguration, + 'red_square.png', + ); + final BitmapDescriptor scaled = await BitmapDescriptor.createFromAsset( + imageConfiguration, + 'red_square.png', + mipmaps: false, + ); + expect((mip.toJson() as List)[2], 1.0); expect((scaled.toJson() as List)[2], 2); }); 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..0dff2e1263af --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/custom_marker_icon.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +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], + ); + canvas.drawRect( + rect, + Paint()..shader = gradient.createShader(rect), + ); + } + + @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 fe28eb680596..defa2d9a0c46 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,10 +5,16 @@ // ignore_for_file: public_member_api_docs // ignore_for_file: unawaited_futures +import 'dart:async'; +import 'dart:math'; +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'; class MarkerIconsPage extends GoogleMapExampleAppPage { @@ -31,65 +37,235 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { + final double _markerAssetImageSize = 48; + final Size _customSize = const Size(55, 30); + Set _markers = {}; + double _markerScale = 1.0; + bool _scalingEnabled = true; + bool _customSizeEnabled = false; + bool _mipMapsEnabled = true; ExampleGoogleMapController? controller; - BitmapDescriptor? _markerIcon; + BitmapDescriptor? _markerIconAsset; + BitmapDescriptor? _markerIconBytes; + final int _markersAmountPerType = 10; @override Widget build(BuildContext context) { - _createMarkerImageFromAsset(context); + _createCustomMarkerIconImages(context); + final Size size = getCurrentMarkerSize(); 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, ), - markers: {_createMarker()}, - onMapCreated: _onMapCreated, ), ), - ) + TextButton( + onPressed: () => _toggleScaling(context), + child: Text(_scalingEnabled + ? 'Disable auto scaling' + : 'Enable auto scaling'), + ), + if (_scalingEnabled) ...[ + Container( + width: size.width, + height: size.height, + color: Colors.red, + ), + Text( + 'Reference box with size of ${size.width} x ${size.height} in logical pixels.'), + const SizedBox(height: 10), + TextButton( + onPressed: () => _toggleCustomSize(context), + child: Text(_customSizeEnabled + ? 'Disable custom size' + : 'Enable custom size'), + ), + if (_customSizeEnabled) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: _markerScale <= 0.5 + ? null + : () => _decreaseScale(context), + child: const Text( + '-', + textScaleFactor: 2, + ), + ), + Text('scale ${_markerScale}x'), + TextButton( + onPressed: _markerScale >= 2.5 + ? null + : () => _increaseScale(context), + child: const Text( + '+', + textScaleFactor: 2, + ), + ), + ], + ), + ], + 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!, - ); - } else { - return const Marker( - markerId: MarkerId('marker_1'), - position: _kMapCenter, - ); - } + Size getCurrentMarkerSize() { + return _scalingEnabled && _customSizeEnabled + ? _customSize * _markerScale + : Size(_markerAssetImageSize, _markerAssetImageSize); } - 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); - } + void _toggleMipMaps(BuildContext context) { + _mipMapsEnabled = !_mipMapsEnabled; + _updateMarkerImages(context); + } + + void _toggleScaling(BuildContext context) { + _scalingEnabled = !_scalingEnabled; + _updateMarkerImages(context); + } + + void _toggleCustomSize(BuildContext context) { + _customSizeEnabled = !_customSizeEnabled; + _updateMarkerImages(context); } - void _updateBitmap(BitmapDescriptor bitmap) { + void _decreaseScale(BuildContext context) { + _markerScale = max(_markerScale - 0.5, 0.0); + _updateMarkerImages(context); + } + + void _increaseScale(BuildContext context) { + _markerScale = min(_markerScale + 0.5, 2.5); + _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 { + final Size? size = + _scalingEnabled && _customSizeEnabled ? getCurrentMarkerSize() : null; + final ImageConfiguration imageConfiguration = createLocalImageConfiguration( + context, + size: size, + ); + BitmapDescriptor.createFromAsset( + imageConfiguration, 'assets/red_square.png', + mipmaps: _mipMapsEnabled, + imagePixelRatio: _mipMapsEnabled ? null : 1.0, + bitmapScaling: + _scalingEnabled ? BitmapScaling.auto : BitmapScaling.noScaling) + .then(_updateAssetBitmap); + } + + Future _updateMarkerBytesImage(BuildContext context) async { + final double devicePixelRatio = + WidgetsBinding.instance.window.devicePixelRatio; + + final Size markerSize = getCurrentMarkerSize(); + + final double? imagePixelRatio = _scalingEnabled ? devicePixelRatio : null; + + // Create canvasSize with physical marker size + final Size canvasSize = Size(markerSize.width * (imagePixelRatio ?? 1.0), + markerSize.height * (imagePixelRatio ?? 1.0)); + + final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); + + // Size is used only for custom size + final Size? size = + _scalingEnabled && _customSizeEnabled ? getCurrentMarkerSize() : null; + + final BitmapDescriptor bitmap = BitmapDescriptor.createFromBytes( + bytes.buffer.asUint8List(), + imagePixelRatio: _customSizeEnabled ? null : imagePixelRatio, + size: size, + bitmapScaling: + _scalingEnabled ? BitmapScaling.auto : BitmapScaling.noScaling); + + _updateBytesBitmap(bitmap); + } + + void _updateAssetBitmap(BitmapDescriptor bitmap) { + _markerIconAsset = bitmap; + _updateMarkers(); + } + + void _updateBytesBitmap(BitmapDescriptor 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 2c6c725a4fa5..24046eb8d46f 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 BitmapDescriptor.createFromBytes(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 aa29fa99a97b..813cfe362718 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 @@ -34,3 +34,11 @@ flutter: uses-material-design: true assets: - assets/ + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_android: + path: ../../../google_maps_flutter/google_maps_flutter_android + google_maps_flutter_platform_interface: + path: ../../../google_maps_flutter/google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index d67e85f15e9a..f3ca68ad942e 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/plugins/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.4.3 +version: 2.5.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -29,3 +29,9 @@ dev_dependencies: flutter_test: sdk: flutter plugin_platform_interface: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter/google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/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 a65523f426c1..250353af7677 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.2.0 +* Adds support for BitmapDescriptors created with `BitmapDescriptor.createFromAsset` or `BitmapDescriptor.createFromBytes`. * Updates minimum Flutter version to 3.0. ## 2.1.13 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart index eb00ccb673f4..5680be3e3449 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart @@ -805,17 +805,21 @@ void main() { expect(iwVisibleStatus, false); }); - testWidgets('fromAssetImage', (WidgetTester tester) async { + testWidgets('createFromAsset', (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); + final BitmapDescriptor mip = await BitmapDescriptor.createFromAsset( + imageConfiguration, + 'red_square.png', + ); + final BitmapDescriptor scaled = await BitmapDescriptor.createFromAsset( + imageConfiguration, + 'red_square.png', + mipmaps: false, + ); + expect((mip.toJson() as List)[3], 1.0); + expect((scaled.toJson() as List)[3], 2); }); testWidgets('testTakeSnapshot', (WidgetTester tester) async { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/custom_marker_icon.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/custom_marker_icon.dart new file mode 100644 index 000000000000..0dff2e1263af --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/custom_marker_icon.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +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], + ); + canvas.drawRect( + rect, + Paint()..shader = gradient.createShader(rect), + ); + } + + @override + bool shouldRepaint(_MarkerPainter oldDelegate) => false; + @override + bool shouldRebuildSemantics(_MarkerPainter oldDelegate) => false; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart index fe28eb680596..defa2d9a0c46 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart @@ -5,10 +5,16 @@ // ignore_for_file: public_member_api_docs // ignore_for_file: unawaited_futures +import 'dart:async'; +import 'dart:math'; +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'; class MarkerIconsPage extends GoogleMapExampleAppPage { @@ -31,65 +37,235 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { + final double _markerAssetImageSize = 48; + final Size _customSize = const Size(55, 30); + Set _markers = {}; + double _markerScale = 1.0; + bool _scalingEnabled = true; + bool _customSizeEnabled = false; + bool _mipMapsEnabled = true; ExampleGoogleMapController? controller; - BitmapDescriptor? _markerIcon; + BitmapDescriptor? _markerIconAsset; + BitmapDescriptor? _markerIconBytes; + final int _markersAmountPerType = 10; @override Widget build(BuildContext context) { - _createMarkerImageFromAsset(context); + _createCustomMarkerIconImages(context); + final Size size = getCurrentMarkerSize(); 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, ), - markers: {_createMarker()}, - onMapCreated: _onMapCreated, ), ), - ) + TextButton( + onPressed: () => _toggleScaling(context), + child: Text(_scalingEnabled + ? 'Disable auto scaling' + : 'Enable auto scaling'), + ), + if (_scalingEnabled) ...[ + Container( + width: size.width, + height: size.height, + color: Colors.red, + ), + Text( + 'Reference box with size of ${size.width} x ${size.height} in logical pixels.'), + const SizedBox(height: 10), + TextButton( + onPressed: () => _toggleCustomSize(context), + child: Text(_customSizeEnabled + ? 'Disable custom size' + : 'Enable custom size'), + ), + if (_customSizeEnabled) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: _markerScale <= 0.5 + ? null + : () => _decreaseScale(context), + child: const Text( + '-', + textScaleFactor: 2, + ), + ), + Text('scale ${_markerScale}x'), + TextButton( + onPressed: _markerScale >= 2.5 + ? null + : () => _increaseScale(context), + child: const Text( + '+', + textScaleFactor: 2, + ), + ), + ], + ), + ], + 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!, - ); - } else { - return const Marker( - markerId: MarkerId('marker_1'), - position: _kMapCenter, - ); - } + Size getCurrentMarkerSize() { + return _scalingEnabled && _customSizeEnabled + ? _customSize * _markerScale + : Size(_markerAssetImageSize, _markerAssetImageSize); } - 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); - } + void _toggleMipMaps(BuildContext context) { + _mipMapsEnabled = !_mipMapsEnabled; + _updateMarkerImages(context); + } + + void _toggleScaling(BuildContext context) { + _scalingEnabled = !_scalingEnabled; + _updateMarkerImages(context); + } + + void _toggleCustomSize(BuildContext context) { + _customSizeEnabled = !_customSizeEnabled; + _updateMarkerImages(context); } - void _updateBitmap(BitmapDescriptor bitmap) { + void _decreaseScale(BuildContext context) { + _markerScale = max(_markerScale - 0.5, 0.0); + _updateMarkerImages(context); + } + + void _increaseScale(BuildContext context) { + _markerScale = min(_markerScale + 0.5, 2.5); + _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 { + final Size? size = + _scalingEnabled && _customSizeEnabled ? getCurrentMarkerSize() : null; + final ImageConfiguration imageConfiguration = createLocalImageConfiguration( + context, + size: size, + ); + BitmapDescriptor.createFromAsset( + imageConfiguration, 'assets/red_square.png', + mipmaps: _mipMapsEnabled, + imagePixelRatio: _mipMapsEnabled ? null : 1.0, + bitmapScaling: + _scalingEnabled ? BitmapScaling.auto : BitmapScaling.noScaling) + .then(_updateAssetBitmap); + } + + Future _updateMarkerBytesImage(BuildContext context) async { + final double devicePixelRatio = + WidgetsBinding.instance.window.devicePixelRatio; + + final Size markerSize = getCurrentMarkerSize(); + + final double? imagePixelRatio = _scalingEnabled ? devicePixelRatio : null; + + // Create canvasSize with physical marker size + final Size canvasSize = Size(markerSize.width * (imagePixelRatio ?? 1.0), + markerSize.height * (imagePixelRatio ?? 1.0)); + + final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); + + // Size is used only for custom size + final Size? size = + _scalingEnabled && _customSizeEnabled ? getCurrentMarkerSize() : null; + + final BitmapDescriptor bitmap = BitmapDescriptor.createFromBytes( + bytes.buffer.asUint8List(), + imagePixelRatio: _customSizeEnabled ? null : imagePixelRatio, + size: size, + bitmapScaling: + _scalingEnabled ? BitmapScaling.auto : BitmapScaling.noScaling); + + _updateBytesBitmap(bitmap); + } + + void _updateAssetBitmap(BitmapDescriptor bitmap) { + _markerIconAsset = bitmap; + _updateMarkers(); + } + + void _updateBytesBitmap(BitmapDescriptor 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/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart index 2c6c725a4fa5..24046eb8d46f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/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 BitmapDescriptor.createFromBytes(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_ios/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml index ac27996fbc25..2f2df32e987d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml @@ -32,3 +32,11 @@ flutter: uses-material-design: true assets: - assets/ + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_ios: + path: ../../../google_maps_flutter/google_maps_flutter_ios + google_maps_flutter_platform_interface: + path: ../../../google_maps_flutter/google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h index cfccb7b0b5f9..7cca23a42749 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.h @@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN + (GMSCoordinateBounds *)coordinateBoundsFromLatLongs:(NSArray *)latlongs; + (GMSMapViewType)mapViewTypeFromTypeValue:(NSNumber *)value; + (nullable GMSCameraUpdate *)cameraUpdateFromChannelValue:(NSArray *)channelValue; ++ (CGSize)sizeFromArray:(NSArray *)array scale:(CGFloat)scale; @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m index d554c501b1e2..1b29051a147b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FLTGoogleMapJSONConversions.m @@ -141,4 +141,7 @@ + (nullable GMSCameraUpdate *)cameraUpdateFromChannelValue:(NSArray *)channelVal } return nil; } + ++ (CGSize)sizeFromArray:(NSArray *)array scale:(CGFloat)scale; +{ return CGSizeMake((int)([array[0] floatValue] * scale), (int)([array[1] floatValue] * scale)); } @end 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..27472a6c3c42 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 @@ -160,13 +160,6 @@ - (UIImage *)extractIconFromData:(NSArray *)iconData saturation:1.0 brightness:0.7 alpha:1.0]]; - } else if ([iconData.firstObject isEqualToString:@"fromAsset"]) { - if (iconData.count == 2) { - image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; - } else { - image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1] - fromPackage:iconData[2]]]; - } } else if ([iconData.firstObject isEqualToString:@"fromAssetImage"]) { if (iconData.count == 3) { image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; @@ -201,11 +194,76 @@ - (UIImage *)extractIconFromData:(NSArray *)iconData userInfo:nil]; @throw exception; } + } else if ([iconData.firstObject isEqualToString:@"asset"]) { + if (iconData.count == 4 || iconData.count == 5) { + image = [UIImage imageNamed:[registrar lookupKeyForAsset:iconData[1]]]; + if ([iconData[2] isEqualToString:@"auto"]) { + if (iconData.count == 4) { + // Update proper scale information for image object. + CGFloat imageScale = [iconData[3] doubleValue]; + image = [self scaleImage:image scale:imageScale]; + } else if (iconData.count == 5) { + CGFloat screenScale = [[UIScreen mainScreen] scale]; + // Update proper scale information for image object. + image = [self scaleImage:image scale:screenScale]; + // Create resized image. + CGSize size = [FLTGoogleMapJSONConversions sizeFromArray:iconData[4] scale:screenScale]; + image = [self scaleImage:image size:size]; + } + } + } else { + NSString *error = + [NSString stringWithFormat:@"'asset' should have exactly 3 or 4 arguments. Got: %lu", + (unsigned long)iconData.count]; + NSException *exception = [NSException exceptionWithName:@"InvalidBitmapDescriptor" + reason:error + userInfo:nil]; + @throw exception; + } + } else if ([iconData[0] isEqualToString:@"bytes"]) { + if (iconData.count == 4 || iconData.count == 5) { + @try { + FlutterStandardTypedData *byteData = iconData[1]; + if ([iconData[2] isEqualToString:@"auto"]) { + if (iconData.count == 4) { + CGFloat imageScale = [iconData[3] doubleValue]; + // Scale parameter given, use it as scale factor + image = [UIImage imageWithData:[byteData data] scale:imageScale]; + } else if (iconData.count == 5) { + CGFloat screenScale = [[UIScreen mainScreen] scale]; + // Size parameter is given, scale image to exact size. + // Update proper scale information for image object. + image = [UIImage imageWithData:[byteData data] scale:screenScale]; + // Create resized image. + CGSize size = [FLTGoogleMapJSONConversions sizeFromArray:iconData[4] scale:screenScale]; + image = [self scaleImage:image size:size]; + } + } else { + // No scaling, load image from bytes without scale parameter. + image = [UIImage imageWithData:[byteData data]]; + } + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:@"Unable to interpret bytes as a valid image." + userInfo:nil]; + } + } else { + NSString *error = + [NSString stringWithFormat:@"bytes should have exactly 3 or 5 arguments, the " + @"bytes, scale and size. Got: %lu", + (unsigned long)iconData.count]; + NSException *exception = [NSException exceptionWithName:@"InvalidByteDescriptor" + reason:error + userInfo:nil]; + @throw exception; + } } return image; } +// Used by deprecated fromBytes functionality. +// Can be removed when deprecated image bitmap types are removed from platform interface. - (UIImage *)scaleImage:(UIImage *)image by:(id)scaleParam { double scale = 1.0; if ([scaleParam isKindOfClass:[NSNumber class]]) { @@ -219,6 +277,34 @@ - (UIImage *)scaleImage:(UIImage *)image by:(id)scaleParam { return image; } +- (UIImage *)scaleImage:(UIImage *)image scale:(CGFloat)scale { + if (fabs(scale - image.scale) > 1e-3) { + return [UIImage imageWithCGImage:[image CGImage] + scale:(scale)orientation:(image.imageOrientation)]; + } + return image; +} + +- (UIImage *)scaleImage:(UIImage *)image size:(CGSize)size { + if (fabs(((int)image.size.width * image.scale) - size.width) > 0 || + fabs(((int)image.size.height * image.scale) - size.height) > 0) { + if (fabs(image.size.width / image.size.height - size.width / size.height) < 1e-2) { + // Scaled image has close to same aspect ratio, updating image scale instead of resizing + // image. + return [self scaleImage:image + scale:(image.scale * (size.width / (image.size.width * image.scale)))]; + } else { + UIGraphicsBeginImageContextWithOptions(size, NO, 1.0); + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + // Return image with proper scaling + return [self scaleImage:newImage scale:image.scale]; + } + } + return image; +} + @end @interface FLTMarkersController () 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 c4f8d23cb382..ffd61462d56e 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/plugins/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.1.13 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -27,3 +27,9 @@ dev_dependencies: flutter_test: sdk: flutter plugin_platform_interface: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter/google_maps_flutter_platform_interface diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS index 493a0b4ef9c2..6f33f4aa0511 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Joonas Kerttula diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index b3d6c5540e7a..8bf7e3768460 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 2.3.0 +* Adds better support for marker size and scaling behaviour with `BitmapDescriptor.createFromAsset` and `BitmapDescriptor.createFromBytes`. +* Deprecates `BitmapDescriptor.fromAssetImage` in favor of `BitmapDescriptor.createFromAsset` +* Deprecates `BitmapDescriptor.fromBytes` in favor of `BitmapDescriptor.createFromBytes` * Updates minimum Flutter version to 3.0. ## 2.2.5 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart index 7dda43a7abf4..f7ed4e8f5104 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart @@ -4,12 +4,27 @@ import 'dart:async' show Future; import 'dart:typed_data' show Uint8List; -import 'dart:ui' show Size; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter/material.dart' - show ImageConfiguration, AssetImage, AssetBundleImageKey; -import 'package:flutter/services.dart' show AssetBundle; +import 'package:flutter/widgets.dart' + show + AssetBundle, + AssetBundleImageKey, + AssetImage, + ImageConfiguration, + Size, + WidgetsBinding; + +/// Type of bitmap scaling options to use on BitmapDescriptor creation. +enum BitmapScaling { + /// Automatically scale image with devices pixel ratio or to given size, + /// to keep marker sizes same between platforms and devices. + auto, + + /// Render marker to the map as without scaling, this can be used if the image + /// is already pre-scaled, or to increase performance with large marker amounts. + noScaling, +} /// Defines a bitmap image. For a marker, this class can be used to set the /// image of the marker icon. For a ground overlay, it can be used to set the @@ -63,15 +78,29 @@ class BitmapDescriptor { } static const String _defaultMarker = 'defaultMarker'; + static const String _asset = 'asset'; + static const String _bytes = 'bytes'; + + @Deprecated('No longer supported') static const String _fromAsset = 'fromAsset'; + @Deprecated('No longer supported') static const String _fromAssetImage = 'fromAssetImage'; + @Deprecated('No longer supported') static const String _fromBytes = 'fromBytes'; + /// Value representing auto scaling parameter. + static const String bitmapAutoScaling = 'auto'; + + /// Value representing auto no scaling parameter. + static const String bitmapNoScaling = 'noScaling'; + static const Set _validTypes = { _defaultMarker, _fromAsset, _fromAssetImage, _fromBytes, + _asset, + _bytes }; /// Convenience hue value representing red. @@ -123,6 +152,7 @@ class BitmapDescriptor { /// This method takes into consideration various asset resolutions /// and scales the images to the right resolution depending on the dpi. /// Set `mipmaps` to false to load the exact dpi version of the image, `mipmap` is true by default. + @Deprecated('No longer supported') static Future fromAssetImage( ImageConfiguration configuration, String assetName, { @@ -161,6 +191,7 @@ class BitmapDescriptor { /// bitmap, regardless of the actual resolution of the encoded PNG. /// This helps the browser to render High-DPI images at the correct size. /// `size` is not required (and ignored, if passed) in other platforms. + @Deprecated('No longer supported') static BitmapDescriptor fromBytes(Uint8List byteData, {Size? size}) { assert(byteData.isNotEmpty, 'Cannot create BitmapDescriptor with empty byteData'); @@ -175,6 +206,104 @@ class BitmapDescriptor { ]); } + /// Creates a [BitmapDescriptor] from an asset image. + /// + /// Asset images in flutter are stored per: + /// https://flutter.dev/docs/development/ui/assets-and-images#declaring-resolution-aware-image-assets + /// This method takes into consideration various asset resolutions + /// and scales the images to the right resolution depending on the dpi. + /// Set `mipmaps` to false to load the exact dpi version of the image, `mipmap` is true by default. + /// If `mipmaps` is set to false, optional `imagePixelRatio` can be given to + /// override `devicePixelRatio` value from `ImageConfiguration`. + static Future createFromAsset( + ImageConfiguration configuration, + String assetName, { + AssetBundle? bundle, + String? package, + bool mipmaps = true, + double? imagePixelRatio, + BitmapScaling bitmapScaling = BitmapScaling.auto, + }) async { + final double devicePixelRatio = + WidgetsBinding.instance.window.devicePixelRatio; + final double? targetImagePixelRatio = + imagePixelRatio ?? configuration.devicePixelRatio; + final Size? size = configuration.size; + + if (!mipmaps && (targetImagePixelRatio != null || size != null)) { + return BitmapDescriptor._([ + _asset, + assetName, + _getBitMapScalingString(bitmapScaling), + targetImagePixelRatio ?? devicePixelRatio, + if (size != null) + [ + size.width, + size.height, + ], + ]); + } + + final AssetImage assetImage = + AssetImage(assetName, package: package, bundle: bundle); + final AssetBundleImageKey assetBundleImageKey = + await assetImage.obtainKey(configuration); + + return BitmapDescriptor._([ + _asset, + assetBundleImageKey.name, + _getBitMapScalingString(bitmapScaling), + assetBundleImageKey.scale, + if (size != null) + [ + size.width, + size.height, + ], + ]); + } + + /// Creates a BitmapDescriptor using an array of bytes that must be encoded + /// as PNG. + /// The optional [size] parameter represents the *logical size* of the + /// bitmap, regardless of the actual resolution of the encoded PNG. + /// This helps the platform to render High-DPI images at the correct size. + /// [ImagePixelRatio] value can be use to scale the image to + /// proper size across platforms. + static BitmapDescriptor createFromBytes( + Uint8List byteData, { + BitmapScaling bitmapScaling = BitmapScaling.auto, + double? imagePixelRatio, + Size? size, + }) { + assert(byteData.isNotEmpty, + 'Cannot create BitmapDescriptor with empty byteData'); + assert(bitmapScaling != BitmapScaling.noScaling || imagePixelRatio == null, + 'If bitmapScaling is set to BitmapScaling.noScaling, scale parameter cannot be used.'); + assert(bitmapScaling != BitmapScaling.noScaling || size == null, + 'If bitmapScaling is set to BitmapScaling.noScaling, size parameter cannot be used.'); + + return BitmapDescriptor._([ + _bytes, + byteData, + _getBitMapScalingString(bitmapScaling), + imagePixelRatio ?? 1.0, + if (size != null) + [ + size.width, + size.height, + ], + ]); + } + + static String _getBitMapScalingString(BitmapScaling bitmapScaling) { + switch (bitmapScaling) { + case BitmapScaling.auto: + return bitmapAutoScaling; + case BitmapScaling.noScaling: + return bitmapNoScaling; + } + } + final Object _json; /// Convert the object to a Json format. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 6dfff89f8c4b..da7168533d4d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_fl issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.2.5 +version: 2.3.0 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart index 2499e87bb649..01d8ffd5ef44 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart @@ -4,9 +4,9 @@ // ignore:unnecessary_import import 'dart:typed_data'; -import 'dart:ui'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; @@ -18,53 +18,118 @@ void main() { test('toJson / fromJson', () { final BitmapDescriptor descriptor = BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); - final Object json = descriptor.toJson(); - // Rehydrate a new bitmap descriptor... - // ignore: deprecated_member_use_from_same_package - final BitmapDescriptor descriptorFromJson = - BitmapDescriptor.fromJson(json); - - expect(descriptorFromJson, isNot(descriptor)); // New instance - expect(identical(descriptorFromJson.toJson(), json), isTrue); // Same JSON + final Object expected = [ + 'defaultMarker', + BitmapDescriptor.hueCyan + ]; + expect(descriptor.toJson(), equals(expected)); // Same JSON }); - group('fromBytes constructor', () { - test('with empty byte array, throws assertion error', () { - expect(() { - BitmapDescriptor.fromBytes(Uint8List.fromList([])); - }, throwsAssertionError); + group('createFromAsset constructor', () { + test('without mipmaps', () async { + final BitmapDescriptor descriptor = + await BitmapDescriptor.createFromAsset( + ImageConfiguration.empty, 'path_to_asset_image', + mipmaps: false); + expect(descriptor, isA()); + expect( + descriptor.toJson(), + equals([ + 'asset', + 'path_to_asset_image', + BitmapDescriptor.bitmapAutoScaling, + 1.0 + ])); + }); + test('with mipmaps', () async { + final BitmapDescriptor descriptor = + await BitmapDescriptor.createFromAsset( + ImageConfiguration.empty, 'path_to_asset_image', + // ignore: avoid_redundant_argument_values + mipmaps: true); + expect(descriptor, isA()); + expect( + descriptor.toJson(), + equals([ + 'asset', + 'path_to_asset_image', + BitmapDescriptor.bitmapAutoScaling, + 1.0 + ])); }); + test('with size and without mipmaps', () async { + final double devicePixelRatio = + WidgetsBinding.instance.window.devicePixelRatio; + const Size size = Size(100, 200); + final ImageConfiguration imageConfiguration = + ImageConfiguration(size: size, devicePixelRatio: devicePixelRatio); + final BitmapDescriptor descriptor = + await BitmapDescriptor.createFromAsset( + imageConfiguration, 'path_to_asset_image', + mipmaps: false); - test('with bytes', () { - final BitmapDescriptor descriptor = BitmapDescriptor.fromBytes( - Uint8List.fromList([1, 2, 3]), - ); expect(descriptor, isA()); expect( descriptor.toJson(), equals([ - 'fromBytes', - [1, 2, 3], + 'asset', + 'path_to_asset_image', + BitmapDescriptor.bitmapAutoScaling, + devicePixelRatio, + [size.width, size.height] ])); }); - test('with size, not on the web, size is ignored', () { - final BitmapDescriptor descriptor = BitmapDescriptor.fromBytes( + test('with size and mipmaps', () async { + final double devicePixelRatio = + WidgetsBinding.instance.window.devicePixelRatio; + const Size size = Size(100, 200); + final ImageConfiguration imageConfiguration = + ImageConfiguration(size: size, devicePixelRatio: devicePixelRatio); + final BitmapDescriptor descriptor = + await BitmapDescriptor.createFromAsset( + imageConfiguration, 'path_to_asset_image', + // ignore: avoid_redundant_argument_values + mipmaps: true); + + expect(descriptor, isA()); + expect( + descriptor.toJson(), + equals([ + 'asset', + 'path_to_asset_image', + BitmapDescriptor.bitmapAutoScaling, + 1.0, + [size.width, size.height] + ])); + }); + }); + + group('createFromBytes constructor', () { + test('with empty byte array, throws assertion error', () { + expect(() { + BitmapDescriptor.createFromBytes(Uint8List.fromList([])); + }, throwsAssertionError); + }); + + test('with bytes', () { + final BitmapDescriptor descriptor = BitmapDescriptor.createFromBytes( Uint8List.fromList([1, 2, 3]), - size: const Size(40, 20), ); - + expect(descriptor, isA()); expect( descriptor.toJson(), equals([ - 'fromBytes', + 'bytes', [1, 2, 3], + BitmapDescriptor.bitmapAutoScaling, + 1.0 ])); - }, skip: kIsWeb); + }); - test('with size, on the web, size is preserved', () { - final BitmapDescriptor descriptor = BitmapDescriptor.fromBytes( + test('with size', () { + final BitmapDescriptor descriptor = BitmapDescriptor.createFromBytes( Uint8List.fromList([1, 2, 3]), size: const Size(40, 20), ); @@ -72,153 +137,12 @@ void main() { expect( descriptor.toJson(), equals([ - 'fromBytes', + 'bytes', [1, 2, 3], + BitmapDescriptor.bitmapAutoScaling, + 1.0, [40, 20], ])); - }, skip: !kIsWeb); - }); - - group('fromJson validation', () { - group('type validation', () { - test('correct type', () { - expect(BitmapDescriptor.fromJson(['defaultMarker']), - isA()); - }); - test('wrong type', () { - expect(() { - BitmapDescriptor.fromJson(['bogusType']); - }, throwsAssertionError); - }); - }); - group('defaultMarker', () { - test('hue is null', () { - expect(BitmapDescriptor.fromJson(['defaultMarker']), - isA()); - }); - test('hue is number', () { - expect(BitmapDescriptor.fromJson(['defaultMarker', 158]), - isA()); - }); - test('hue is not number', () { - expect(() { - BitmapDescriptor.fromJson(['defaultMarker', 'nope']); - }, throwsAssertionError); - }); - test('hue is out of range', () { - expect(() { - BitmapDescriptor.fromJson(['defaultMarker', -1]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson(['defaultMarker', 361]); - }, throwsAssertionError); - }); - }); - group('fromBytes', () { - test('with bytes', () { - expect( - BitmapDescriptor.fromJson([ - 'fromBytes', - Uint8List.fromList([1, 2, 3]) - ]), - isA()); - }); - test('without bytes', () { - expect(() { - BitmapDescriptor.fromJson(['fromBytes', null]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson(['fromBytes', []]); - }, throwsAssertionError); - }); - }); - group('fromAsset', () { - test('name is passed', () { - expect( - BitmapDescriptor.fromJson( - ['fromAsset', 'some/path.png']), - isA()); - }); - test('name cannot be null or empty', () { - expect(() { - BitmapDescriptor.fromJson(['fromAsset', null]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson(['fromAsset', '']); - }, throwsAssertionError); - }); - test('package is passed', () { - expect( - BitmapDescriptor.fromJson( - ['fromAsset', 'some/path.png', 'some_package']), - isA()); - }); - test('package cannot be null or empty', () { - expect(() { - BitmapDescriptor.fromJson( - ['fromAsset', 'some/path.png', null]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson( - ['fromAsset', 'some/path.png', '']); - }, throwsAssertionError); - }); - }); - group('fromAssetImage', () { - test('name and dpi passed', () { - expect( - BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 1.0]), - isA()); - }); - test('name cannot be null or empty', () { - expect(() { - BitmapDescriptor.fromJson(['fromAssetImage', null, 1.0]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson(['fromAssetImage', '', 1.0]); - }, throwsAssertionError); - }); - test('dpi must be number', () { - expect(() { - BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', null]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 'one']); - }, throwsAssertionError); - }); - test('with optional [width, height] List', () { - expect( - BitmapDescriptor.fromJson([ - 'fromAssetImage', - 'some/path.png', - 1.0, - [640, 480] - ]), - isA()); - }); - test( - 'optional [width, height] List cannot be null or not contain 2 elements', - () { - expect(() { - BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 1.0, null]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson( - ['fromAssetImage', 'some/path.png', 1.0, []]); - }, throwsAssertionError); - expect(() { - BitmapDescriptor.fromJson([ - 'fromAssetImage', - 'some/path.png', - 1.0, - [640, 480, 1024] - ]); - }, throwsAssertionError); - }); }); }); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS index 493a0b4ef9c2..6f33f4aa0511 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Joonas Kerttula 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 42930348965f..20bf7527a658 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.4.1 +* Adds support for BitmapDescriptors created with `BitmapDescriptor.createFromAsset` or `BitmapDescriptor.createFromBytes`. * Updates minimum Flutter version to 3.0. ## 0.4.0+5 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md index 692814731bec..486c59267289 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -1,5 +1,7 @@ # google_maps_flutter_web + + This is an implementation of the [google_maps_flutter](https://pub.dev/packages/google_maps_flutter) plugin for web. Behind the scenes, it uses a14n's [google_maps](https://pub.dev/packages/google_maps) dart JS interop layer. ## Usage @@ -48,3 +50,25 @@ Indoor and building layers are still not available on the web. Traffic is. Only Android supports "[Lite Mode](https://developers.google.com/maps/documentation/android-sdk/lite)", so the `liteModeEnabled` constructor argument can't be set to `true` on web apps. Google Maps for web uses `HtmlElementView` to render maps. When a `GoogleMap` is stacked below other widgets, [`package:pointer_interceptor`](https://www.pub.dev/packages/pointer_interceptor) must be used to capture mouse events on the Flutter overlays. See issue [#73830](https://github.com/flutter/flutter/issues/73830). + +## Custom marker icons + +In order to achieve the best possible performance when using custom marker images on the web platform, the `size` parameter should be used to scale the marker when using `BitmapDescriptor.createFromAsset` or `BitmapDescriptor.createFromBytes` methods. + + +```dart +final ImageConfiguration imageConfiguration = createLocalImageConfiguration( + context, + size: const Size(48, 48), +); +final BitmapDescriptor bitmapDescriptor = + await BitmapDescriptor.createFromAsset( + imageConfiguration, 'assets/red_square.png'); +``` + + +```dart +final Uint8List bytes = _getMarkerImageBytes(); +final BitmapDescriptor bitmapDescriptor = + BitmapDescriptor.createFromBytes(bytes, size: const Size(48, 48)); +``` 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 0226234ea97a..737ab0407f3e 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 @@ -180,8 +180,8 @@ void main() { (WidgetTester tester) async { controller.dispose(); - expect(() { - controller.updateMarkers( + expect(() async { + await controller.updateMarkers( MarkerUpdates.from( const {}, const {}, @@ -616,7 +616,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 efde66459327..9e5b0dc41970 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; @@ -336,21 +338,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 a85bce31e20f..5f5089d9f7bc 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 @@ -253,13 +253,15 @@ class MockGoogleMapController extends _i1.Mock returnValueForMissingStub: null, ); @override - void updateMarkers(_i3.MarkerUpdates? updates) => super.noSuchMethod( + _i2.Future updateMarkers(_i3.MarkerUpdates? updates) => + (super.noSuchMethod( Invocation.method( #updateMarkers, [updates], ), - returnValueForMissingStub: null, - ); + returnValue: _i2.Future.value(), + returnValueForMissingStub: _i2.Future.value(), + ) as _i2.Future); @override void showInfoWindow(_i3.MarkerId? markerId) => super.noSuchMethod( Invocation.method( 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 e4c4dd7c0cfe..6087b72a3abf 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 @@ -38,7 +38,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'))); @@ -50,7 +50,7 @@ void main() { final Set markers = { const Marker(markerId: MarkerId('1')), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect( controller.markers[const MarkerId('1')]?.marker?.draggable, isFalse); @@ -59,7 +59,7 @@ void main() { final Set updatedMarkers = { const Marker(markerId: MarkerId('1'), draggable: true), }; - controller.changeMarkers(updatedMarkers); + await controller.changeMarkers(updatedMarkers); expect(controller.markers.length, 1); expect( @@ -73,7 +73,7 @@ void main() { const Marker(markerId: MarkerId('3')), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 3); @@ -99,7 +99,7 @@ void main() { ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers[const MarkerId('1')]?.infoWindowShown, isFalse); @@ -125,7 +125,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); @@ -148,11 +148,11 @@ void main() { final Set markers = { Marker( markerId: const MarkerId('1'), - icon: BitmapDescriptor.fromBytes(bytes), + icon: BitmapDescriptor.createFromBytes(bytes), ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final gmaps.Icon? icon = @@ -175,11 +175,12 @@ void main() { final Set markers = { Marker( markerId: const MarkerId('1'), - icon: BitmapDescriptor.fromBytes(bytes, size: const Size(20, 30)), + icon: + BitmapDescriptor.createFromBytes(bytes, size: const Size(20, 30)), ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final gmaps.Icon? icon = @@ -208,7 +209,7 @@ void main() { ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final html.HtmlElement? content = controller.markers[const MarkerId('1')] @@ -233,7 +234,7 @@ void main() { ), }; - controller.addMarkers(markers); + await controller.addMarkers(markers); expect(controller.markers.length, 1); final html.HtmlElement? content = controller.markers[const MarkerId('1')] diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart index e93a60e19906..45e45623823a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart @@ -20,6 +20,8 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { @override Widget build(BuildContext context) { - return const Text('Testing... Look at the console output for results!'); + return const Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!')); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/lib/readme_excerpts.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/readme_excerpts.dart new file mode 100644 index 000000000000..49f63c2aae0e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/readme_excerpts.dart @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('README snippet app'), + ), + body: const Text('See example in main.dart'), + ), + ); + } + + Future getIconFromAssets() async { + // #docregion CreateFromAsset + final ImageConfiguration imageConfiguration = createLocalImageConfiguration( + context, + size: const Size(48, 48), + ); + final BitmapDescriptor bitmapDescriptor = + await BitmapDescriptor.createFromAsset( + imageConfiguration, 'assets/red_square.png'); + // #enddocregion CreateFromAsset + return bitmapDescriptor; + } + + BitmapDescriptor getIconFromBytes() { + // #docregion CreateFromBytes + final Uint8List bytes = _getMarkerImageBytes(); + final BitmapDescriptor bitmapDescriptor = + BitmapDescriptor.createFromBytes(bytes, size: const Size(48, 48)); + // #enddocregion CreateFromBytes + return bitmapDescriptor; + } + + Uint8List _getMarkerImageBytes() => Uint8List(0); +} 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 43f67946464a..ead011d45d5e 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 @@ -26,3 +26,11 @@ dev_dependencies: integration_test: sdk: flutter mockito: ^5.3.2 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../../google_maps_flutter/google_maps_flutter_platform_interface + google_maps_flutter_web: + path: ../../../google_maps_flutter/google_maps_flutter_web diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index 0650184a14d0..cdcd9c61537a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -16,6 +16,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:image/image.dart' as img; import 'package:sanitize_html/sanitize_html.dart'; import 'package:stream_transform/stream_transform.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index 25cba849475b..adea7c17650b 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 @@ -241,8 +241,8 @@ gmaps.Size? _gmSizeFromIconConfig(List iconConfig, int sizeIndex) { final List? rawIconSize = iconConfig[sizeIndex] as List?; if (rawIconSize != null) { size = gmaps.Size( - rawIconSize[0] as num?, - rawIconSize[1] as num?, + rawIconSize[0]! as double, + rawIconSize[1]! as double, ); } } @@ -250,7 +250,8 @@ gmaps.Size? _gmSizeFromIconConfig(List iconConfig, int sizeIndex) { } // Converts a [BitmapDescriptor] into a [gmaps.Icon] that can be used in Markers. -gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { +Future _gmIconFromBitmapDescriptor( + BitmapDescriptor bitmapDescriptor) async { final List iconConfig = bitmapDescriptor.toJson() as List; gmaps.Icon? icon; @@ -282,6 +283,84 @@ gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { ..size = size ..scaledSize = size; } + } else if (iconConfig[0] == 'asset') { + assert(iconConfig.length >= 3); + // iconConfig[2] contains the DPIs of the screen, but that information is + // already encoded in the iconConfig[1] + + final String assetUrl = + ui.webOnlyAssetManager.getAssetUrl(iconConfig[1]! as String); + icon = gmaps.Icon()..url = assetUrl; + + switch (iconConfig[2]! as String) { + case BitmapDescriptor.bitmapAutoScaling: + if (iconConfig.length == 4) { + final double scale = iconConfig[3]! as double; + // Google Maps Web SDK does not support the scaling of the marker with anything other than + // the size parameter, therefore the width and height of the image must be read from the image. + // To avoid this, it is best to provide the image size instead of scale when using the web platform. + + final ByteData bytedata = + await ui.webOnlyAssetManager.load(iconConfig[1]! as String); + final Uint8List bytes = bytedata.buffer.asUint8List(); + final img.Decoder? decoder = img.findDecoderForData(bytes); + final img.DecodeInfo? decodeInfo = decoder?.startDecode(bytes); + + if (decodeInfo != null) { + final gmaps.Size size = gmaps.Size( + decodeInfo.width / scale, + decodeInfo.height / scale, + ); + icon.size = size; + icon.scaledSize = size; + } + } else if (iconConfig.length == 5) { + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 4); + if (size != null) { + icon.size = size; + icon.scaledSize = size; + } + } + break; + case BitmapDescriptor.bitmapNoScaling: + break; + } + } else if (iconConfig[0] == 'bytes') { + // Grab the bytes, and put them into a blob + final List bytes = iconConfig[1]! as List; + // Create a Blob from bytes, but let the browser figure out the encoding + final Blob blob = Blob([bytes]); + icon = gmaps.Icon()..url = Url.createObjectUrlFromBlob(blob); + switch (iconConfig[2]! as String) { + case BitmapDescriptor.bitmapAutoScaling: + if (iconConfig.length == 4) { + final double scale = iconConfig[3]! as double; + // Google Maps Web SDK does not support the scaling of the marker with anything other than + // the size parameter, therefore the width and height of the image must be read from the image. + // To avoid this, it is best to provide the image size instead of scale when using the web platform. + final img.Decoder? decoder = + img.findDecoderForData(bytes as Uint8List); + final img.DecodeInfo? decodeInfo = decoder?.startDecode(bytes); + + if (decodeInfo != null) { + final gmaps.Size size = gmaps.Size( + decodeInfo.width / scale, + decodeInfo.height / scale, + ); + icon.size = size; + icon.scaledSize = size; + } + } else if (iconConfig.length == 5) { + final gmaps.Size? size = _gmSizeFromIconConfig(iconConfig, 4); + if (size != null) { + icon.size = size; + icon.scaledSize = size; + } + } + break; + case BitmapDescriptor.bitmapNoScaling: + break; + } } } @@ -291,10 +370,10 @@ gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { // Computes the options for a new [gmaps.Marker] from an incoming set of options // [marker], and the existing marker registered with the map: [currentMarker]. // Preserves the position from the [currentMarker], if set. -gmaps.MarkerOptions _markerOptionsFromMarker( +Future _markerOptionsFromMarker( Marker marker, gmaps.Marker? currentMarker, -) { +) async { return gmaps.MarkerOptions() ..position = currentMarker?.position ?? gmaps.LatLng( @@ -306,7 +385,7 @@ gmaps.MarkerOptions _markerOptionsFromMarker( ..visible = marker.visible ..opacity = marker.alpha ..draggable = marker.draggable - ..icon = _gmIconFromBitmapDescriptor(marker.icon); + ..icon = await _gmIconFromBitmapDescriptor(marker.icon); // TODO(ditman): Compute anchor properly, otherwise infowindows attach to the wrong spot. // Flat and Rotation are not supported directly on the web. } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index a659fb218803..ec937ca29187 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -386,11 +386,11 @@ class GoogleMapController { } /// Applies [MarkerUpdates] to the currently managed markers. - void updateMarkers(MarkerUpdates updates) { + Future updateMarkers(MarkerUpdates updates) async { assert( _markersController != null, 'Cannot update markers after dispose().'); - _markersController?.addMarkers(updates.markersToAdd); - _markersController?.changeMarkers(updates.markersToChange); + await _markersController?.addMarkers(updates.markersToAdd); + await _markersController?.changeMarkers(updates.markersToChange); _markersController?.removeMarkers(updates.markerIdsToRemove); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index c2085a2bddfc..4941855ffd1a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -60,7 +60,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { MarkerUpdates markerUpdates, { required int mapId, }) async { - _map(mapId).updateMarkers(markerUpdates); + await _map(mapId).updateMarkers(markerUpdates); } /// Applies the passed in `polygonUpdates` to the `mapId`. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart index 1a712b109677..e18226600018 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -25,11 +25,13 @@ class MarkersController extends GeometryController { /// Adds a set of [Marker] objects to the cache. /// /// Wraps each [Marker] into its corresponding [MarkerController]. - void addMarkers(Set markersToAdd) { - markersToAdd.forEach(_addMarker); + Future addMarkers(Set markersToAdd) async { + for (final Marker marker in markersToAdd) { + await _addMarker(marker); + } } - void _addMarker(Marker marker) { + Future _addMarker(Marker marker) async { if (marker == null) { return; } @@ -55,7 +57,7 @@ class MarkersController extends GeometryController { _markerIdToController[marker.markerId]?.marker; final gmaps.MarkerOptions markerOptions = - _markerOptionsFromMarker(marker, currentMarker); + await _markerOptionsFromMarker(marker, currentMarker); final gmaps.Marker gmMarker = gmaps.Marker(markerOptions)..map = googleMap; final MarkerController controller = MarkerController( marker: gmMarker, @@ -79,15 +81,17 @@ class MarkersController extends GeometryController { } /// Updates a set of [Marker] objects with new options. - void changeMarkers(Set markersToChange) { - markersToChange.forEach(_changeMarker); + Future changeMarkers(Set markersToChange) async { + for (final Marker marker in markersToChange) { + await _changeMarker(marker); + } } - void _changeMarker(Marker marker) { + Future _changeMarker(Marker marker) async { final MarkerController? markerController = _markerIdToController[marker.markerId]; if (markerController != null) { - final gmaps.MarkerOptions markerOptions = _markerOptionsFromMarker( + final gmaps.MarkerOptions markerOptions = await _markerOptionsFromMarker( marker, markerController.marker, ); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart index 40d8f1903111..a20a6a6a18e3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html' as html; +import 'dart:typed_data'; // Fake interface for the logic that this package needs from (web-only) dart:ui. // This is conditionally exported so the analyzer sees these methods as available. @@ -25,8 +26,13 @@ class platformViewRegistry { /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L12 class webOnlyAssetManager { /// Shim for getAssetUrl. - /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L45 + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L59 static String getAssetUrl(String asset) => ''; + + /// Shim for load. + /// https://github.com/flutter/engine/blob/main/lib/web_ui/lib/src/engine/assets.dart#L67 + static Future load(String asset) async => + Future.value(ByteData(0)); } /// Signature of callbacks that have no arguments and return no data. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 072d584b133f..18290f3c2a7b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.4.0+5 +version: 0.4.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -23,6 +23,7 @@ dependencies: sdk: flutter google_maps: ^6.1.0 google_maps_flutter_platform_interface: ^2.2.2 + image: ^4.0.13 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 @@ -33,3 +34,9 @@ dev_dependencies: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + google_maps_flutter_platform_interface: + path: ../../google_maps_flutter/google_maps_flutter_platform_interface