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 a56f931dbc0..aee6c12b2cf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,7 +1,11 @@ +## 2.16.0 + +* Adds support for animating the camera with a duration. + ## 2.15.0 * Adds support for ground overlay. - + ## 2.14.14 * Updates compileSdk 34 to flutter.compileSdkVersion. 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 dc3fe1d5ad5..012f0689d7c 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 @@ -26,6 +26,7 @@ import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; +import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMapOptions; import com.google.android.gms.maps.MapView; @@ -976,12 +977,18 @@ public void moveCamera(@NonNull Messages.PlatformCameraUpdate cameraUpdate) { } @Override - public void animateCamera(@NonNull Messages.PlatformCameraUpdate cameraUpdate) { + public void animateCamera( + @NonNull Messages.PlatformCameraUpdate cameraUpdate, @Nullable Long durationMilliseconds) { if (googleMap == null) { throw new FlutterError( "GoogleMap uninitialized", "animateCamera called prior to map initialization", null); } - googleMap.animateCamera(Convert.cameraUpdateFromPigeon(cameraUpdate, density)); + CameraUpdate update = Convert.cameraUpdateFromPigeon(cameraUpdate, density); + if (durationMilliseconds != null) { + googleMap.animateCamera(update, durationMilliseconds.intValue(), null); + } else { + googleMap.animateCamera(update); + } } @Override @@ -1100,6 +1107,11 @@ public Boolean isLiteModeEnabled() { return Objects.requireNonNull(googleMap).isTrafficEnabled(); } + @Override + public @NonNull Messages.PlatformCameraPosition getCameraPosition() { + return Convert.cameraPositionToPigeon(Objects.requireNonNull(googleMap).getCameraPosition()); + } + @Override public @Nullable Messages.PlatformTileLayer getTileOverlayInfo(@NonNull String tileOverlayId) { TileOverlay tileOverlay = tileOverlaysController.getTileOverlay(tileOverlayId); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java index bc5ec22f43f..85e0b7926a5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Messages.java @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.7.3), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.googlemaps; @@ -6567,8 +6567,12 @@ void updateGroundOverlays( PlatformLatLngBounds getVisibleRegion(); /** Moves the camera according to [cameraUpdate] immediately, with no animation. */ void moveCamera(@NonNull PlatformCameraUpdate cameraUpdate); - /** Moves the camera according to [cameraUpdate], animating the update. */ - void animateCamera(@NonNull PlatformCameraUpdate cameraUpdate); + /** + * Moves the camera according to [cameraUpdate], animating the update using a duration in + * milliseconds if provided. + */ + void animateCamera( + @NonNull PlatformCameraUpdate cameraUpdate, @Nullable Long durationMilliseconds); /** Gets the current map zoom level. */ @NonNull Double getZoomLevel(); @@ -6996,8 +7000,9 @@ public void error(Throwable error) { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; PlatformCameraUpdate cameraUpdateArg = (PlatformCameraUpdate) args.get(0); + Long durationMillisecondsArg = (Long) args.get(1); try { - api.animateCamera(cameraUpdateArg); + api.animateCamera(cameraUpdateArg, durationMillisecondsArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -7809,6 +7814,9 @@ public interface MapsInspectorApi { @NonNull List getClusters(@NonNull String clusterManagerId); + @NonNull + PlatformCameraPosition getCameraPosition(); + /** The codec used by MapsInspectorApi. */ static @NonNull MessageCodec getCodec() { return PigeonCodec.INSTANCE; @@ -8176,6 +8184,29 @@ static void setUp( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.google_maps_flutter_android.MapsInspectorApi.getCameraPosition" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + PlatformCameraPosition output = api.getCameraPosition(); + wrapped.add(0, output); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index 65be99a3ff8..c03156e68c1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -7,16 +7,25 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; import android.os.Build; import androidx.activity.ComponentActivity; import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.maps.CameraUpdate; +import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.maps.android.clustering.ClusterManager; import io.flutter.plugin.common.BinaryMessenger; @@ -28,6 +37,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; @@ -250,4 +260,64 @@ public void UpdateHeatmaps() { verify(mockHeatmapsController, times(1)).changeHeatmaps(toChange); verify(mockHeatmapsController, times(1)).removeHeatmaps(idsToRemove); } + + @Test + public void AnimateCamera() { + GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); + googleMapController.onMapReady(mockGoogleMap); + + Messages.PlatformCameraUpdateZoomBy newCameraPosition = + new Messages.PlatformCameraUpdateZoomBy.Builder().setAmount(1.0).build(); + Messages.PlatformCameraUpdate cameraUpdate = + new Messages.PlatformCameraUpdate.Builder().setCameraUpdate(newCameraPosition).build(); + + try (MockedStatic mockedFactory = mockStatic(CameraUpdateFactory.class)) { + mockedFactory + .when(() -> CameraUpdateFactory.zoomBy(anyFloat())) + .thenReturn(mock(CameraUpdate.class)); + googleMapController.animateCamera(cameraUpdate, null); + } + + verify(mockGoogleMap, times(1)).animateCamera(any(CameraUpdate.class)); + } + + @Test + public void AnimateCameraWithDuration() { + GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); + googleMapController.onMapReady(mockGoogleMap); + + Messages.PlatformCameraUpdateZoomBy newCameraPosition = + new Messages.PlatformCameraUpdateZoomBy.Builder().setAmount(1.0).build(); + Messages.PlatformCameraUpdate cameraUpdate = + new Messages.PlatformCameraUpdate.Builder().setCameraUpdate(newCameraPosition).build(); + + Long durationMilliseconds = 1000L; + + try (MockedStatic mockedFactory = mockStatic(CameraUpdateFactory.class)) { + mockedFactory + .when(() -> CameraUpdateFactory.zoomBy(anyFloat())) + .thenReturn(mock(CameraUpdate.class)); + googleMapController.animateCamera(cameraUpdate, durationMilliseconds); + } + + verify(mockGoogleMap, times(1)) + .animateCamera(any(CameraUpdate.class), eq(durationMilliseconds.intValue()), isNull()); + } + + @Test + public void getCameraPositionReturnsCorrectData() { + GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies(); + googleMapController.onMapReady(mockGoogleMap); + + CameraPosition cameraPosition = new CameraPosition(new LatLng(10.0, 20.0), 15.0f, 30.0f, 45.0f); + when(mockGoogleMap.getCameraPosition()).thenReturn(cameraPosition); + + Messages.PlatformCameraPosition result = googleMapController.getCameraPosition(); + + Assert.assertEquals(cameraPosition.target.latitude, result.getTarget().getLatitude(), 1e-15); + Assert.assertEquals(cameraPosition.target.longitude, result.getTarget().getLongitude(), 1e-15); + Assert.assertEquals(cameraPosition.zoom, result.getZoom(), 1e-15); + Assert.assertEquals(cameraPosition.tilt, result.getTilt(), 1e-15); + Assert.assertEquals(cameraPosition.bearing, result.getBearing(), 1e-15); + } } 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 3fff5c2d01b..952c381df53 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 @@ -17,8 +17,10 @@ import 'resources/icon_image_base64.dart'; const LatLng _kInitialMapCenter = LatLng(0, 0); const double _kInitialZoomLevel = 5; -const CameraPosition _kInitialCameraPosition = - CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); +const CameraPosition _kInitialCameraPosition = CameraPosition( + target: _kInitialMapCenter, + zoom: _kInitialZoomLevel, +); const String _kCloudMapId = '000000000000000'; // Dummy map ID. // The tolerance value for floating-point comparisons in the tests. @@ -26,6 +28,19 @@ const String _kCloudMapId = '000000000000000'; // Dummy map ID. // There are multiple float conversions and calculations when data is converted // between Dart and platform implementations. const double _floatTolerance = 1e-8; +const double _kTestCameraZoomLevel = 10; +const double _kTestZoomByAmount = 2; +const LatLng _kTestMapCenter = LatLng(65, 25.5); +const CameraPosition _kTestCameraPosition = CameraPosition( + target: _kTestMapCenter, + zoom: _kTestCameraZoomLevel, + bearing: 1.0, + tilt: 1.0, +); +final LatLngBounds _testCameraBounds = LatLngBounds( + northeast: const LatLng(50, -65), southwest: const LatLng(28.5, -123)); +final ValueVariant _cameraUpdateTypeVariants = + ValueVariant(CameraUpdateType.values.toSet()); void googleMapsTests() { GoogleMapsFlutterPlatform.instance.enableDebugInspection(); @@ -1706,6 +1721,234 @@ void googleMapsTests() { } }); }); + + testWidgets( + 'testAnimateCameraWithoutDuration', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + /// Completer to track when the camera has come to rest. + Completer? cameraIdleCompleter; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onCameraIdle: () { + if (cameraIdleCompleter != null && + !cameraIdleCompleter.isCompleted) { + cameraIdleCompleter.complete(); + } + }, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and + // `mapControllerCompleter.complete(controller)` above should happen in + // `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + // Create completer for camera idle event. + cameraIdleCompleter = Completer(); + + final CameraUpdate cameraUpdate = + _getCameraUpdateForType(_cameraUpdateTypeVariants.currentValue!); + await controller.animateCamera(cameraUpdate); + + // Immediately after calling animateCamera, check that the camera hasn't + // reached its final position. This relies on the assumption that the + // camera move is animated and won't complete instantly. + final CameraPosition beforeFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + beforeFinishedPosition, + null, + controller, + (Matcher matcher) => isNot(matcher)); + + // Wait for the animation to complete (onCameraIdle). + expect(cameraIdleCompleter.isCompleted, isFalse); + await cameraIdleCompleter.future; + + // After onCameraIdle event, the camera should be at the final position. + final CameraPosition afterFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + afterFinishedPosition, + beforeFinishedPosition, + controller, + (Matcher matcher) => matcher); + + await tester.pumpAndSettle(); + }, + variant: _cameraUpdateTypeVariants, + // TODO(stuartmorgan): Remove skip once Maps API key is available for LUCI, + // https://github.com/flutter/flutter/issues/131071 + skip: true, + ); + + /// Tests animating the camera with specified durations to verify timing + /// behavior. + /// + /// This test checks two scenarios: short and long animation durations. + /// It uses a midpoint duration to ensure the short animation completes in + /// less time and the long animation takes more time than that midpoint. + /// This ensures that the animation duration is respected by the platform and + /// that the default camera animation duration does not affect the test + /// results. + testWidgets( + 'testAnimateCameraWithDuration', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + /// Completer to track when the camera has come to rest. + Completer? cameraIdleCompleter; + + const int shortCameraAnimationDurationMS = 200; + const int longCameraAnimationDurationMS = 1000; + + /// Calculate the midpoint duration of the animation test, which will + /// serve as a reference to verify that animations complete more quickly + /// with shorter durations and more slowly with longer durations. + const int animationDurationMiddlePoint = + (shortCameraAnimationDurationMS + longCameraAnimationDurationMS) ~/ 2; + + // Stopwatch to measure the time taken for the animation to complete. + final Stopwatch stopwatch = Stopwatch(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onCameraIdle: () { + if (cameraIdleCompleter != null && + !cameraIdleCompleter.isCompleted) { + stopwatch.stop(); + cameraIdleCompleter.complete(); + } + }, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and + // `mapControllerCompleter.complete(controller)` above should happen in + // `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + // Create completer for camera idle event. + cameraIdleCompleter = Completer(); + + // Start stopwatch to check the time taken for the animation to complete. + // Stopwatch is stopped on camera idle callback. + stopwatch.reset(); + stopwatch.start(); + + // First phase with shorter animation duration. + final CameraUpdate cameraUpdateShort = + _getCameraUpdateForType(_cameraUpdateTypeVariants.currentValue!); + await controller.animateCamera( + cameraUpdateShort, + duration: const Duration(milliseconds: shortCameraAnimationDurationMS), + ); + + // Wait for the animation to complete (onCameraIdle). + expect(cameraIdleCompleter.isCompleted, isFalse); + await cameraIdleCompleter.future; + + // For short animation duration, check that the animation is completed + // faster than the midpoint benchmark. + expect(stopwatch.elapsedMilliseconds, + lessThan(animationDurationMiddlePoint)); + + // Reset camera to initial position before testing long duration. + await controller + .moveCamera(CameraUpdate.newCameraPosition(_kInitialCameraPosition)); + await tester.pumpAndSettle(); + + // Create completer for camera idle event. + cameraIdleCompleter = Completer(); + + // Start stopwatch to check the time taken for the animation to complete. + // Stopwatch is stopped on camera idle callback. + stopwatch.reset(); + stopwatch.start(); + + // Second phase with longer animation duration. + final CameraUpdate cameraUpdateLong = + _getCameraUpdateForType(_cameraUpdateTypeVariants.currentValue!); + await controller.animateCamera( + cameraUpdateLong, + duration: const Duration(milliseconds: longCameraAnimationDurationMS), + ); + + // Immediately after calling animateCamera, check that the camera hasn't + // reached its final position. This relies on the assumption that the + // camera move is animated and won't complete instantly. + final CameraPosition beforeFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + beforeFinishedPosition, + null, + controller, + (Matcher matcher) => isNot(matcher)); + + // Wait for the animation to complete (onCameraIdle). + expect(cameraIdleCompleter.isCompleted, isFalse); + await cameraIdleCompleter.future; + + // For longer animation duration, check that the animation is completed + // slower than the midpoint benchmark. + expect(stopwatch.elapsedMilliseconds, + greaterThan(animationDurationMiddlePoint)); + + // Camera should be at the final position. + final CameraPosition afterFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + afterFinishedPosition, + beforeFinishedPosition, + controller, + (Matcher matcher) => matcher); + + await tester.pumpAndSettle(); + }, + variant: _cameraUpdateTypeVariants, + // TODO(stuartmorgan): Remove skip once Maps API key is available for LUCI, + // https://github.com/flutter/flutter/issues/131071 + skip: true, + ); } class _DebugTileProvider implements TileProvider { @@ -1774,3 +2017,89 @@ Marker _copyMarkerWithClusterManagerId( clusterManagerId: clusterManagerId, ); } + +CameraUpdate _getCameraUpdateForType(CameraUpdateType type) { + return switch (type) { + CameraUpdateType.newCameraPosition => + CameraUpdate.newCameraPosition(_kTestCameraPosition), + CameraUpdateType.newLatLng => CameraUpdate.newLatLng(_kTestMapCenter), + CameraUpdateType.newLatLngBounds => + CameraUpdate.newLatLngBounds(_testCameraBounds, 0), + CameraUpdateType.newLatLngZoom => + CameraUpdate.newLatLngZoom(_kTestMapCenter, _kTestCameraZoomLevel), + CameraUpdateType.scrollBy => CameraUpdate.scrollBy(10, 10), + CameraUpdateType.zoomBy => + CameraUpdate.zoomBy(_kTestZoomByAmount, const Offset(1, 1)), + CameraUpdateType.zoomTo => CameraUpdate.zoomTo(_kTestCameraZoomLevel), + CameraUpdateType.zoomIn => CameraUpdate.zoomIn(), + CameraUpdateType.zoomOut => CameraUpdate.zoomOut(), + }; +} + +Future _checkCameraUpdateByType( + CameraUpdateType type, + CameraPosition currentPosition, + CameraPosition? oldPosition, + ExampleGoogleMapController controller, + Matcher Function(Matcher matcher) wrapMatcher, +) async { + // As the target might differ a bit from the expected target, a threshold is + // used. + const double latLngThreshold = 0.05; + + switch (type) { + case CameraUpdateType.newCameraPosition: + expect(currentPosition.bearing, + wrapMatcher(equals(_kTestCameraPosition.bearing))); + expect( + currentPosition.zoom, wrapMatcher(equals(_kTestCameraPosition.zoom))); + expect( + currentPosition.tilt, wrapMatcher(equals(_kTestCameraPosition.tilt))); + expect( + currentPosition.target.latitude, + wrapMatcher( + closeTo(_kTestCameraPosition.target.latitude, latLngThreshold))); + expect( + currentPosition.target.longitude, + wrapMatcher( + closeTo(_kTestCameraPosition.target.longitude, latLngThreshold))); + case CameraUpdateType.newLatLng: + expect(currentPosition.target.latitude, + wrapMatcher(closeTo(_kTestMapCenter.latitude, latLngThreshold))); + expect(currentPosition.target.longitude, + wrapMatcher(closeTo(_kTestMapCenter.longitude, latLngThreshold))); + case CameraUpdateType.newLatLngBounds: + final LatLngBounds bounds = await controller.getVisibleRegion(); + expect( + bounds.northeast.longitude, + wrapMatcher( + closeTo(_testCameraBounds.northeast.longitude, latLngThreshold))); + expect( + bounds.southwest.longitude, + wrapMatcher( + closeTo(_testCameraBounds.southwest.longitude, latLngThreshold))); + case CameraUpdateType.newLatLngZoom: + expect(currentPosition.target.latitude, + wrapMatcher(closeTo(_kTestMapCenter.latitude, latLngThreshold))); + expect(currentPosition.target.longitude, + wrapMatcher(closeTo(_kTestMapCenter.longitude, latLngThreshold))); + expect(currentPosition.zoom, wrapMatcher(equals(_kTestCameraZoomLevel))); + case CameraUpdateType.scrollBy: + // For scrollBy, just check that the location has changed. + if (oldPosition != null) { + expect(currentPosition.target.latitude, + isNot(equals(oldPosition.target.latitude))); + expect(currentPosition.target.longitude, + isNot(equals(oldPosition.target.longitude))); + } + case CameraUpdateType.zoomBy: + expect(currentPosition.zoom, + wrapMatcher(equals(_kInitialZoomLevel + _kTestZoomByAmount))); + case CameraUpdateType.zoomTo: + expect(currentPosition.zoom, wrapMatcher(equals(_kTestCameraZoomLevel))); + case CameraUpdateType.zoomIn: + expect(currentPosition.zoom, wrapMatcher(equals(_kInitialZoomLevel + 1))); + case CameraUpdateType.zoomOut: + expect(currentPosition.zoom, wrapMatcher(equals(_kInitialZoomLevel - 1))); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/animate_camera.dart index c77f9ededac..ed4c744c48b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/animate_camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/animate_camera.dart @@ -26,14 +26,26 @@ class AnimateCamera extends StatefulWidget { State createState() => AnimateCameraState(); } +// Animation duration for a animation configuration. +const int _durationSeconds = 10; + class AnimateCameraState extends State { ExampleGoogleMapController? mapController; + Duration? _cameraUpdateAnimationDuration; // ignore: use_setters_to_change_properties void _onMapCreated(ExampleGoogleMapController controller) { mapController = controller; } + void _toggleAnimationDuration() { + setState(() { + _cameraUpdateAnimationDuration = _cameraUpdateAnimationDuration != null + ? null + : const Duration(seconds: _durationSeconds); + }); + } + @override Widget build(BuildContext context) { return Column( @@ -67,6 +79,7 @@ class AnimateCameraState extends State { zoom: 17.0, ), ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newCameraPosition'), @@ -77,6 +90,7 @@ class AnimateCameraState extends State { CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newLatLng'), @@ -91,6 +105,7 @@ class AnimateCameraState extends State { ), 10.0, ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newLatLngBounds'), @@ -102,6 +117,7 @@ class AnimateCameraState extends State { const LatLng(37.4231613, -122.087159), 11.0, ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newLatLngZoom'), @@ -110,6 +126,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.scrollBy(150.0, -225.0), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('scrollBy'), @@ -125,6 +142,7 @@ class AnimateCameraState extends State { -0.5, const Offset(30.0, 20.0), ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomBy with focus'), @@ -133,6 +151,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomBy(-0.5), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomBy'), @@ -141,6 +160,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomIn(), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomIn'), @@ -149,6 +169,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomOut(), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomOut'), @@ -157,6 +178,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomTo(16.0), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomTo'), @@ -164,7 +186,23 @@ class AnimateCameraState extends State { ], ), ], - ) + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'With 10 second duration', + textAlign: TextAlign.right, + ), + const SizedBox(width: 5), + Switch( + value: _cameraUpdateAnimationDuration != null, + onChanged: (bool value) { + _toggleAnimationDuration(); + }, + ), + ], + ), ], ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart index b0066680b89..e712208421c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/example_google_map.dart @@ -153,9 +153,10 @@ class ExampleGoogleMapController { } /// Starts an animated change of the map camera position. - Future animateCamera(CameraUpdate cameraUpdate) { - return GoogleMapsFlutterPlatform.instance - .animateCamera(cameraUpdate, mapId: mapId); + Future animateCamera(CameraUpdate cameraUpdate, {Duration? duration}) { + return GoogleMapsFlutterPlatform.instance.animateCameraWithConfiguration( + cameraUpdate, CameraUpdateAnimationConfiguration(duration: duration), + mapId: mapId); } /// Changes the map camera position. 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 8ed76b3155b..4acca270819 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_maps_flutter_platform_interface: ^2.10.0 + google_maps_flutter_platform_interface: ^2.11.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart index 6b3b759b119..8568329a6cf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/test/fake_google_maps_flutter_platform.dart @@ -124,6 +124,13 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { required int mapId, }) async {} + @override + Future animateCameraWithConfiguration( + CameraUpdate cameraUpdate, + CameraUpdateAnimationConfiguration configuration, { + required int mapId, + }) async {} + @override Future moveCamera( CameraUpdate cameraUpdate, { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart index 801a0bd5099..63dd910ce72 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_map_inspector_android.dart @@ -175,4 +175,22 @@ class GoogleMapsInspectorAndroid extends GoogleMapsInspectorPlatform { GoogleMapsFlutterAndroid.clusterFromPlatformCluster(cluster!)) .toList(); } + + @override + bool supportsGettingGameraPosition() => true; + + @override + Future getCameraPosition({required int mapId}) async { + final PlatformCameraPosition cameraPosition = + await _inspectorProvider(mapId)!.getCameraPosition(); + return CameraPosition( + target: LatLng( + cameraPosition.target.latitude, + cameraPosition.target.longitude, + ), + bearing: cameraPosition.bearing, + tilt: cameraPosition.tilt, + zoom: cameraPosition.zoom, + ); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index 816327a646c..ef713e6fd0a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -390,8 +390,20 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { CameraUpdate cameraUpdate, { required int mapId, }) { - return _hostApi(mapId) - .animateCamera(_platformCameraUpdateFromCameraUpdate(cameraUpdate)); + return animateCameraWithConfiguration( + cameraUpdate, const CameraUpdateAnimationConfiguration(), + mapId: mapId); + } + + @override + Future animateCameraWithConfiguration( + CameraUpdate cameraUpdate, + CameraUpdateAnimationConfiguration configuration, { + required int mapId, + }) { + return _hostApi(mapId).animateCamera( + _platformCameraUpdateFromCameraUpdate(cameraUpdate), + configuration.duration?.inMilliseconds); } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart index c8f27bd5d67..d77cb4b83ea 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.7.3), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -2260,8 +2260,10 @@ class MapsApi { } } - /// Moves the camera according to [cameraUpdate], animating the update. - Future animateCamera(PlatformCameraUpdate cameraUpdate) async { + /// Moves the camera according to [cameraUpdate], animating the update using a + /// duration in milliseconds if provided. + Future animateCamera( + PlatformCameraUpdate cameraUpdate, int? durationMilliseconds) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_maps_flutter_android.MapsApi.animateCamera$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -2270,8 +2272,8 @@ class MapsApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([cameraUpdate]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([cameraUpdate, durationMilliseconds]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -3584,4 +3586,33 @@ class MapsInspectorApi { .cast(); } } + + Future getCameraPosition() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_android.MapsInspectorApi.getCameraPosition$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as PlatformCameraPosition?)!; + } + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart index b6c9a26c050..04989b20e00 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/pigeons/messages.dart @@ -681,8 +681,10 @@ abstract class MapsApi { /// animation. void moveCamera(PlatformCameraUpdate cameraUpdate); - /// Moves the camera according to [cameraUpdate], animating the update. - void animateCamera(PlatformCameraUpdate cameraUpdate); + /// Moves the camera according to [cameraUpdate], animating the update using a + /// duration in milliseconds if provided. + void animateCamera( + PlatformCameraUpdate cameraUpdate, int? durationMilliseconds); /// Gets the current map zoom level. double getZoomLevel(); @@ -813,4 +815,5 @@ abstract class MapsInspectorApi { PlatformGroundOverlay? getGroundOverlayInfo(String groundOverlayId); PlatformZoomRange getZoomRange(); List getClusters(String clusterManagerId); + PlatformCameraPosition getCameraPosition(); } 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 a74c69c3338..ad3b86b3ad2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.15.0 +version: 2.16.0 environment: sdk: ^3.6.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.10.0 + google_maps_flutter_platform_interface: ^2.11.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart index b9bfc945e37..210be3c3e01 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart @@ -131,7 +131,7 @@ void main() { await maps.animateCamera(update, mapId: mapId); final VerificationResult verification = - verify(api.animateCamera(captureAny)); + verify(api.animateCamera(captureAny, captureAny)); final PlatformCameraUpdate passedUpdate = verification.captured[0] as PlatformCameraUpdate; final PlatformCameraUpdateScrollBy scroll = @@ -139,6 +139,34 @@ void main() { update as CameraUpdateScrollBy; expect(scroll.dx, update.dx); expect(scroll.dy, update.dy); + final int? passedDuration = verification.captured[1] as int?; + expect(passedDuration, isNull); + }); + + test('animateCameraWithConfiguration calls through', () async { + const int mapId = 1; + final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = + setUpMockMap(mapId: mapId); + + final CameraUpdate update = CameraUpdate.scrollBy(10, 20); + const CameraUpdateAnimationConfiguration configuration = + CameraUpdateAnimationConfiguration(duration: Duration(seconds: 1)); + expect(configuration.duration?.inSeconds, 1); + await maps.animateCameraWithConfiguration(update, configuration, + mapId: mapId); + + final VerificationResult verification = + verify(api.animateCamera(captureAny, captureAny)); + final PlatformCameraUpdate passedUpdate = + verification.captured[0] as PlatformCameraUpdate; + final PlatformCameraUpdateScrollBy scroll = + passedUpdate.cameraUpdate as PlatformCameraUpdateScrollBy; + update as CameraUpdateScrollBy; + expect(scroll.dx, update.dx); + expect(scroll.dy, update.dy); + + final int? passedDuration = verification.captured[1] as int?; + expect(passedDuration, configuration.duration?.inMilliseconds); }); test('getZoomLevel passes values correctly', () async { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart index cd9ebcb887b..463abd22234 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.5 from annotations // in google_maps_flutter_android/test/google_maps_flutter_android_test.dart. // Do not manually edit this file. @@ -18,41 +18,27 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class class _FakePlatformPoint_0 extends _i1.SmartFake implements _i2.PlatformPoint { - _FakePlatformPoint_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakePlatformPoint_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakePlatformLatLng_1 extends _i1.SmartFake implements _i2.PlatformLatLng { - _FakePlatformLatLng_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakePlatformLatLng_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakePlatformLatLngBounds_2 extends _i1.SmartFake implements _i2.PlatformLatLngBounds { - _FakePlatformLatLngBounds_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakePlatformLatLngBounds_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } /// A class which mocks [MapsApi]. @@ -74,22 +60,17 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { @override _i4.Future waitForMap() => (super.noSuchMethod( - Invocation.method( - #waitForMap, - [], - ), + Invocation.method(#waitForMap, []), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future updateMapConfiguration( - _i2.PlatformMapConfiguration? configuration) => + _i2.PlatformMapConfiguration? configuration, + ) => (super.noSuchMethod( - Invocation.method( - #updateMapConfiguration, - [configuration], - ), + Invocation.method(#updateMapConfiguration, [configuration]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -101,14 +82,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateCircles, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateCircles, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -120,14 +94,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateHeatmaps, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateHeatmaps, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -138,13 +105,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateClusterManagers, - [ - toAdd, - idsToRemove, - ], - ), + Invocation.method(#updateClusterManagers, [toAdd, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -156,14 +117,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateMarkers, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateMarkers, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -175,14 +129,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updatePolygons, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updatePolygons, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -194,14 +141,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updatePolylines, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updatePolylines, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -213,14 +153,11 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateTileOverlays, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateTileOverlays, [ + toAdd, + toChange, + idsToRemove, + ]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -232,193 +169,150 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateGroundOverlays, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateGroundOverlays, [ + toAdd, + toChange, + idsToRemove, + ]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future<_i2.PlatformPoint> getScreenCoordinate( - _i2.PlatformLatLng? latLng) => + _i2.PlatformLatLng? latLng, + ) => (super.noSuchMethod( - Invocation.method( - #getScreenCoordinate, - [latLng], - ), - returnValue: _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( - this, - Invocation.method( - #getScreenCoordinate, - [latLng], + Invocation.method(#getScreenCoordinate, [latLng]), + returnValue: _i4.Future<_i2.PlatformPoint>.value( + _FakePlatformPoint_0( + this, + Invocation.method(#getScreenCoordinate, [latLng]), ), - )), - returnValueForMissingStub: - _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( - this, - Invocation.method( - #getScreenCoordinate, - [latLng], + ), + returnValueForMissingStub: _i4.Future<_i2.PlatformPoint>.value( + _FakePlatformPoint_0( + this, + Invocation.method(#getScreenCoordinate, [latLng]), ), - )), + ), ) as _i4.Future<_i2.PlatformPoint>); @override _i4.Future<_i2.PlatformLatLng> getLatLng( - _i2.PlatformPoint? screenCoordinate) => + _i2.PlatformPoint? screenCoordinate, + ) => (super.noSuchMethod( - Invocation.method( - #getLatLng, - [screenCoordinate], - ), - returnValue: _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( - this, - Invocation.method( - #getLatLng, - [screenCoordinate], + Invocation.method(#getLatLng, [screenCoordinate]), + returnValue: _i4.Future<_i2.PlatformLatLng>.value( + _FakePlatformLatLng_1( + this, + Invocation.method(#getLatLng, [screenCoordinate]), ), - )), - returnValueForMissingStub: - _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( - this, - Invocation.method( - #getLatLng, - [screenCoordinate], + ), + returnValueForMissingStub: _i4.Future<_i2.PlatformLatLng>.value( + _FakePlatformLatLng_1( + this, + Invocation.method(#getLatLng, [screenCoordinate]), ), - )), + ), ) as _i4.Future<_i2.PlatformLatLng>); @override _i4.Future<_i2.PlatformLatLngBounds> getVisibleRegion() => (super.noSuchMethod( - Invocation.method( - #getVisibleRegion, - [], - ), + Invocation.method(#getVisibleRegion, []), returnValue: _i4.Future<_i2.PlatformLatLngBounds>.value( - _FakePlatformLatLngBounds_2( - this, - Invocation.method( - #getVisibleRegion, - [], + _FakePlatformLatLngBounds_2( + this, + Invocation.method(#getVisibleRegion, []), ), - )), + ), returnValueForMissingStub: _i4.Future<_i2.PlatformLatLngBounds>.value( - _FakePlatformLatLngBounds_2( - this, - Invocation.method( - #getVisibleRegion, - [], + _FakePlatformLatLngBounds_2( + this, + Invocation.method(#getVisibleRegion, []), ), - )), + ), ) as _i4.Future<_i2.PlatformLatLngBounds>); @override _i4.Future moveCamera(_i2.PlatformCameraUpdate? cameraUpdate) => (super.noSuchMethod( - Invocation.method( - #moveCamera, - [cameraUpdate], - ), + Invocation.method(#moveCamera, [cameraUpdate]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future animateCamera(_i2.PlatformCameraUpdate? cameraUpdate) => + _i4.Future animateCamera( + _i2.PlatformCameraUpdate? cameraUpdate, + int? durationMilliseconds, + ) => (super.noSuchMethod( - Invocation.method( - #animateCamera, - [cameraUpdate], - ), + Invocation.method(#animateCamera, [ + cameraUpdate, + durationMilliseconds, + ]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future getZoomLevel() => (super.noSuchMethod( - Invocation.method( - #getZoomLevel, - [], - ), + Invocation.method(#getZoomLevel, []), returnValue: _i4.Future.value(0.0), returnValueForMissingStub: _i4.Future.value(0.0), ) as _i4.Future); @override _i4.Future showInfoWindow(String? markerId) => (super.noSuchMethod( - Invocation.method( - #showInfoWindow, - [markerId], - ), + Invocation.method(#showInfoWindow, [markerId]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future hideInfoWindow(String? markerId) => (super.noSuchMethod( - Invocation.method( - #hideInfoWindow, - [markerId], - ), + Invocation.method(#hideInfoWindow, [markerId]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future isInfoWindowShown(String? markerId) => (super.noSuchMethod( - Invocation.method( - #isInfoWindowShown, - [markerId], - ), + Invocation.method(#isInfoWindowShown, [markerId]), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.Future); @override _i4.Future setStyle(String? style) => (super.noSuchMethod( - Invocation.method( - #setStyle, - [style], - ), + Invocation.method(#setStyle, [style]), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.Future); @override _i4.Future didLastStyleSucceed() => (super.noSuchMethod( - Invocation.method( - #didLastStyleSucceed, - [], - ), + Invocation.method(#didLastStyleSucceed, []), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.Future); @override _i4.Future clearTileCache(String? tileOverlayId) => (super.noSuchMethod( - Invocation.method( - #clearTileCache, - [tileOverlayId], - ), + Invocation.method(#clearTileCache, [tileOverlayId]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future<_i5.Uint8List> takeSnapshot() => (super.noSuchMethod( - Invocation.method( - #takeSnapshot, - [], - ), + Invocation.method(#takeSnapshot, []), returnValue: _i4.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - returnValueForMissingStub: - _i4.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), + returnValueForMissingStub: _i4.Future<_i5.Uint8List>.value( + _i5.Uint8List(0), + ), ) as _i4.Future<_i5.Uint8List>); } 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 dd58bd9e23b..9fb403c399f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.15.0 + +* Adds support for animating the camera with a duration. + ## 2.14.0 * Adds support for ground overlay. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart index f80acd15ddd..8883fd47020 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/integration_test/google_maps_test.dart @@ -17,8 +17,10 @@ import 'resources/icon_image_base64.dart'; const LatLng _kInitialMapCenter = LatLng(0, 0); const double _kInitialZoomLevel = 5; -const CameraPosition _kInitialCameraPosition = - CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); +const CameraPosition _kInitialCameraPosition = CameraPosition( + target: _kInitialMapCenter, + zoom: _kInitialZoomLevel, +); const String _kCloudMapId = '000000000000000'; // Dummy map ID. // The tolerance value for floating-point comparisons in the tests. @@ -26,6 +28,19 @@ const String _kCloudMapId = '000000000000000'; // Dummy map ID. // There are multiple float conversions and calculations when data is converted // between Dart and platform implementations. const double _floatTolerance = 1e-6; +const double _kTestCameraZoomLevel = 10; +const double _kTestZoomByAmount = 2; +const LatLng _kTestMapCenter = LatLng(65, 25.5); +const CameraPosition _kTestCameraPosition = CameraPosition( + target: _kTestMapCenter, + zoom: _kTestCameraZoomLevel, + bearing: 1.0, + tilt: 1.0, +); +final LatLngBounds _testCameraBounds = LatLngBounds( + northeast: const LatLng(50, -65), southwest: const LatLng(28.5, -123)); +final ValueVariant _cameraUpdateTypeVariants = + ValueVariant(CameraUpdateType.values.toSet()); void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -1550,6 +1565,228 @@ void main() { } }); }); + + testWidgets( + 'testAnimateCameraWithoutDuration', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + /// Completer to track when the camera has come to rest. + Completer? cameraIdleCompleter; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onCameraIdle: () { + if (cameraIdleCompleter != null && + !cameraIdleCompleter.isCompleted) { + cameraIdleCompleter.complete(); + } + }, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and + // `mapControllerCompleter.complete(controller)` above should happen in + // `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + // Create completer for camera idle event. + cameraIdleCompleter = Completer(); + + final CameraUpdate cameraUpdate = + _getCameraUpdateForType(_cameraUpdateTypeVariants.currentValue!); + await controller.animateCamera(cameraUpdate); + + // Immediately after calling animateCamera, check that the camera hasn't + // reached its final position. This relies on the assumption that the + // camera move is animated and won't complete instantly. + final CameraPosition beforeFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + beforeFinishedPosition, + null, + controller, + (Matcher matcher) => isNot(matcher)); + + // Wait for the animation to complete (onCameraIdle). + expect(cameraIdleCompleter.isCompleted, isFalse); + await cameraIdleCompleter.future; + + // After onCameraIdle event, the camera should be at the final position. + final CameraPosition afterFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + afterFinishedPosition, + beforeFinishedPosition, + controller, + (Matcher matcher) => matcher); + + await tester.pumpAndSettle(); + }, + variant: _cameraUpdateTypeVariants, + ); + + /// Tests animating the camera with specified durations to verify timing + /// behavior. + /// + /// This test checks two scenarios: short and long animation durations. + /// It uses a midpoint duration to ensure the short animation completes in + /// less time and the long animation takes more time than that midpoint. + /// This ensures that the animation duration is respected by the platform and + /// that the default camera animation duration does not affect the test + /// results. + testWidgets( + 'testAnimateCameraWithDuration', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + final GoogleMapsInspectorPlatform inspector = + GoogleMapsInspectorPlatform.instance!; + + /// Completer to track when the camera has come to rest. + Completer? cameraIdleCompleter; + + const int shortCameraAnimationDurationMS = 200; + const int longCameraAnimationDurationMS = 1000; + + /// Calculate the midpoint duration of the animation test, which will + /// serve as a reference to verify that animations complete more quickly + /// with shorter durations and more slowly with longer durations. + const int animationDurationMiddlePoint = + (shortCameraAnimationDurationMS + longCameraAnimationDurationMS) ~/ 2; + + // Stopwatch to measure the time taken for the animation to complete. + final Stopwatch stopwatch = Stopwatch(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ExampleGoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onCameraIdle: () { + if (cameraIdleCompleter != null && + !cameraIdleCompleter.isCompleted) { + stopwatch.stop(); + cameraIdleCompleter.complete(); + } + }, + onMapCreated: (ExampleGoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final ExampleGoogleMapController controller = + await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and + // `mapControllerCompleter.complete(controller)` above should happen in + // `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(const Duration(seconds: 1)); + + // Create completer for camera idle event. + cameraIdleCompleter = Completer(); + + // Start stopwatch to check the time taken for the animation to complete. + // Stopwatch is stopped on camera idle callback. + stopwatch.reset(); + stopwatch.start(); + + // First phase with shorter animation duration. + final CameraUpdate cameraUpdateShort = + _getCameraUpdateForType(_cameraUpdateTypeVariants.currentValue!); + await controller.animateCamera( + cameraUpdateShort, + duration: const Duration(milliseconds: shortCameraAnimationDurationMS), + ); + + // Wait for the animation to complete (onCameraIdle). + expect(cameraIdleCompleter.isCompleted, isFalse); + await cameraIdleCompleter.future; + + // For short animation duration, check that the animation is completed + // faster than the midpoint benchmark. + expect(stopwatch.elapsedMilliseconds, + lessThan(animationDurationMiddlePoint)); + + // Reset camera to initial position before testing long duration. + await controller + .moveCamera(CameraUpdate.newCameraPosition(_kInitialCameraPosition)); + await tester.pumpAndSettle(); + + // Create completer for camera idle event. + cameraIdleCompleter = Completer(); + + // Start stopwatch to check the time taken for the animation to complete. + // Stopwatch is stopped on camera idle callback. + stopwatch.reset(); + stopwatch.start(); + + // Second phase with longer animation duration. + final CameraUpdate cameraUpdateLong = + _getCameraUpdateForType(_cameraUpdateTypeVariants.currentValue!); + await controller.animateCamera( + cameraUpdateLong, + duration: const Duration(milliseconds: longCameraAnimationDurationMS), + ); + + // Immediately after calling animateCamera, check that the camera hasn't + // reached its final position. This relies on the assumption that the + // camera move is animated and won't complete instantly. + final CameraPosition beforeFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + beforeFinishedPosition, + null, + controller, + (Matcher matcher) => isNot(matcher)); + + // Wait for the animation to complete (onCameraIdle). + expect(cameraIdleCompleter.isCompleted, isFalse); + await cameraIdleCompleter.future; + + // For longer animation duration, check that the animation is completed + // slower than the midpoint benchmark. + expect(stopwatch.elapsedMilliseconds, + greaterThan(animationDurationMiddlePoint)); + + // Camera should be at the final position. + final CameraPosition afterFinishedPosition = + await inspector.getCameraPosition(mapId: controller.mapId); + await _checkCameraUpdateByType( + _cameraUpdateTypeVariants.currentValue!, + afterFinishedPosition, + beforeFinishedPosition, + controller, + (Matcher matcher) => matcher); + + await tester.pumpAndSettle(); + }, + variant: _cameraUpdateTypeVariants, + ); } class _DebugTileProvider implements TileProvider { @@ -1618,3 +1855,89 @@ Marker _copyMarkerWithClusterManagerId( clusterManagerId: clusterManagerId, ); } + +CameraUpdate _getCameraUpdateForType(CameraUpdateType type) { + return switch (type) { + CameraUpdateType.newCameraPosition => + CameraUpdate.newCameraPosition(_kTestCameraPosition), + CameraUpdateType.newLatLng => CameraUpdate.newLatLng(_kTestMapCenter), + CameraUpdateType.newLatLngBounds => + CameraUpdate.newLatLngBounds(_testCameraBounds, 0), + CameraUpdateType.newLatLngZoom => + CameraUpdate.newLatLngZoom(_kTestMapCenter, _kTestCameraZoomLevel), + CameraUpdateType.scrollBy => CameraUpdate.scrollBy(10, 10), + CameraUpdateType.zoomBy => + CameraUpdate.zoomBy(_kTestZoomByAmount, const Offset(1, 1)), + CameraUpdateType.zoomTo => CameraUpdate.zoomTo(_kTestCameraZoomLevel), + CameraUpdateType.zoomIn => CameraUpdate.zoomIn(), + CameraUpdateType.zoomOut => CameraUpdate.zoomOut(), + }; +} + +Future _checkCameraUpdateByType( + CameraUpdateType type, + CameraPosition currentPosition, + CameraPosition? oldPosition, + ExampleGoogleMapController controller, + Matcher Function(Matcher matcher) wrapMatcher, +) async { + // As the target might differ a bit from the expected target, a threshold is + // used. + const double latLngThreshold = 0.05; + + switch (type) { + case CameraUpdateType.newCameraPosition: + expect(currentPosition.bearing, + wrapMatcher(equals(_kTestCameraPosition.bearing))); + expect( + currentPosition.zoom, wrapMatcher(equals(_kTestCameraPosition.zoom))); + expect( + currentPosition.tilt, wrapMatcher(equals(_kTestCameraPosition.tilt))); + expect( + currentPosition.target.latitude, + wrapMatcher( + closeTo(_kTestCameraPosition.target.latitude, latLngThreshold))); + expect( + currentPosition.target.longitude, + wrapMatcher( + closeTo(_kTestCameraPosition.target.longitude, latLngThreshold))); + case CameraUpdateType.newLatLng: + expect(currentPosition.target.latitude, + wrapMatcher(closeTo(_kTestMapCenter.latitude, latLngThreshold))); + expect(currentPosition.target.longitude, + wrapMatcher(closeTo(_kTestMapCenter.longitude, latLngThreshold))); + case CameraUpdateType.newLatLngBounds: + final LatLngBounds bounds = await controller.getVisibleRegion(); + expect( + bounds.northeast.longitude, + wrapMatcher( + closeTo(_testCameraBounds.northeast.longitude, latLngThreshold))); + expect( + bounds.southwest.longitude, + wrapMatcher( + closeTo(_testCameraBounds.southwest.longitude, latLngThreshold))); + case CameraUpdateType.newLatLngZoom: + expect(currentPosition.target.latitude, + wrapMatcher(closeTo(_kTestMapCenter.latitude, latLngThreshold))); + expect(currentPosition.target.longitude, + wrapMatcher(closeTo(_kTestMapCenter.longitude, latLngThreshold))); + expect(currentPosition.zoom, wrapMatcher(equals(_kTestCameraZoomLevel))); + case CameraUpdateType.scrollBy: + // For scrollBy, just check that the location has changed. + if (oldPosition != null) { + expect(currentPosition.target.latitude, + isNot(equals(oldPosition.target.latitude))); + expect(currentPosition.target.longitude, + isNot(equals(oldPosition.target.longitude))); + } + case CameraUpdateType.zoomBy: + expect(currentPosition.zoom, + wrapMatcher(equals(_kInitialZoomLevel + _kTestZoomByAmount))); + case CameraUpdateType.zoomTo: + expect(currentPosition.zoom, wrapMatcher(equals(_kTestCameraZoomLevel))); + case CameraUpdateType.zoomIn: + expect(currentPosition.zoom, wrapMatcher(equals(_kInitialZoomLevel + 1))); + case CameraUpdateType.zoomOut: + expect(currentPosition.zoom, wrapMatcher(equals(_kInitialZoomLevel - 1))); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m index 6d821b4d11f..4982d4a59ce 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/ios/RunnerTests/GoogleMapsTests.m @@ -8,6 +8,7 @@ @import GoogleMaps; #import +#import "FGMCATransactionWrapper.h" #import "PartiallyMockedMapView.h" @interface FLTGoogleMapFactory (Test) @@ -86,6 +87,112 @@ - (void)testHandleResultTileDownsamplesWideGamutImages { XCTAssert(bitsPerComponent == 8); } +- (void)testAnimateCameraWithUpdate { + NSObject *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + + CGRect frame = CGRectMake(0, 0, 100, 100); + GMSMapViewOptions *mapViewOptions = [[GMSMapViewOptions alloc] init]; + mapViewOptions.frame = frame; + + // Init camera with zero zoom. + mapViewOptions.camera = [[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]; + + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions]; + + FLTGoogleMapController *controller = + [[FLTGoogleMapController alloc] initWithMapView:mapView + viewIdentifier:0 + creationParameters:[self emptyCreationParameters] + registrar:registrar]; + + id mapViewMock = OCMPartialMock(mapView); + id mockTransactionWrapper = OCMProtocolMock(@protocol(FGMCATransactionProtocol)); + controller.callHandler.transactionWrapper = mockTransactionWrapper; + + FGMPlatformCameraUpdateZoomTo *zoomTo = [FGMPlatformCameraUpdateZoomTo makeWithZoom:10.0]; + FGMPlatformCameraUpdate *cameraUpdate = [FGMPlatformCameraUpdate makeWithCameraUpdate:zoomTo]; + FlutterError *error = nil; + + OCMReject([mockTransactionWrapper begin]); + OCMReject([mockTransactionWrapper commit]); + OCMExpect([mapViewMock animateWithCameraUpdate:[OCMArg any]]); + [controller.callHandler animateCameraWithUpdate:cameraUpdate duration:nil error:&error]; + OCMVerifyAll(mapViewMock); + OCMVerifyAll(mockTransactionWrapper); +} + +- (void)testAnimateCameraWithUpdateAndDuration { + NSObject *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + + CGRect frame = CGRectMake(0, 0, 100, 100); + GMSMapViewOptions *mapViewOptions = [[GMSMapViewOptions alloc] init]; + mapViewOptions.frame = frame; + + // Init camera with zero zoom. + mapViewOptions.camera = [[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]; + + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions]; + + FLTGoogleMapController *controller = + [[FLTGoogleMapController alloc] initWithMapView:mapView + viewIdentifier:0 + creationParameters:[self emptyCreationParameters] + registrar:registrar]; + + id mapViewMock = OCMPartialMock(mapView); + id mockTransactionWrapper = OCMProtocolMock(@protocol(FGMCATransactionProtocol)); + controller.callHandler.transactionWrapper = mockTransactionWrapper; + + FGMPlatformCameraUpdateZoomTo *zoomTo = [FGMPlatformCameraUpdateZoomTo makeWithZoom:10.0]; + FGMPlatformCameraUpdate *cameraUpdate = [FGMPlatformCameraUpdate makeWithCameraUpdate:zoomTo]; + FlutterError *error = nil; + + NSNumber *durationMilliseconds = @100; + OCMExpect([mockTransactionWrapper begin]); + OCMExpect( + [mockTransactionWrapper setAnimationDuration:[durationMilliseconds doubleValue] / 1000]); + OCMExpect([mockTransactionWrapper commit]); + OCMExpect([mapViewMock animateWithCameraUpdate:[OCMArg any]]); + [controller.callHandler animateCameraWithUpdate:cameraUpdate + duration:durationMilliseconds + error:&error]; + OCMVerifyAll(mapViewMock); + OCMVerifyAll(mockTransactionWrapper); +} + +- (void)testInspectorAPICameraPosition { + NSObject *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + + CGRect frame = CGRectMake(0, 0, 100, 100); + GMSMapViewOptions *mapViewOptions = [[GMSMapViewOptions alloc] init]; + mapViewOptions.frame = frame; + + // Init camera with specific position. + GMSCameraPosition *initialCameraPosition = [[GMSCameraPosition alloc] initWithLatitude:37.7749 + longitude:-122.4194 + zoom:10]; + mapViewOptions.camera = initialCameraPosition; + + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] initWithOptions:mapViewOptions]; + + FLTGoogleMapController *controller = + [[FLTGoogleMapController alloc] initWithMapView:mapView + viewIdentifier:0 + creationParameters:[self emptyCreationParameters] + registrar:registrar]; + + FGMMapInspector *inspector = [[FGMMapInspector alloc] initWithMapController:controller + messenger:registrar.messenger + pigeonSuffix:@"0"]; + + FlutterError *error = nil; + FGMPlatformCameraPosition *cameraPosition = [inspector cameraPosition:&error]; + + XCTAssertEqual(cameraPosition.target.latitude, initialCameraPosition.target.latitude); + XCTAssertEqual(cameraPosition.target.longitude, initialCameraPosition.target.longitude); + XCTAssertEqual(cameraPosition.zoom, initialCameraPosition.zoom); +} + /// Creates an empty creation paramaters object for tests where the values don't matter, just that /// there's a valid object to pass in. - (FGMPlatformMapViewCreationParams *)emptyCreationParameters { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml index 5d4f5d3cedd..947a78b4241 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios14/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../ - google_maps_flutter_platform_interface: ^2.10.0 + google_maps_flutter_platform_interface: ^2.11.0 maps_example_dart: path: ../shared/maps_example_dart/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml index 5d4f5d3cedd..947a78b4241 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios15/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../ - google_maps_flutter_platform_interface: ^2.10.0 + google_maps_flutter_platform_interface: ^2.11.0 maps_example_dart: path: ../shared/maps_example_dart/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/animate_camera.dart index c77f9ededac..ed4c744c48b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/animate_camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/animate_camera.dart @@ -26,14 +26,26 @@ class AnimateCamera extends StatefulWidget { State createState() => AnimateCameraState(); } +// Animation duration for a animation configuration. +const int _durationSeconds = 10; + class AnimateCameraState extends State { ExampleGoogleMapController? mapController; + Duration? _cameraUpdateAnimationDuration; // ignore: use_setters_to_change_properties void _onMapCreated(ExampleGoogleMapController controller) { mapController = controller; } + void _toggleAnimationDuration() { + setState(() { + _cameraUpdateAnimationDuration = _cameraUpdateAnimationDuration != null + ? null + : const Duration(seconds: _durationSeconds); + }); + } + @override Widget build(BuildContext context) { return Column( @@ -67,6 +79,7 @@ class AnimateCameraState extends State { zoom: 17.0, ), ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newCameraPosition'), @@ -77,6 +90,7 @@ class AnimateCameraState extends State { CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newLatLng'), @@ -91,6 +105,7 @@ class AnimateCameraState extends State { ), 10.0, ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newLatLngBounds'), @@ -102,6 +117,7 @@ class AnimateCameraState extends State { const LatLng(37.4231613, -122.087159), 11.0, ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('newLatLngZoom'), @@ -110,6 +126,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.scrollBy(150.0, -225.0), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('scrollBy'), @@ -125,6 +142,7 @@ class AnimateCameraState extends State { -0.5, const Offset(30.0, 20.0), ), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomBy with focus'), @@ -133,6 +151,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomBy(-0.5), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomBy'), @@ -141,6 +160,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomIn(), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomIn'), @@ -149,6 +169,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomOut(), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomOut'), @@ -157,6 +178,7 @@ class AnimateCameraState extends State { onPressed: () { mapController?.animateCamera( CameraUpdate.zoomTo(16.0), + duration: _cameraUpdateAnimationDuration, ); }, child: const Text('zoomTo'), @@ -164,7 +186,23 @@ class AnimateCameraState extends State { ], ), ], - ) + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'With 10 second duration', + textAlign: TextAlign.right, + ), + const SizedBox(width: 5), + Switch( + value: _cameraUpdateAnimationDuration != null, + onChanged: (bool value) { + _toggleAnimationDuration(); + }, + ), + ], + ), ], ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart index 378993cb476..351e1c117c4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart @@ -153,9 +153,10 @@ class ExampleGoogleMapController { } /// Starts an animated change of the map camera position. - Future animateCamera(CameraUpdate cameraUpdate) { - return GoogleMapsFlutterPlatform.instance - .animateCamera(cameraUpdate, mapId: mapId); + Future animateCamera(CameraUpdate cameraUpdate, {Duration? duration}) { + return GoogleMapsFlutterPlatform.instance.animateCameraWithConfiguration( + cameraUpdate, CameraUpdateAnimationConfiguration(duration: duration), + mapId: mapId); } /// Changes the map camera position. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml index e098970f1ac..071506ac425 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../../../ - google_maps_flutter_platform_interface: ^2.10.0 + google_maps_flutter_platform_interface: ^2.11.0 dev_dependencies: flutter_test: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart index cb3d24fef72..7e35a4e0bd9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/test/fake_google_maps_flutter_platform.dart @@ -124,6 +124,13 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { required int mapId, }) async {} + @override + Future animateCameraWithConfiguration( + CameraUpdate cameraUpdate, + CameraUpdateAnimationConfiguration configuration, { + required int mapId, + }) async {} + @override Future moveCamera( CameraUpdate cameraUpdate, { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMCATransactionWrapper.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMCATransactionWrapper.h new file mode 100644 index 00000000000..ed456917484 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMCATransactionWrapper.h @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Protocol for CATransaction to allow mocking in tests. +@protocol FGMCATransactionProtocol +- (void)begin; +- (void)commit; +- (void)setAnimationDuration:(CFTimeInterval)duration; +@end + +/// Wrapper for CATransaction to allow mocking in tests. +@interface FGMCATransactionWrapper : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMCATransactionWrapper.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMCATransactionWrapper.m new file mode 100644 index 00000000000..8c8e8b2a352 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/FGMCATransactionWrapper.m @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FGMCATransactionWrapper.h" +#import + +@implementation FGMCATransactionWrapper + +- (void)begin { + [CATransaction begin]; +} + +- (void)commit { + [CATransaction commit]; +} + +- (void)setAnimationDuration:(CFTimeInterval)duration { + [CATransaction setAnimationDuration:duration]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h index a2b75d74c75..f96bd244504 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.h @@ -5,6 +5,7 @@ #import #import +#import "FGMCATransactionWrapper.h" #import "FGMClusterManagersController.h" #import "GoogleMapCircleController.h" #import "GoogleMapMarkerController.h" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m index a57a9ff5183..6844a67fb9b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController.m @@ -5,6 +5,7 @@ @import GoogleMapsUtils; #import "GoogleMapController.h" +#import "GoogleMapController_Test.h" #import "FGMGroundOverlayController.h" #import "FGMMarkerUserData.h" @@ -65,21 +66,12 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar #pragma mark - -/// Implementation of the Pigeon maps API. -/// -/// This is a separate object from the maps controller because the Pigeon API registration keeps a -/// strong reference to the implementor, but as the FlutterPlatformView, the lifetime of the -/// FLTGoogleMapController instance is what needs to trigger Pigeon unregistration, so can't be -/// the target of the registration. -@interface FGMMapCallHandler : NSObject +/// Private declarations of the FGMMapCallHandler. +@interface FGMMapCallHandler () - (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controller messenger:(NSObject *)messenger pigeonSuffix:(NSString *)suffix; -@end -/// Private declarations. -// This is separate in case the above is made public in the future (e.g., for unit testing). -@interface FGMMapCallHandler () /// The map controller this inspector corresponds to. @property(nonatomic, weak) FLTGoogleMapController *controller; /// The messenger this instance was registered with by Pigeon. @@ -90,21 +82,9 @@ @interface FGMMapCallHandler () #pragma mark - -/// Implementation of the Pigeon maps inspector API. -/// -/// This is a separate object from the maps controller because the Pigeon API registration keeps a -/// strong reference to the implementor, but as the FlutterPlatformView, the lifetime of the -/// FLTGoogleMapController instance is what needs to trigger Pigeon unregistration, so can't be -/// the target of the registration. -@interface FGMMapInspector : NSObject -- (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controller - messenger:(NSObject *)messenger - pigeonSuffix:(NSString *)suffix; -@end - -/// Private declarations. -// This is separate in case the above is made public in the future (e.g., for unit testing). +/// Private declarations of the FGMMapInspector. @interface FGMMapInspector () + /// The map controller this inspector corresponds to. @property(nonatomic, weak) FLTGoogleMapController *controller; /// The messenger this instance was registered with by Pigeon. @@ -524,6 +504,7 @@ - (void)interpretMapConfiguration:(FGMPlatformMapConfiguration *)config { #pragma mark - +/// Private declarations of the FGMMapCallHandler. @implementation FGMMapCallHandler - (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controller @@ -534,6 +515,7 @@ - (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controll _controller = controller; _messenger = messenger; _pigeonSuffix = suffix; + _transactionWrapper = [[FGMCATransactionWrapper alloc] init]; } return self; } @@ -674,6 +656,7 @@ - (void)moveCameraWithUpdate:(nonnull FGMPlatformCameraUpdate *)cameraUpdate } - (void)animateCameraWithUpdate:(nonnull FGMPlatformCameraUpdate *)cameraUpdate + duration:(nullable NSNumber *)durationMilliseconds error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { GMSCameraUpdate *update = FGMGetCameraUpdateForPigeonCameraUpdate(cameraUpdate); if (!update) { @@ -682,7 +665,11 @@ - (void)animateCameraWithUpdate:(nonnull FGMPlatformCameraUpdate *)cameraUpdate details:nil]; return; } + FGMCATransactionWrapper *transaction = durationMilliseconds ? self.transactionWrapper : nil; + [transaction begin]; + [transaction setAnimationDuration:[durationMilliseconds doubleValue] / 1000]; [self.controller.mapView animateWithCameraUpdate:update]; + [transaction commit]; } - (nullable NSNumber *)currentZoomLevel:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { @@ -748,6 +735,7 @@ - (nullable FlutterStandardTypedData *)takeSnapshotWithError: #pragma mark - +/// Private declarations of the FGMMapInspector. @implementation FGMMapInspector - (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controller @@ -846,4 +834,9 @@ - (nullable FGMPlatformZoomRange *)zoomRange: return [self.controller.groundOverlaysController groundOverlayWithIdentifier:groundOverlayId]; } +- (nullable FGMPlatformCameraPosition *)cameraPosition: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return FGMGetPigeonCameraPositionForPosition(self.controller.mapView.camera); +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController_Test.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController_Test.h index da4da303ff7..d144c478e65 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController_Test.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/GoogleMapController_Test.h @@ -5,8 +5,39 @@ #import #import +#import "FGMCATransactionWrapper.h" +#import "GoogleMapController.h" + NS_ASSUME_NONNULL_BEGIN +/// Implementation of the Pigeon maps API. +/// +/// This is a separate object from the maps controller because the Pigeon API registration keeps a +/// strong reference to the implementor, but as the FlutterPlatformView, the lifetime of the +/// FLTGoogleMapController instance is what needs to trigger Pigeon unregistration, so can't be +/// the target of the registration. +@interface FGMMapCallHandler : NSObject + +/// The transaction wrapper to use for camera animations. +@property(nonatomic, strong) id transactionWrapper; + +@end + +/// Implementation of the Pigeon maps inspector API. +/// +/// This is a separate object from the maps controller because the Pigeon API registration keeps a +/// strong reference to the implementor, but as the FlutterPlatformView, the lifetime of the +/// FLTGoogleMapController instance is what needs to trigger Pigeon unregistration, so can't be +/// the target of the registration. +@interface FGMMapInspector : NSObject + +/// Initializes a Pigeon API for inpector with a map controller. +- (instancetype)initWithMapController:(nonnull FLTGoogleMapController *)controller + messenger:(NSObject *)messenger + pigeonSuffix:(NSString *)suffix; + +@end + @interface FLTGoogleMapController (Test) /// Initializes a map controller with a concrete map view. @@ -20,6 +51,9 @@ NS_ASSUME_NONNULL_BEGIN creationParameters:(FGMPlatformMapViewCreationParams *)creationParameters registrar:(NSObject *)registrar; +// The main Pigeon API implementation. +@property(nonatomic, strong, readonly) FGMMapCallHandler *callHandler; + @end NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h index 1f51686fecb..0d1c3935e8a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.7.3), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -723,8 +723,10 @@ NSObject *FGMGetMessagesCodec(void); /// animation. - (void)moveCameraWithUpdate:(FGMPlatformCameraUpdate *)cameraUpdate error:(FlutterError *_Nullable *_Nonnull)error; -/// Moves the camera according to [cameraUpdate], animating the update. +/// Moves the camera according to [cameraUpdate], animating the update using a +/// duration in milliseconds if provided. - (void)animateCameraWithUpdate:(FGMPlatformCameraUpdate *)cameraUpdate + duration:(nullable NSNumber *)durationMilliseconds error:(FlutterError *_Nullable *_Nonnull)error; /// Gets the current map zoom level. /// @@ -876,6 +878,8 @@ extern void SetUpFGMMapsPlatformViewApiWithSuffix(id bin - (nullable NSArray *) clustersWithIdentifier:(NSString *)clusterManagerId error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable FGMPlatformCameraPosition *)cameraPosition:(FlutterError *_Nullable *_Nonnull)error; @end extern void SetUpFGMMapsInspectorApi(id binaryMessenger, diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m index 045a4459330..ccf0735ec94 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.7.3), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" @@ -2280,7 +2280,8 @@ void SetUpFGMMapsApiWithSuffix(id binaryMessenger, [channel setMessageHandler:nil]; } } - /// Moves the camera according to [cameraUpdate], animating the update. + /// Moves the camera according to [cameraUpdate], animating the update using a + /// duration in milliseconds if provided. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:[NSString @@ -2291,14 +2292,18 @@ void SetUpFGMMapsApiWithSuffix(id binaryMessenger, binaryMessenger:binaryMessenger codec:FGMGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(animateCameraWithUpdate:error:)], - @"FGMMapsApi api (%@) doesn't respond to @selector(animateCameraWithUpdate:error:)", + NSCAssert([api respondsToSelector:@selector(animateCameraWithUpdate:duration:error:)], + @"FGMMapsApi api (%@) doesn't respond to " + @"@selector(animateCameraWithUpdate:duration:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FGMPlatformCameraUpdate *arg_cameraUpdate = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_durationMilliseconds = GetNullableObjectAtIndex(args, 1); FlutterError *error; - [api animateCameraWithUpdate:arg_cameraUpdate error:&error]; + [api animateCameraWithUpdate:arg_cameraUpdate + duration:arg_durationMilliseconds + error:&error]; callback(wrapResult(nil, error)); }]; } else { @@ -3278,4 +3283,24 @@ void SetUpFGMMapsInspectorApiWithSuffix(id binaryMesseng [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_maps_flutter_ios." + @"MapsInspectorApi.getCameraPosition", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FGMGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(cameraPosition:)], + @"FGMMapsInspectorApi api (%@) doesn't respond to @selector(cameraPosition:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + FGMPlatformCameraPosition *output = [api cameraPosition:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart index 7461a4f2aa4..0c12fe3efef 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_map_inspector_ios.dart @@ -198,4 +198,22 @@ class GoogleMapsInspectorIOS extends GoogleMapsInspectorPlatform { GoogleMapsFlutterIOS.clusterFromPlatformCluster(cluster)) .toList(); } + + @override + bool supportsGettingGameraPosition() => true; + + @override + Future getCameraPosition({required int mapId}) async { + final PlatformCameraPosition cameraPosition = + await _inspectorProvider(mapId)!.getCameraPosition(); + return CameraPosition( + target: LatLng( + cameraPosition.target.latitude, + cameraPosition.target.longitude, + ), + bearing: cameraPosition.bearing, + tilt: cameraPosition.tilt, + zoom: cameraPosition.zoom, + ); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart index 475c40bcbad..ab76471fc19 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -377,8 +377,20 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { CameraUpdate cameraUpdate, { required int mapId, }) { - return _hostApi(mapId) - .animateCamera(_platformCameraUpdateFromCameraUpdate(cameraUpdate)); + return animateCameraWithConfiguration( + cameraUpdate, const CameraUpdateAnimationConfiguration(), + mapId: mapId); + } + + @override + Future animateCameraWithConfiguration( + CameraUpdate cameraUpdate, + CameraUpdateAnimationConfiguration configuration, { + required int mapId, + }) { + return _hostApi(mapId).animateCamera( + _platformCameraUpdateFromCameraUpdate(cameraUpdate), + configuration.duration?.inMilliseconds); } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart index ade49313866..c7b67e500c9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.7.3), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -2155,8 +2155,10 @@ class MapsApi { } } - /// Moves the camera according to [cameraUpdate], animating the update. - Future animateCamera(PlatformCameraUpdate cameraUpdate) async { + /// Moves the camera according to [cameraUpdate], animating the update using a + /// duration in milliseconds if provided. + Future animateCamera( + PlatformCameraUpdate cameraUpdate, int? durationMilliseconds) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_maps_flutter_ios.MapsApi.animateCamera$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -2165,8 +2167,8 @@ class MapsApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([cameraUpdate]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([cameraUpdate, durationMilliseconds]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -3354,4 +3356,33 @@ class MapsInspectorApi { .cast(); } } + + Future getCameraPosition() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_maps_flutter_ios.MapsInspectorApi.getCameraPosition$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as PlatformCameraPosition?)!; + } + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart b/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart index 2509fdee7f2..dcbe6691bd1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pigeons/messages.dart @@ -649,9 +649,11 @@ abstract class MapsApi { @ObjCSelector('moveCameraWithUpdate:') void moveCamera(PlatformCameraUpdate cameraUpdate); - /// Moves the camera according to [cameraUpdate], animating the update. - @ObjCSelector('animateCameraWithUpdate:') - void animateCamera(PlatformCameraUpdate cameraUpdate); + /// Moves the camera according to [cameraUpdate], animating the update using a + /// duration in milliseconds if provided. + @ObjCSelector('animateCameraWithUpdate:duration:') + void animateCamera( + PlatformCameraUpdate cameraUpdate, int? durationMilliseconds); /// Gets the current map zoom level. @ObjCSelector('currentZoomLevel') @@ -794,4 +796,6 @@ abstract class MapsInspectorApi { PlatformZoomRange getZoomRange(); @ObjCSelector('clustersWithIdentifier:') List getClusters(String clusterManagerId); + @ObjCSelector('cameraPosition') + PlatformCameraPosition getCameraPosition(); } 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 6a0a0ebfed5..32520c7e3fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_ios description: iOS implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.14.0 +version: 2.15.0 environment: sdk: ^3.4.0 @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - google_maps_flutter_platform_interface: ^2.10.0 + google_maps_flutter_platform_interface: ^2.11.0 stream_transform: ^2.0.0 dev_dependencies: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart index 99ee3577962..ba920814a7e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart @@ -131,7 +131,7 @@ void main() { await maps.animateCamera(update, mapId: mapId); final VerificationResult verification = - verify(api.animateCamera(captureAny)); + verify(api.animateCamera(captureAny, captureAny)); final PlatformCameraUpdate passedUpdate = verification.captured[0] as PlatformCameraUpdate; final PlatformCameraUpdateScrollBy scroll = @@ -139,6 +139,36 @@ void main() { update as CameraUpdateScrollBy; expect(scroll.dx, update.dx); expect(scroll.dy, update.dy); + expect(verification.captured[1], isNull); + }); + + test('animateCameraWithConfiguration calls through', () async { + const int mapId = 1; + final (GoogleMapsFlutterIOS maps, MockMapsApi api) = + setUpMockMap(mapId: mapId); + + final CameraUpdate update = CameraUpdate.scrollBy(10, 20); + const CameraUpdateAnimationConfiguration configuration = + CameraUpdateAnimationConfiguration(duration: Duration(seconds: 1)); + expect(configuration.duration?.inSeconds, 1); + await maps.animateCameraWithConfiguration( + update, + configuration, + mapId: mapId, + ); + + final VerificationResult verification = + verify(api.animateCamera(captureAny, captureAny)); + final PlatformCameraUpdate passedUpdate = + verification.captured[0] as PlatformCameraUpdate; + final PlatformCameraUpdateScrollBy scroll = + passedUpdate.cameraUpdate as PlatformCameraUpdateScrollBy; + update as CameraUpdateScrollBy; + expect(scroll.dx, update.dx); + expect(scroll.dy, update.dy); + + final int? passedDuration = verification.captured[1] as int?; + expect(passedDuration, configuration.duration?.inMilliseconds); }); test('getZoomLevel passes values correctly', () async { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart index abff2a3e6d9..8d64047847f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.mocks.dart @@ -25,35 +25,20 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: subtype_of_sealed_class class _FakePlatformPoint_0 extends _i1.SmartFake implements _i2.PlatformPoint { - _FakePlatformPoint_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakePlatformPoint_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakePlatformLatLng_1 extends _i1.SmartFake implements _i2.PlatformLatLng { - _FakePlatformLatLng_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakePlatformLatLng_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } class _FakePlatformLatLngBounds_2 extends _i1.SmartFake implements _i2.PlatformLatLngBounds { - _FakePlatformLatLngBounds_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); + _FakePlatformLatLngBounds_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } /// A class which mocks [MapsApi]. @@ -75,22 +60,17 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { @override _i4.Future waitForMap() => (super.noSuchMethod( - Invocation.method( - #waitForMap, - [], - ), + Invocation.method(#waitForMap, []), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future updateMapConfiguration( - _i2.PlatformMapConfiguration? configuration) => + _i2.PlatformMapConfiguration? configuration, + ) => (super.noSuchMethod( - Invocation.method( - #updateMapConfiguration, - [configuration], - ), + Invocation.method(#updateMapConfiguration, [configuration]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -102,14 +82,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateCircles, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateCircles, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -121,14 +94,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateHeatmaps, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateHeatmaps, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -139,13 +105,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateClusterManagers, - [ - toAdd, - idsToRemove, - ], - ), + Invocation.method(#updateClusterManagers, [toAdd, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -157,14 +117,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateMarkers, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateMarkers, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -176,14 +129,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updatePolygons, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updatePolygons, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -195,14 +141,7 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updatePolylines, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updatePolylines, [toAdd, toChange, idsToRemove]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -214,14 +153,11 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateTileOverlays, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateTileOverlays, [ + toAdd, + toChange, + idsToRemove, + ]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @@ -233,191 +169,147 @@ class MockMapsApi extends _i1.Mock implements _i2.MapsApi { List? idsToRemove, ) => (super.noSuchMethod( - Invocation.method( - #updateGroundOverlays, - [ - toAdd, - toChange, - idsToRemove, - ], - ), + Invocation.method(#updateGroundOverlays, [ + toAdd, + toChange, + idsToRemove, + ]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future<_i2.PlatformPoint> getScreenCoordinate( - _i2.PlatformLatLng? latLng) => + _i2.PlatformLatLng? latLng, + ) => (super.noSuchMethod( - Invocation.method( - #getScreenCoordinate, - [latLng], - ), - returnValue: _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( - this, - Invocation.method( - #getScreenCoordinate, - [latLng], + Invocation.method(#getScreenCoordinate, [latLng]), + returnValue: _i4.Future<_i2.PlatformPoint>.value( + _FakePlatformPoint_0( + this, + Invocation.method(#getScreenCoordinate, [latLng]), ), - )), - returnValueForMissingStub: - _i4.Future<_i2.PlatformPoint>.value(_FakePlatformPoint_0( - this, - Invocation.method( - #getScreenCoordinate, - [latLng], + ), + returnValueForMissingStub: _i4.Future<_i2.PlatformPoint>.value( + _FakePlatformPoint_0( + this, + Invocation.method(#getScreenCoordinate, [latLng]), ), - )), + ), ) as _i4.Future<_i2.PlatformPoint>); @override _i4.Future<_i2.PlatformLatLng> getLatLng( - _i2.PlatformPoint? screenCoordinate) => + _i2.PlatformPoint? screenCoordinate, + ) => (super.noSuchMethod( - Invocation.method( - #getLatLng, - [screenCoordinate], - ), - returnValue: _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( - this, - Invocation.method( - #getLatLng, - [screenCoordinate], + Invocation.method(#getLatLng, [screenCoordinate]), + returnValue: _i4.Future<_i2.PlatformLatLng>.value( + _FakePlatformLatLng_1( + this, + Invocation.method(#getLatLng, [screenCoordinate]), ), - )), - returnValueForMissingStub: - _i4.Future<_i2.PlatformLatLng>.value(_FakePlatformLatLng_1( - this, - Invocation.method( - #getLatLng, - [screenCoordinate], + ), + returnValueForMissingStub: _i4.Future<_i2.PlatformLatLng>.value( + _FakePlatformLatLng_1( + this, + Invocation.method(#getLatLng, [screenCoordinate]), ), - )), + ), ) as _i4.Future<_i2.PlatformLatLng>); @override _i4.Future<_i2.PlatformLatLngBounds> getVisibleRegion() => (super.noSuchMethod( - Invocation.method( - #getVisibleRegion, - [], - ), + Invocation.method(#getVisibleRegion, []), returnValue: _i4.Future<_i2.PlatformLatLngBounds>.value( - _FakePlatformLatLngBounds_2( - this, - Invocation.method( - #getVisibleRegion, - [], + _FakePlatformLatLngBounds_2( + this, + Invocation.method(#getVisibleRegion, []), ), - )), + ), returnValueForMissingStub: _i4.Future<_i2.PlatformLatLngBounds>.value( - _FakePlatformLatLngBounds_2( - this, - Invocation.method( - #getVisibleRegion, - [], + _FakePlatformLatLngBounds_2( + this, + Invocation.method(#getVisibleRegion, []), ), - )), + ), ) as _i4.Future<_i2.PlatformLatLngBounds>); @override _i4.Future moveCamera(_i2.PlatformCameraUpdate? cameraUpdate) => (super.noSuchMethod( - Invocation.method( - #moveCamera, - [cameraUpdate], - ), + Invocation.method(#moveCamera, [cameraUpdate]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future animateCamera(_i2.PlatformCameraUpdate? cameraUpdate) => + _i4.Future animateCamera( + _i2.PlatformCameraUpdate? cameraUpdate, + int? durationMilliseconds, + ) => (super.noSuchMethod( - Invocation.method( - #animateCamera, - [cameraUpdate], - ), + Invocation.method(#animateCamera, [ + cameraUpdate, + durationMilliseconds, + ]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future getZoomLevel() => (super.noSuchMethod( - Invocation.method( - #getZoomLevel, - [], - ), + Invocation.method(#getZoomLevel, []), returnValue: _i4.Future.value(0.0), returnValueForMissingStub: _i4.Future.value(0.0), ) as _i4.Future); @override _i4.Future showInfoWindow(String? markerId) => (super.noSuchMethod( - Invocation.method( - #showInfoWindow, - [markerId], - ), + Invocation.method(#showInfoWindow, [markerId]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future hideInfoWindow(String? markerId) => (super.noSuchMethod( - Invocation.method( - #hideInfoWindow, - [markerId], - ), + Invocation.method(#hideInfoWindow, [markerId]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future isInfoWindowShown(String? markerId) => (super.noSuchMethod( - Invocation.method( - #isInfoWindowShown, - [markerId], - ), + Invocation.method(#isInfoWindowShown, [markerId]), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.Future); @override _i4.Future setStyle(String? style) => (super.noSuchMethod( - Invocation.method( - #setStyle, - [style], - ), + Invocation.method(#setStyle, [style]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future getLastStyleError() => (super.noSuchMethod( - Invocation.method( - #getLastStyleError, - [], - ), + Invocation.method(#getLastStyleError, []), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future clearTileCache(String? tileOverlayId) => (super.noSuchMethod( - Invocation.method( - #clearTileCache, - [tileOverlayId], - ), + Invocation.method(#clearTileCache, [tileOverlayId]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override _i4.Future<_i5.Uint8List?> takeSnapshot() => (super.noSuchMethod( - Invocation.method( - #takeSnapshot, - [], - ), + Invocation.method(#takeSnapshot, []), returnValue: _i4.Future<_i5.Uint8List?>.value(), returnValueForMissingStub: _i4.Future<_i5.Uint8List?>.value(), ) as _i4.Future<_i5.Uint8List?>);