Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[google_maps_flutter] Add ability to animate camera with duration #7648

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ android {

defaultConfig {
applicationId "io.flutter.plugins.googlemapsexample"
minSdkVersion 20
minSdkVersion flutter.minSdkVersion
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:async';

import 'package:async/async.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Expand All @@ -13,6 +14,20 @@ import 'package:integration_test/integration_test.dart';

import 'shared.dart';

const double _kTestCameraZoomLevel = 10;
const double _kTestZoombByAmount = 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<CameraUpdateType> _cameraUpdateTypeVariants =
ValueVariant<CameraUpdateType>(CameraUpdateType.values.toSet());

/// Integration Tests that use the [GoogleMapsInspectorPlatform].
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -612,6 +627,220 @@ void runTests() {
expect(clusters.length, 0);
}
});

testWidgets('testAnimateCamera', (WidgetTester tester) async {
final Key key = GlobalKey();
final Completer<GoogleMapController> controllerCompleter =
Completer<GoogleMapController>();
final GoogleMapsInspectorPlatform inspector =
GoogleMapsInspectorPlatform.instance!;

final StreamController<void> cameraFiredStream = StreamController<void>();
final StreamQueue<void> cameraFiredQueue =
StreamQueue<void>(cameraFiredStream.stream);

bool cameraIdleFired = false;

await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: GoogleMap(
key: key,
initialCameraPosition: kInitialCameraPosition,
onCameraIdle: () {
cameraIdleFired = true;
cameraFiredStream.add(null);
},
onMapCreated: (GoogleMapController controller) {
controllerCompleter.complete(controller);
},
),
));

final GoogleMapController 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<void>.delayed(const Duration(seconds: 1));

// Drain any event that might have been added from the initial setup or
// resetting the camera position.
if (cameraIdleFired) {
await cameraFiredQueue.next;
cameraIdleFired = false;
}

final CameraUpdate cameraUpdate =
_getCameraUpdateForType(_cameraUpdateTypeVariants.currentValue!);
await controller.animateCamera(cameraUpdate);

// Check that position is not updated immediately to the target.
final CameraPosition beforeFinishedPosition =
await inspector.getCameraPosition(mapId: controller.mapId);
await _checkCameraUpdateByType(
_cameraUpdateTypeVariants.currentValue!,
beforeFinishedPosition,
null,
controller,
(Matcher matcher) => isNot(matcher));

// Check that position is animated after the animation is done.
expect(cameraIdleFired, isFalse);
await cameraFiredQueue.next;
expect(cameraIdleFired, isTrue);
final CameraPosition afterFinishedPosition =
await inspector.getCameraPosition(mapId: controller.mapId);
await _checkCameraUpdateByType(
_cameraUpdateTypeVariants.currentValue!,
afterFinishedPosition,
beforeFinishedPosition,
controller,
(Matcher matcher) => matcher);

await tester.pumpAndSettle();
}, variant: _cameraUpdateTypeVariants);

testWidgets(
'testAnimateCameraWithConfiguration',
(WidgetTester tester) async {
final Key key = GlobalKey();
final Completer<GoogleMapController> controllerCompleter =
Completer<GoogleMapController>();
final GoogleMapsInspectorPlatform inspector =
GoogleMapsInspectorPlatform.instance!;

final StreamController<void> cameraFiredStream = StreamController<void>();
final StreamQueue<void> cameraFiredQueue =
StreamQueue<void>(cameraFiredStream.stream);

const int shortCameraAnimationDurationMS = 10;
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();

bool cameraIdleFired = false;

await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: GoogleMap(
key: key,
initialCameraPosition: kInitialCameraPosition,
onCameraIdle: () {
stopwatch.stop();
cameraIdleFired = true;
cameraFiredStream.add(null);
},
onMapCreated: (GoogleMapController controller) {
controllerCompleter.complete(controller);
},
),
));

final GoogleMapController 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<void>.delayed(const Duration(seconds: 1));

// Drain any event that might have been added from the initial setup or
// resetting the camera position.
if (cameraIdleFired) {
await cameraFiredQueue.next;
cameraIdleFired = false;
}

// 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 camera idle callback to fire.
expect(cameraIdleFired, isFalse);
await cameraFiredQueue.next;
expect(cameraIdleFired, isTrue);

// For short animation duration, check that the animation is completed
// faster than the middle point of the animation test durations.
expect(stopwatch.elapsedMilliseconds,
lessThan(animationDurationMiddlePoint));

// Reset camera to initial position before second phase.
await controller
.moveCamera(CameraUpdate.newCameraPosition(kInitialCameraPosition));
await tester.pumpAndSettle();

// Drain camera idle event.
if (cameraIdleFired) {
await cameraFiredQueue.next;
cameraIdleFired = false;
}

// 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),
);

// Check that position is not updated immediately to the target.
final CameraPosition beforeFinishedPosition =
await inspector.getCameraPosition(mapId: controller.mapId);
await _checkCameraUpdateByType(
_cameraUpdateTypeVariants.currentValue!,
beforeFinishedPosition,
null,
controller,
(Matcher matcher) => isNot(matcher));

// Wait for the camera idle callback to fire.
expect(cameraIdleFired, isFalse);
await cameraFiredQueue.next;
expect(cameraIdleFired, isTrue);

// For short animation duration, check that the animation is completed
// shower than the middle point of the animation test durations.
expect(stopwatch.elapsedMilliseconds,
greaterThan(animationDurationMiddlePoint));

// Check that position is animated after the animation is done.
final CameraPosition afterFinishedPosition =
await inspector.getCameraPosition(mapId: controller.mapId);
await _checkCameraUpdateByType(
_cameraUpdateTypeVariants.currentValue!,
afterFinishedPosition,
beforeFinishedPosition,
controller,
(Matcher matcher) => matcher);

await tester.pumpAndSettle();
},
variant: _cameraUpdateTypeVariants,
skip: kIsWeb,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need a tracking issue. Or is the plan to have web support ready before the app-facing PR lands?

);
}

Marker _copyMarkerWithClusterManagerId(
Expand All @@ -636,3 +865,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(_kTestZoombByAmount, const Offset(1, 1)),
CameraUpdateType.zoomTo => CameraUpdate.zoomTo(_kTestCameraZoomLevel),
CameraUpdateType.zoomIn => CameraUpdate.zoomIn(),
CameraUpdateType.zoomOut => CameraUpdate.zoomOut(),
};
}

Future<void> _checkCameraUpdateByType(
CameraUpdateType type,
CameraPosition currentPosition,
CameraPosition? oldPosition,
GoogleMapController 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 + _kTestZoombByAmount)));
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)));
}
}
Loading