blob: 15ea6182d7b2844ac33f6c0d6bc11ad283f55a70 [file] [log] [blame]
// 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.
part of google_maps_flutter;
/// Callback method for when the map is ready to be used.
///
/// Pass to [GoogleMap.onMapCreated] to receive a [GoogleMapController] when the
/// map is created.
typedef MapCreatedCallback = void Function(GoogleMapController controller);
// This counter is used to provide a stable "constant" initialization id
// to the buildView function, so the web implementation can use it as a
// cache key. This needs to be provided from the outside, because web
// views seem to re-render much more often that mobile platform views.
int _nextMapCreationId = 0;
/// Error thrown when an unknown map object ID is provided to a method.
class UnknownMapObjectIdError extends Error {
/// Creates an assertion error with the provided [message].
UnknownMapObjectIdError(this.objectType, this.objectId, [this.context]);
/// The name of the map object whose ID is unknown.
final String objectType;
/// The unknown maps object ID.
final MapsObjectId<Object> objectId;
/// The context where the error occurred.
final String? context;
@override
String toString() {
if (context != null) {
return 'Unknown $objectType ID "${objectId.value}" in $context';
}
return 'Unknown $objectType ID "${objectId.value}"';
}
}
/// Android specific settings for [GoogleMap].
@Deprecated(
'See https://pub.dev/packages/google_maps_flutter_android#display-mode')
class AndroidGoogleMapsFlutter {
@Deprecated(
'See https://pub.dev/packages/google_maps_flutter_android#display-mode')
AndroidGoogleMapsFlutter._();
/// Whether to render [GoogleMap] with a [AndroidViewSurface] to build the Google Maps widget.
///
/// This implementation uses hybrid composition to render the Google Maps
/// Widget on Android. This comes at the cost of some performance on Android
/// versions below 10. See
/// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more
/// information.
@Deprecated(
'See https://pub.dev/packages/google_maps_flutter_android#display-mode')
static bool get useAndroidViewSurface {
final GoogleMapsFlutterPlatform platform =
GoogleMapsFlutterPlatform.instance;
if (platform is GoogleMapsFlutterAndroid) {
return platform.useAndroidViewSurface;
}
return false;
}
/// Set whether to render [GoogleMap] with a [AndroidViewSurface] to build the Google Maps widget.
///
/// This implementation uses hybrid composition to render the Google Maps
/// Widget on Android. This comes at the cost of some performance on Android
/// versions below 10. See
/// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more
/// information.
@Deprecated(
'See https://pub.dev/packages/google_maps_flutter_android#display-mode')
static set useAndroidViewSurface(bool useAndroidViewSurface) {
final GoogleMapsFlutterPlatform platform =
GoogleMapsFlutterPlatform.instance;
if (platform is GoogleMapsFlutterAndroid) {
platform.useAndroidViewSurface = useAndroidViewSurface;
}
}
}
/// A widget which displays a map with data obtained from the Google Maps service.
class GoogleMap extends StatefulWidget {
/// Creates a widget displaying data from Google Maps services.
///
/// [AssertionError] will be thrown if [initialCameraPosition] is null;
const GoogleMap({
super.key,
required this.initialCameraPosition,
this.onMapCreated,
this.gestureRecognizers = const <Factory<OneSequenceGestureRecognizer>>{},
this.compassEnabled = true,
this.mapToolbarEnabled = true,
this.cameraTargetBounds = CameraTargetBounds.unbounded,
this.mapType = MapType.normal,
this.minMaxZoomPreference = MinMaxZoomPreference.unbounded,
this.rotateGesturesEnabled = true,
this.scrollGesturesEnabled = true,
this.zoomControlsEnabled = true,
this.zoomGesturesEnabled = true,
this.liteModeEnabled = false,
this.tiltGesturesEnabled = true,
this.myLocationEnabled = false,
this.myLocationButtonEnabled = true,
this.layoutDirection,
/// If no padding is specified default padding will be 0.
this.padding = EdgeInsets.zero,
this.indoorViewEnabled = false,
this.trafficEnabled = false,
this.buildingsEnabled = true,
this.markers = const <Marker>{},
this.polygons = const <Polygon>{},
this.polylines = const <Polyline>{},
this.circles = const <Circle>{},
this.onCameraMoveStarted,
this.tileOverlays = const <TileOverlay>{},
this.onCameraMove,
this.onCameraIdle,
this.onTap,
this.onLongPress,
}) : assert(initialCameraPosition != null);
/// Callback method for when the map is ready to be used.
///
/// Used to receive a [GoogleMapController] for this [GoogleMap].
final MapCreatedCallback? onMapCreated;
/// The initial position of the map's camera.
final CameraPosition initialCameraPosition;
/// True if the map should show a compass when rotated.
final bool compassEnabled;
/// True if the map should show a toolbar when you interact with the map. Android only.
final bool mapToolbarEnabled;
/// Geographical bounding box for the camera target.
final CameraTargetBounds cameraTargetBounds;
/// Type of map tiles to be rendered.
final MapType mapType;
/// The layout direction to use for the embedded view.
///
/// If this is null, the ambient [Directionality] is used instead. If there is
/// no ambient [Directionality], [TextDirection.ltr] is used.
final TextDirection? layoutDirection;
/// Preferred bounds for the camera zoom level.
///
/// Actual bounds depend on map data and device.
final MinMaxZoomPreference minMaxZoomPreference;
/// True if the map view should respond to rotate gestures.
final bool rotateGesturesEnabled;
/// True if the map view should respond to scroll gestures.
final bool scrollGesturesEnabled;
/// True if the map view should show zoom controls. This includes two buttons
/// to zoom in and zoom out. The default value is to show zoom controls.
///
/// This is only supported on Android. And this field is silently ignored on iOS.
final bool zoomControlsEnabled;
/// True if the map view should respond to zoom gestures.
final bool zoomGesturesEnabled;
/// True if the map view should be in lite mode. Android only.
///
/// See https://developers.google.com/maps/documentation/android-sdk/lite#overview_of_lite_mode for more details.
final bool liteModeEnabled;
/// True if the map view should respond to tilt gestures.
final bool tiltGesturesEnabled;
/// Padding to be set on map. See https://developers.google.com/maps/documentation/android-sdk/map#map_padding for more details.
final EdgeInsets padding;
/// Markers to be placed on the map.
final Set<Marker> markers;
/// Polygons to be placed on the map.
final Set<Polygon> polygons;
/// Polylines to be placed on the map.
final Set<Polyline> polylines;
/// Circles to be placed on the map.
final Set<Circle> circles;
/// Tile overlays to be placed on the map.
final Set<TileOverlay> tileOverlays;
/// Called when the camera starts moving.
///
/// This can be initiated by the following:
/// 1. Non-gesture animation initiated in response to user actions.
/// For example: zoom buttons, my location button, or marker clicks.
/// 2. Programmatically initiated animation.
/// 3. Camera motion initiated in response to user gestures on the map.
/// For example: pan, tilt, pinch to zoom, or rotate.
final VoidCallback? onCameraMoveStarted;
/// Called repeatedly as the camera continues to move after an
/// onCameraMoveStarted call.
///
/// This may be called as often as once every frame and should
/// not perform expensive operations.
final CameraPositionCallback? onCameraMove;
/// Called when camera movement has ended, there are no pending
/// animations and the user has stopped interacting with the map.
final VoidCallback? onCameraIdle;
/// Called every time a [GoogleMap] is tapped.
final ArgumentCallback<LatLng>? onTap;
/// Called every time a [GoogleMap] is long pressed.
final ArgumentCallback<LatLng>? onLongPress;
/// True if a "My Location" layer should be shown on the map.
///
/// This layer includes a location indicator at the current device location,
/// as well as a My Location button.
/// * The indicator is a small blue dot if the device is stationary, or a
/// chevron if the device is moving.
/// * The My Location button animates to focus on the user's current location
/// if the user's location is currently known.
///
/// Enabling this feature requires adding location permissions to both native
/// platforms of your app.
/// * On Android add either
/// `<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />`
/// or `<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />`
/// to your `AndroidManifest.xml` file. `ACCESS_COARSE_LOCATION` returns a
/// location with an accuracy approximately equivalent to a city block, while
/// `ACCESS_FINE_LOCATION` returns as precise a location as possible, although
/// it consumes more battery power. You will also need to request these
/// permissions during run-time. If they are not granted, the My Location
/// feature will fail silently.
/// * On iOS add a `NSLocationWhenInUseUsageDescription` key to your
/// `Info.plist` file. This will automatically prompt the user for permissions
/// when the map tries to turn on the My Location layer.
final bool myLocationEnabled;
/// Enables or disables the my-location button.
///
/// The my-location button causes the camera to move such that the user's
/// location is in the center of the map. If the button is enabled, it is
/// only shown when the my-location layer is enabled.
///
/// By default, the my-location button is enabled (and hence shown when the
/// my-location layer is enabled).
///
/// See also:
/// * [myLocationEnabled] parameter.
final bool myLocationButtonEnabled;
/// Enables or disables the indoor view from the map
final bool indoorViewEnabled;
/// Enables or disables the traffic layer of the map
final bool trafficEnabled;
/// Enables or disables showing 3D buildings where available
final bool buildingsEnabled;
/// Which gestures should be consumed by the map.
///
/// It is possible for other gesture recognizers to be competing with the map on pointer
/// events, e.g if the map is inside a [ListView] the [ListView] will want to handle
/// vertical drags. The map will claim gestures that are recognized by any of the
/// recognizers on this list.
///
/// When this set is empty, the map will only handle pointer events for gestures that
/// were not claimed by any other gesture recognizer.
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
/// Creates a [State] for this [GoogleMap].
@override
State createState() => _GoogleMapState();
}
class _GoogleMapState extends State<GoogleMap> {
final int _mapId = _nextMapCreationId++;
final Completer<GoogleMapController> _controller =
Completer<GoogleMapController>();
Map<MarkerId, Marker> _markers = <MarkerId, Marker>{};
Map<PolygonId, Polygon> _polygons = <PolygonId, Polygon>{};
Map<PolylineId, Polyline> _polylines = <PolylineId, Polyline>{};
Map<CircleId, Circle> _circles = <CircleId, Circle>{};
late MapConfiguration _mapConfiguration;
@override
Widget build(BuildContext context) {
return GoogleMapsFlutterPlatform.instance.buildViewWithConfiguration(
_mapId,
onPlatformViewCreated,
widgetConfiguration: MapWidgetConfiguration(
textDirection: widget.layoutDirection ??
Directionality.maybeOf(context) ??
TextDirection.ltr,
initialCameraPosition: widget.initialCameraPosition,
gestureRecognizers: widget.gestureRecognizers,
),
mapObjects: MapObjects(
markers: widget.markers,
polygons: widget.polygons,
polylines: widget.polylines,
circles: widget.circles,
),
mapConfiguration: _mapConfiguration,
);
}
@override
void initState() {
super.initState();
_mapConfiguration = _configurationFromMapWidget(widget);
_markers = keyByMarkerId(widget.markers);
_polygons = keyByPolygonId(widget.polygons);
_polylines = keyByPolylineId(widget.polylines);
_circles = keyByCircleId(widget.circles);
}
@override
void dispose() {
_disposeController();
super.dispose();
}
Future<void> _disposeController() async {
final GoogleMapController controller = await _controller.future;
controller.dispose();
}
@override
void didUpdateWidget(GoogleMap oldWidget) {
super.didUpdateWidget(oldWidget);
_updateOptions();
_updateMarkers();
_updatePolygons();
_updatePolylines();
_updateCircles();
_updateTileOverlays();
}
Future<void> _updateOptions() async {
final MapConfiguration newConfig = _configurationFromMapWidget(widget);
final MapConfiguration updates = newConfig.diffFrom(_mapConfiguration);
if (updates.isEmpty) {
return;
}
final GoogleMapController controller = await _controller.future;
// ignore: unawaited_futures
controller._updateMapConfiguration(updates);
_mapConfiguration = newConfig;
}
Future<void> _updateMarkers() async {
final GoogleMapController controller = await _controller.future;
// ignore: unawaited_futures
controller._updateMarkers(
MarkerUpdates.from(_markers.values.toSet(), widget.markers));
_markers = keyByMarkerId(widget.markers);
}
Future<void> _updatePolygons() async {
final GoogleMapController controller = await _controller.future;
// ignore: unawaited_futures
controller._updatePolygons(
PolygonUpdates.from(_polygons.values.toSet(), widget.polygons));
_polygons = keyByPolygonId(widget.polygons);
}
Future<void> _updatePolylines() async {
final GoogleMapController controller = await _controller.future;
// ignore: unawaited_futures
controller._updatePolylines(
PolylineUpdates.from(_polylines.values.toSet(), widget.polylines));
_polylines = keyByPolylineId(widget.polylines);
}
Future<void> _updateCircles() async {
final GoogleMapController controller = await _controller.future;
// ignore: unawaited_futures
controller._updateCircles(
CircleUpdates.from(_circles.values.toSet(), widget.circles));
_circles = keyByCircleId(widget.circles);
}
Future<void> _updateTileOverlays() async {
final GoogleMapController controller = await _controller.future;
// ignore: unawaited_futures
controller._updateTileOverlays(widget.tileOverlays);
}
Future<void> onPlatformViewCreated(int id) async {
final GoogleMapController controller = await GoogleMapController.init(
id,
widget.initialCameraPosition,
this,
);
_controller.complete(controller);
_updateTileOverlays();
final MapCreatedCallback? onMapCreated = widget.onMapCreated;
if (onMapCreated != null) {
onMapCreated(controller);
}
}
void onMarkerTap(MarkerId markerId) {
assert(markerId != null);
final Marker? marker = _markers[markerId];
if (marker == null) {
throw UnknownMapObjectIdError('marker', markerId, 'onTap');
}
final VoidCallback? onTap = marker.onTap;
if (onTap != null) {
onTap();
}
}
void onMarkerDragStart(MarkerId markerId, LatLng position) {
assert(markerId != null);
final Marker? marker = _markers[markerId];
if (marker == null) {
throw UnknownMapObjectIdError('marker', markerId, 'onDragStart');
}
final ValueChanged<LatLng>? onDragStart = marker.onDragStart;
if (onDragStart != null) {
onDragStart(position);
}
}
void onMarkerDrag(MarkerId markerId, LatLng position) {
assert(markerId != null);
final Marker? marker = _markers[markerId];
if (marker == null) {
throw UnknownMapObjectIdError('marker', markerId, 'onDrag');
}
final ValueChanged<LatLng>? onDrag = marker.onDrag;
if (onDrag != null) {
onDrag(position);
}
}
void onMarkerDragEnd(MarkerId markerId, LatLng position) {
assert(markerId != null);
final Marker? marker = _markers[markerId];
if (marker == null) {
throw UnknownMapObjectIdError('marker', markerId, 'onDragEnd');
}
final ValueChanged<LatLng>? onDragEnd = marker.onDragEnd;
if (onDragEnd != null) {
onDragEnd(position);
}
}
void onPolygonTap(PolygonId polygonId) {
assert(polygonId != null);
final Polygon? polygon = _polygons[polygonId];
if (polygon == null) {
throw UnknownMapObjectIdError('polygon', polygonId, 'onTap');
}
final VoidCallback? onTap = polygon.onTap;
if (onTap != null) {
onTap();
}
}
void onPolylineTap(PolylineId polylineId) {
assert(polylineId != null);
final Polyline? polyline = _polylines[polylineId];
if (polyline == null) {
throw UnknownMapObjectIdError('polyline', polylineId, 'onTap');
}
final VoidCallback? onTap = polyline.onTap;
if (onTap != null) {
onTap();
}
}
void onCircleTap(CircleId circleId) {
assert(circleId != null);
final Circle? circle = _circles[circleId];
if (circle == null) {
throw UnknownMapObjectIdError('marker', circleId, 'onTap');
}
final VoidCallback? onTap = circle.onTap;
if (onTap != null) {
onTap();
}
}
void onInfoWindowTap(MarkerId markerId) {
assert(markerId != null);
final Marker? marker = _markers[markerId];
if (marker == null) {
throw UnknownMapObjectIdError('marker', markerId, 'InfoWindow onTap');
}
final VoidCallback? onTap = marker.infoWindow.onTap;
if (onTap != null) {
onTap();
}
}
void onTap(LatLng position) {
assert(position != null);
final ArgumentCallback<LatLng>? onTap = widget.onTap;
if (onTap != null) {
onTap(position);
}
}
void onLongPress(LatLng position) {
assert(position != null);
final ArgumentCallback<LatLng>? onLongPress = widget.onLongPress;
if (onLongPress != null) {
onLongPress(position);
}
}
}
/// Builds a [MapConfiguration] from the given [map].
MapConfiguration _configurationFromMapWidget(GoogleMap map) {
assert(!map.liteModeEnabled || Platform.isAndroid);
return MapConfiguration(
compassEnabled: map.compassEnabled,
mapToolbarEnabled: map.mapToolbarEnabled,
cameraTargetBounds: map.cameraTargetBounds,
mapType: map.mapType,
minMaxZoomPreference: map.minMaxZoomPreference,
rotateGesturesEnabled: map.rotateGesturesEnabled,
scrollGesturesEnabled: map.scrollGesturesEnabled,
tiltGesturesEnabled: map.tiltGesturesEnabled,
trackCameraPosition: map.onCameraMove != null,
zoomControlsEnabled: map.zoomControlsEnabled,
zoomGesturesEnabled: map.zoomGesturesEnabled,
liteModeEnabled: map.liteModeEnabled,
myLocationEnabled: map.myLocationEnabled,
myLocationButtonEnabled: map.myLocationButtonEnabled,
padding: map.padding,
indoorViewEnabled: map.indoorViewEnabled,
trafficEnabled: map.trafficEnabled,
buildingsEnabled: map.buildingsEnabled,
);
}