Skip to content
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
4 changes: 4 additions & 0 deletions packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.17.1

* Adds support for `CameraUpdate.newLatLngBoundsWithEdgeInsets`.

## 2.17.0

* Adds missing re-exports of classes related to advanced markers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: google_maps_flutter
description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
version: 2.17.0
version: 2.17.1

environment:
sdk: ^3.10.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
Expand Down Expand Up @@ -406,20 +407,134 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
CameraUpdate cameraUpdate,
CameraUpdateAnimationConfiguration configuration, {
required int mapId,
}) {
}) async {
var effective = cameraUpdate;
if (cameraUpdate is CameraUpdateNewLatLngBoundsWithEdgeInsets) {
final CameraPosition pos = await _computeBoundsWithEdgeInsets(
cameraUpdate.bounds,
cameraUpdate.padding,
mapId,
);
effective = CameraUpdate.newCameraPosition(pos);
}
return _hostApi(mapId).animateCamera(
_platformCameraUpdateFromCameraUpdate(cameraUpdate),
_platformCameraUpdateFromCameraUpdate(effective),
configuration.duration?.inMilliseconds,
);
}

@override
Future<void> moveCamera(CameraUpdate cameraUpdate, {required int mapId}) {
Future<void> moveCamera(
CameraUpdate cameraUpdate, {
required int mapId,
}) async {
var effective = cameraUpdate;
if (cameraUpdate is CameraUpdateNewLatLngBoundsWithEdgeInsets) {
final CameraPosition pos = await _computeBoundsWithEdgeInsets(
cameraUpdate.bounds,
cameraUpdate.padding,
mapId,
);
effective = CameraUpdate.newCameraPosition(pos);
}
return _hostApi(
mapId,
).moveCamera(_platformCameraUpdateFromCameraUpdate(cameraUpdate));
).moveCamera(_platformCameraUpdateFromCameraUpdate(effective));
}

static const double _kMercatorTileSize = 256.0;

/// Computes a [CameraPosition] that fits [bounds] within the map viewport
/// minus [padding].
///
/// Uses Web Mercator projection to calculate the zoom level and offset
/// center from the current map dimensions.
Future<CameraPosition> _computeBoundsWithEdgeInsets(
LatLngBounds bounds,
EdgeInsets padding,
int mapId,
) async {
final double currentZoom = await getZoomLevel(mapId: mapId);
final LatLngBounds region = await getVisibleRegion(mapId: mapId);
final double scale = pow(2.0, currentZoom).toDouble();

double regionLngSpan =
region.northeast.longitude - region.southwest.longitude;
if (regionLngSpan <= 0) {
regionLngSpan += 360;
}
final double mapWidthDp =
regionLngSpan / 360.0 * _kMercatorTileSize * scale;

final double regionNeY = _mercatorY(region.northeast.latitude);
final double regionSwY = _mercatorY(region.southwest.latitude);
final double mapHeightDp =
(regionSwY - regionNeY) * _kMercatorTileSize * scale;

final double availW = mapWidthDp - padding.left - padding.right;
final double availH = mapHeightDp - padding.top - padding.bottom;

final LatLng ne = bounds.northeast;
final LatLng sw = bounds.southwest;
double lngSpan = ne.longitude - sw.longitude;
if (lngSpan <= 0) {
lngSpan += 360;
}

final double neY = _mercatorY(ne.latitude);
final double swY = _mercatorY(sw.latitude);
final double latSpanMerc = (swY - neY).abs();

final double centerLat = (ne.latitude + sw.latitude) / 2;
double centerLng = (ne.longitude + sw.longitude) / 2;
if (ne.longitude < sw.longitude) {
centerLng += 180;
if (centerLng > 180) {
centerLng -= 360;
}
}

if (availW <= 0 || availH <= 0) {
return CameraPosition(
target: LatLng(centerLat, centerLng),
zoom: currentZoom,
);
}

final double zoomLng = _log2(
availW / (lngSpan / 360.0 * _kMercatorTileSize),
);
final double zoomLat = _log2(availH / (latSpanMerc * _kMercatorTileSize));
final double zoom = min(zoomLng, zoomLat);

final double targetScale = pow(2.0, zoom).toDouble();
final double lngPerDp = 360.0 / (_kMercatorTileSize * targetScale);
final double offsetLng = (padding.right - padding.left) / 2 * lngPerDp;

final double centerMercY = _mercatorY(centerLat);
final double centerPxY = centerMercY * _kMercatorTileSize * targetScale;
final double offsetPxY = centerPxY + (padding.bottom - padding.top) / 2;
final double offsetLat = _inverseMercatorY(
offsetPxY / (_kMercatorTileSize * targetScale),
);

return CameraPosition(
target: LatLng(offsetLat, centerLng + offsetLng),
zoom: zoom,
);
}

static double _mercatorY(double lat) {
final double latRad = lat * pi / 180;
return (1 - log(tan(latRad) + 1 / cos(latRad)) / pi) / 2;
}

static double _inverseMercatorY(double y) {
return (2 * atan(exp(pi * (1 - 2 * y))) - pi / 2) * 180 / pi;
}

static double _log2(double x) => log(x) / ln2;

@override
Future<void> setMapStyle(String? mapStyle, {required int mapId}) async {
final bool success = await _hostApi(mapId).setStyle(mapStyle ?? '');
Expand Down Expand Up @@ -1007,6 +1122,13 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
dy: update.dy,
),
);
case CameraUpdateType.newLatLngBoundsWithEdgeInsets:
// Requires async platform calls (getZoomLevel, getVisibleRegion) to
// compute the polyfill, so it is handled in moveCamera/animateCamera
// before reaching this static sync method.
throw StateError(
'newLatLngBoundsWithEdgeInsets should be unreachable here.',
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies:
flutter:
sdk: flutter
flutter_plugin_android_lifecycle: ^2.0.1
google_maps_flutter_platform_interface: ^2.13.0
google_maps_flutter_platform_interface: ^2.16.0
stream_transform: ^2.0.0

dev_dependencies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1673,4 +1673,113 @@ void main() {
PlatformMarkerType.marker,
);
});

group('newLatLngBoundsWithEdgeInsets polyfill', () {
void stubMapState(MockMapsApi api, {required double zoom}) {
when(api.getZoomLevel()).thenAnswer((_) async => zoom);
when(api.getVisibleRegion()).thenAnswer(
(_) async => PlatformLatLngBounds(
southwest: PlatformLatLng(latitude: -10, longitude: -20),
northeast: PlatformLatLng(latitude: 10, longitude: 20),
),
);
}

test(
'moveCamera with symmetric padding produces centered result',
() async {
const mapId = 1;
final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = setUpMockMap(
mapId: mapId,
);
stubMapState(api, zoom: 5);

final bounds = LatLngBounds(
southwest: const LatLng(-1, -1),
northeast: const LatLng(1, 1),
);
await maps.moveCamera(
CameraUpdate.newLatLngBoundsWithEdgeInsets(
bounds,
const EdgeInsets.all(50),
),
mapId: mapId,
);

final verification = verify(api.moveCamera(captureAny));
final passedUpdate = verification.captured[0] as PlatformCameraUpdate;
final pos =
passedUpdate.cameraUpdate as PlatformCameraUpdateNewCameraPosition;
expect(pos.cameraPosition.target.latitude, closeTo(0, 0.01));
expect(pos.cameraPosition.target.longitude, closeTo(0, 0.01));
expect(pos.cameraPosition.zoom, greaterThan(0));
},
);

test('moveCamera with bottom-heavy padding shifts center upward', () async {
const mapId = 1;
final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = setUpMockMap(
mapId: mapId,
);
stubMapState(api, zoom: 5);

final bounds = LatLngBounds(
southwest: const LatLng(-1, -1),
northeast: const LatLng(1, 1),
);
await maps.moveCamera(
CameraUpdate.newLatLngBoundsWithEdgeInsets(
bounds,
const EdgeInsets.only(bottom: 200),
),
mapId: mapId,
);

final verification = verify(api.moveCamera(captureAny));
final passedUpdate = verification.captured[0] as PlatformCameraUpdate;
final pos =
passedUpdate.cameraUpdate as PlatformCameraUpdateNewCameraPosition;
expect(
pos.cameraPosition.target.latitude,
lessThan(0),
reason:
'Bottom-heavy padding should shift the center south (negative latitude)',
);
expect(pos.cameraPosition.target.longitude, closeTo(0, 0.01));
});

test(
'moveCamera with right-heavy padding shifts center rightward',
() async {
const mapId = 1;
final (GoogleMapsFlutterAndroid maps, MockMapsApi api) = setUpMockMap(
mapId: mapId,
);
stubMapState(api, zoom: 5);

final bounds = LatLngBounds(
southwest: const LatLng(-1, -1),
northeast: const LatLng(1, 1),
);
await maps.moveCamera(
CameraUpdate.newLatLngBoundsWithEdgeInsets(
bounds,
const EdgeInsets.only(right: 200),
),
mapId: mapId,
);

final verification = verify(api.moveCamera(captureAny));
final passedUpdate = verification.captured[0] as PlatformCameraUpdate;
final pos =
passedUpdate.cameraUpdate as PlatformCameraUpdateNewCameraPosition;
expect(pos.cameraPosition.target.latitude, closeTo(0, 0.01));
expect(
pos.cameraPosition.target.longitude,
greaterThan(0),
reason: 'Right-heavy padding should shift the center eastward',
);
},
);
});
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.19.0

* Adds support for `CameraUpdate.newLatLngBoundsWithEdgeInsets`.

## 2.18.1

* Removes conditional header logic that broke add-to-app builds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,14 @@ GMSCollisionBehavior FGMGetCollisionBehaviorForPigeonCollisionBehavior(
return
[GMSCameraUpdate fitBounds:FGMGetCoordinateBoundsForPigeonLatLngBounds(typedUpdate.bounds)
withPadding:typedUpdate.padding];
} else if ([update isKindOfClass:[FGMPlatformCameraUpdateNewLatLngBoundsWithEdgeInsets class]]) {
FGMPlatformCameraUpdateNewLatLngBoundsWithEdgeInsets *typedUpdate =
(FGMPlatformCameraUpdateNewLatLngBoundsWithEdgeInsets *)update;
FGMPlatformEdgeInsets *padding = typedUpdate.padding;
return
[GMSCameraUpdate fitBounds:FGMGetCoordinateBoundsForPigeonLatLngBounds(typedUpdate.bounds)
withEdgeInsets:UIEdgeInsetsMake(padding.top, padding.left,
padding.bottom, padding.right)];
} else if ([update isKindOfClass:[FGMPlatformCameraUpdateNewLatLngZoom class]]) {
FGMPlatformCameraUpdateNewLatLngZoom *typedUpdate =
(FGMPlatformCameraUpdateNewLatLngZoom *)update;
Expand Down
Loading