API documentation and cleanup (#545)
diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index 0302726..7b9b5f7 100644 --- a/packages/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/README.md
@@ -2,13 +2,45 @@ [](https://pub.dartlang.org/packages/google_maps_flutter) -A Flutter plugin to use [Google Maps](https://developers.google.com/maps/) for iOS and Android. +A Flutter plugin to use [Google Maps](https://developers.google.com/maps/) in +iOS and Android apps. -*Note*: This plugin is currently a stub and under active development. +## Caveat + +This plugin provides an *unpublished preview* of the Flutter API for GoogleMaps: +* Dart APIs for controlling and interacting with a GoogleMap view from Flutter + code are still being consolidated and expanded. The intention is to grow + current coverage into a complete offering. Issues and pull requests aimed to + help us prioritize and speed up this effort are very welcome. +* The technique currently used for compositing GoogleMap views with Flutter + widgets is *inherently limited* and will be replaced by a fully compositional + [Texture](https://docs.flutter.io/flutter/widgets/Texture-class.html)-based + approach before we publish this plugin. + + In detail: the plugin currently relies on placing platform overlays on top of + a bitmap snapshotting widget for creating the illusion of in-line compositing + of GoogleMap views with Flutter widgets. This works only in very limited + situations where + * the widget is stationary + * the widget is drawn on top of all other widgets within bounds + * touch events within widget bounds can be safely ignored by Flutter + + The root problem with platform overlays is that they cannot be freely composed + with other widgets. Many workarounds can be devised to address this shortcoming + in particular situations, but the Flutter team does not intend to support such + work, as it would not move us forward towards our goal of a fully compositional + GoogleMaps widget. ## Usage -To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add +```yaml + google_maps_flutter: + git: + url: git://github.com/flutter/plugins + path: packages/google_maps_flutter +``` +as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). ## Getting Started
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 0ee9d40..de2023e 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java
@@ -104,6 +104,9 @@ } static Object toJson(CameraPosition position) { + if (position == null) { + return null; + } final Map<String, Object> data = new HashMap<>(); data.put("bearing", position.bearing); data.put("target", toJson(position.target));
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 13a1862..ef905c3 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -163,6 +163,10 @@ googleMap.animateCamera(cameraUpdate); } + CameraPosition getCameraPosition() { + return trackCameraPosition ? googleMap.getCameraPosition() : null; + } + MarkerBuilder newMarkerBuilder() { return new MarkerBuilder(this); }
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java index 692a820..5805aab 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java
@@ -65,7 +65,7 @@ result.success(null); break; } - case "createMap": + case "map#create": { final int width = Convert.toPixels(call.argument("width"), density); final int height = Convert.toPixels(call.argument("height"), density); @@ -82,7 +82,7 @@ final Map<String, Object> arguments = new HashMap<>(2); arguments.put("map", controller.id()); arguments.put("isGesture", isGesture); - channel.invokeMethod("map#onCameraMoveStarted", arguments); + channel.invokeMethod("camera#onMoveStarted", arguments); } @Override @@ -90,13 +90,13 @@ final Map<String, Object> arguments = new HashMap<>(2); arguments.put("map", controller.id()); arguments.put("position", Convert.toJson(position)); - channel.invokeMethod("map#onCameraMove", arguments); + channel.invokeMethod("camera#onMove", arguments); } @Override public void onCameraIdle() { channel.invokeMethod( - "map#onCameraIdle", Collections.singletonMap("map", controller.id())); + "camera#onIdle", Collections.singletonMap("map", controller.id())); } }); controller.setOnMarkerTappedListener( @@ -113,14 +113,14 @@ // is ready break; } - case "updateMapOptions": + case "map#update": { final GoogleMapController controller = mapsController(call); Convert.interpretGoogleMapOptions(call.argument("options"), controller); - result.success(null); + result.success(Convert.toJson(controller.getCameraPosition())); break; } - case "moveCamera": + case "camera#move": { final GoogleMapController controller = mapsController(call); final CameraUpdate cameraUpdate = @@ -129,7 +129,7 @@ result.success(null); break; } - case "animateCamera": + case "camera#animate": { final GoogleMapController controller = mapsController(call); final CameraUpdate cameraUpdate = @@ -138,7 +138,7 @@ result.success(null); break; } - case "addMarker": + case "marker#add": { final GoogleMapController controller = mapsController(call); final MarkerBuilder markerBuilder = controller.newMarkerBuilder(); @@ -164,7 +164,7 @@ result.success(null); break; } - case "showMapOverlay": + case "map#show": { final GoogleMapController controller = mapsController(call); final int x = Convert.toPixels(call.argument("x"), density); @@ -173,14 +173,14 @@ result.success(null); break; } - case "hideMapOverlay": + case "map#hide": { final GoogleMapController controller = mapsController(call); controller.hideOverlay(); result.success(null); break; } - case "disposeMap": + case "map#dispose": { final GoogleMapController controller = mapsController(call); controller.dispose();
diff --git a/packages/google_maps_flutter/example/lib/animate_camera.dart b/packages/google_maps_flutter/example/lib/animate_camera.dart index 25a7769..0a7bfe1 100644 --- a/packages/google_maps_flutter/example/lib/animate_camera.dart +++ b/packages/google_maps_flutter/example/lib/animate_camera.dart
@@ -9,31 +9,31 @@ class AnimateCameraPage extends Page { AnimateCameraPage() - : super(const Icon(Icons.map), "Camera control, animated"); + : super(const Icon(Icons.map), 'Camera control, animated'); @override final GoogleMapOverlayController controller = - new GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0); + GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0); @override Widget build(BuildContext context) { - return new Column( + return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - new Center(child: new GoogleMapOverlay(controller: controller)), - new Row( + Center(child: GoogleMapOverlay(controller: controller)), + Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ - new Column( + Column( children: <Widget>[ - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, - target: const LatLng(51.5160895, -0.1294527), + target: LatLng(51.5160895, -0.1294527), tilt: 30.0, zoom: 17.0, ), @@ -42,7 +42,7 @@ }, child: const Text('newCameraPosition'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.newLatLng( @@ -52,11 +52,11 @@ }, child: const Text('newLatLng'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.newLatLngBounds( - const LatLngBounds( + LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), northeast: const LatLng(-8.982446, 153.823821), ), @@ -66,7 +66,7 @@ }, child: const Text('newLatLngBounds'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.newLatLngZoom( @@ -77,7 +77,7 @@ }, child: const Text('newLatLngZoom'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.scrollBy(150.0, -225.0), @@ -87,9 +87,9 @@ ), ], ), - new Column( + Column( children: <Widget>[ - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.zoomBy( @@ -100,7 +100,7 @@ }, child: const Text('zoomBy with focus'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.zoomBy(-0.5), @@ -108,7 +108,7 @@ }, child: const Text('zoomBy'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.zoomIn(), @@ -116,7 +116,7 @@ }, child: const Text('zoomIn'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.zoomOut(), @@ -124,7 +124,7 @@ }, child: const Text('zoomOut'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.animateCamera( CameraUpdate.zoomTo(16.0),
diff --git a/packages/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/example/lib/main.dart index de1affa..e8161b6 100644 --- a/packages/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/example/lib/main.dart
@@ -11,30 +11,30 @@ import 'place_marker.dart'; final List<Page> _allPages = <Page>[ - new MapUiPage(), - new AnimateCameraPage(), - new MoveCameraPage(), - new PlaceMarkerPage(), + MapUiPage(), + AnimateCameraPage(), + MoveCameraPage(), + PlaceMarkerPage(), ]; class MapsDemo extends StatelessWidget { void _pushPage(BuildContext context, Page page) { - Navigator.of(context).push(new MaterialPageRoute<void>( - builder: (_) => new Scaffold( - appBar: new AppBar(title: new Text(page.title)), + Navigator.of(context).push(MaterialPageRoute<void>( + builder: (_) => Scaffold( + appBar: AppBar(title: Text(page.title)), body: page, ))); } @override Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar(title: const Text('GoogleMaps examples')), - body: new ListView.builder( + return Scaffold( + appBar: AppBar(title: const Text('GoogleMaps examples')), + body: ListView.builder( itemCount: _allPages.length, - itemBuilder: (_, int index) => new ListTile( + itemBuilder: (_, int index) => ListTile( leading: _allPages[index].leading, - title: new Text(_allPages[index].title), + title: Text(_allPages[index].title), onTap: () => _pushPage(context, _allPages[index]), ), ), @@ -48,5 +48,5 @@ for (Page p in _allPages) { observers.add(p.controller.overlayController); } - runApp(new MaterialApp(home: new MapsDemo(), navigatorObservers: observers)); + runApp(MaterialApp(home: MapsDemo(), navigatorObservers: observers)); }
diff --git a/packages/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/example/lib/map_ui.dart index b362e32..3c11273 100644 --- a/packages/google_maps_flutter/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/example/lib/map_ui.dart
@@ -7,22 +7,22 @@ import 'page.dart'; -const LatLngBounds sydneyBounds = const LatLngBounds( +final LatLngBounds sydneyBounds = LatLngBounds( southwest: const LatLng(-34.022631, 150.620685), northeast: const LatLng(-33.571835, 151.325952), ); class MapUiPage extends Page { - MapUiPage() : super(const Icon(Icons.map), "User interface"); + MapUiPage() : super(const Icon(Icons.map), 'User interface'); @override final GoogleMapOverlayController controller = - new GoogleMapOverlayController.fromSize( + GoogleMapOverlayController.fromSize( width: 300.0, height: 200.0, - options: const GoogleMapOptions( + options: GoogleMapOptions( cameraPosition: const CameraPosition( - target: const LatLng(-33.852, 151.211), + target: LatLng(-33.852, 151.211), zoom: 11.0, ), trackCameraPosition: true, @@ -31,7 +31,7 @@ @override Widget build(BuildContext context) { - return new MapUiBody(controller); + return MapUiBody(controller); } } @@ -41,7 +41,7 @@ const MapUiBody(this.controller); @override - State<StatefulWidget> createState() => new MapUiBodyState(); + State<StatefulWidget> createState() => MapUiBodyState(); } class MapUiBodyState extends State<MapUiBody> { @@ -66,29 +66,28 @@ } Widget _compassToggler() { - return new FlatButton( - child: - new Text('${_options.compassEnabled ? 'disable' : 'enable'} compass'), + return FlatButton( + child: Text('${_options.compassEnabled ? 'disable' : 'enable'} compass'), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions(compassEnabled: !_options.compassEnabled), + GoogleMapOptions(compassEnabled: !_options.compassEnabled), ); }, ); } Widget _latLngBoundsToggler() { - return new FlatButton( - child: new Text( + return FlatButton( + child: Text( _options.cameraTargetBounds.bounds == null ? 'bound camera target' : 'release camera target', ), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions( + GoogleMapOptions( cameraTargetBounds: _options.cameraTargetBounds.bounds == null - ? const CameraTargetBounds(sydneyBounds) + ? CameraTargetBounds(sydneyBounds) : CameraTargetBounds.unbounded, ), ); @@ -97,13 +96,13 @@ } Widget _zoomBoundsToggler() { - return new FlatButton( - child: new Text(_options.minMaxZoomPreference.minZoom == null + return FlatButton( + child: Text(_options.minMaxZoomPreference.minZoom == null ? 'bound zoom' : 'release zoom'), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions( + GoogleMapOptions( minMaxZoomPreference: _options.minMaxZoomPreference.minZoom == null ? const MinMaxZoomPreference(12.0, 16.0) : MinMaxZoomPreference.unbounded, @@ -116,23 +115,23 @@ Widget _mapTypeCycler() { final MapType nextType = MapType.values[(_options.mapType.index + 1) % MapType.values.length]; - return new FlatButton( - child: new Text('change map type to $nextType'), + return FlatButton( + child: Text('change map type to $nextType'), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions(mapType: nextType), + GoogleMapOptions(mapType: nextType), ); }, ); } Widget _rotateToggler() { - return new FlatButton( - child: new Text( + return FlatButton( + child: Text( '${_options.rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions( + GoogleMapOptions( rotateGesturesEnabled: !_options.rotateGesturesEnabled, ), ); @@ -141,12 +140,12 @@ } Widget _scrollToggler() { - return new FlatButton( - child: new Text( + return FlatButton( + child: Text( '${_options.scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions( + GoogleMapOptions( scrollGesturesEnabled: !_options.scrollGesturesEnabled, ), ); @@ -155,12 +154,12 @@ } Widget _tiltToggler() { - return new FlatButton( - child: new Text( - '${_options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), + return FlatButton( + child: + Text('${_options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions( + GoogleMapOptions( tiltGesturesEnabled: !_options.tiltGesturesEnabled, ), ); @@ -169,12 +168,12 @@ } Widget _zoomToggler() { - return new FlatButton( - child: new Text( - '${_options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), + return FlatButton( + child: + Text('${_options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), onPressed: () { widget.controller.mapController.updateMapOptions( - new GoogleMapOptions( + GoogleMapOptions( zoomGesturesEnabled: !_options.zoomGesturesEnabled, ), ); @@ -184,25 +183,25 @@ @override Widget build(BuildContext context) { - return new Column( + return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - new Padding( + Padding( padding: const EdgeInsets.all(10.0), - child: new Center( - child: new GoogleMapOverlay(controller: widget.controller), + child: Center( + child: GoogleMapOverlay(controller: widget.controller), ), ), - new Column( + Column( children: <Widget>[ - new Text('camera bearing: ${_position.bearing}'), - new Text( + Text('camera bearing: ${_position.bearing}'), + Text( 'camera target: ${_position.target.latitude.toStringAsFixed(4)},' '${_position.target.longitude.toStringAsFixed(4)}'), - new Text('camera zoom: ${_position.zoom}'), - new Text('camera tilt: ${_position.tilt}'), - new Text(_isMoving ? '(Camera moving)' : '(Camera idle)'), + Text('camera zoom: ${_position.zoom}'), + Text('camera tilt: ${_position.tilt}'), + Text(_isMoving ? '(Camera moving)' : '(Camera idle)'), _compassToggler(), _latLngBoundsToggler(), _mapTypeCycler(),
diff --git a/packages/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/example/lib/move_camera.dart index 2675d6b..8160495 100644 --- a/packages/google_maps_flutter/example/lib/move_camera.dart +++ b/packages/google_maps_flutter/example/lib/move_camera.dart
@@ -8,31 +8,31 @@ import 'page.dart'; class MoveCameraPage extends Page { - MoveCameraPage() : super(const Icon(Icons.map), "Camera control"); + MoveCameraPage() : super(const Icon(Icons.map), 'Camera control'); @override final GoogleMapOverlayController controller = - new GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0); + GoogleMapOverlayController.fromSize(width: 300.0, height: 200.0); @override Widget build(BuildContext context) { - return new Column( + return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - new Center(child: new GoogleMapOverlay(controller: controller)), - new Row( + Center(child: GoogleMapOverlay(controller: controller)), + Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ - new Column( + Column( children: <Widget>[ - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, - target: const LatLng(51.5160895, -0.1294527), + target: LatLng(51.5160895, -0.1294527), tilt: 30.0, zoom: 17.0, ), @@ -41,7 +41,7 @@ }, child: const Text('newCameraPosition'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.newLatLng( @@ -51,11 +51,11 @@ }, child: const Text('newLatLng'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.newLatLngBounds( - const LatLngBounds( + LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), northeast: const LatLng(-8.982446, 153.823821), ), @@ -65,7 +65,7 @@ }, child: const Text('newLatLngBounds'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.newLatLngZoom( @@ -76,7 +76,7 @@ }, child: const Text('newLatLngZoom'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.scrollBy(150.0, -225.0), @@ -86,9 +86,9 @@ ), ], ), - new Column( + Column( children: <Widget>[ - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.zoomBy( @@ -99,7 +99,7 @@ }, child: const Text('zoomBy with focus'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.zoomBy(-0.5), @@ -107,7 +107,7 @@ }, child: const Text('zoomBy'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.zoomIn(), @@ -115,7 +115,7 @@ }, child: const Text('zoomIn'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.zoomOut(), @@ -123,7 +123,7 @@ }, child: const Text('zoomOut'), ), - new FlatButton( + FlatButton( onPressed: () { controller.mapController.moveCamera( CameraUpdate.zoomTo(16.0),
diff --git a/packages/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/example/lib/place_marker.dart index b2fe8c6..5b0a3eb 100644 --- a/packages/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/example/lib/place_marker.dart
@@ -10,16 +10,16 @@ import 'page.dart'; class PlaceMarkerPage extends Page { - PlaceMarkerPage() : super(const Icon(Icons.place), "Place marker"); + PlaceMarkerPage() : super(const Icon(Icons.place), 'Place marker'); @override final GoogleMapOverlayController controller = - new GoogleMapOverlayController.fromSize( + GoogleMapOverlayController.fromSize( width: 300.0, height: 200.0, - options: const GoogleMapOptions( + options: GoogleMapOptions( cameraPosition: const CameraPosition( - target: const LatLng(-33.852, 151.211), + target: LatLng(-33.852, 151.211), zoom: 11.0, ), ), @@ -27,7 +27,7 @@ @override Widget build(BuildContext context) { - return new PlaceMarkerBody(controller); + return PlaceMarkerBody(controller); } } @@ -37,39 +37,53 @@ const PlaceMarkerBody(this.controller); @override - State<StatefulWidget> createState() => new PlaceMarkerBodyState(); + State<StatefulWidget> createState() { + return PlaceMarkerBodyState(controller.mapController); + } } class PlaceMarkerBodyState extends State<PlaceMarkerBody> { - static const LatLng center = const LatLng(-33.86711, 151.1947171); + static final LatLng center = const LatLng(-33.86711, 151.1947171); + PlaceMarkerBodyState(this.controller); + + final GoogleMapController controller; int _markerCount = 0; Marker _selectedMarker; @override void initState() { super.initState(); - widget.controller.mapController.onMarkerTapped.add((Marker marker) { + controller.onMarkerTapped.add((Marker marker) { if (_selectedMarker != null) { - _selectedMarker - .update(const MarkerOptions(icon: BitmapDescriptor.defaultMarker)); + _updateSelectedMarker( + const MarkerOptions(icon: BitmapDescriptor.defaultMarker), + ); } setState(() { _selectedMarker = marker; }); - _selectedMarker.update(new MarkerOptions( - icon: BitmapDescriptor - .defaultMarkerWithHue(BitmapDescriptor.hueGreen))); + _updateSelectedMarker( + MarkerOptions( + icon: BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueGreen, + ), + ), + ); }); } + void _updateSelectedMarker(MarkerOptions changes) { + controller.updateMarker(_selectedMarker, changes); + } + void _add() { - widget.controller.mapController.addMarker(new MarkerOptions( - position: new LatLng( + controller.addMarker(MarkerOptions( + position: LatLng( center.latitude + sin(_markerCount * pi / 6.0) / 20.0, center.longitude + cos(_markerCount * pi / 6.0) / 20.0, ), - infoWindowText: new InfoWindowText('Marker #${_markerCount + 1}', '*'), + infoWindowText: InfoWindowText('Marker #${_markerCount + 1}', '*'), )); setState(() { _markerCount += 1; @@ -77,7 +91,7 @@ } void _remove() { - _selectedMarker.remove(); + controller.removeMarker(_selectedMarker); setState(() { _selectedMarker = null; _markerCount -= 1; @@ -86,13 +100,13 @@ void _changePosition() { final LatLng current = _selectedMarker.options.position; - final Offset offset = new Offset( + final Offset offset = Offset( center.latitude - current.latitude, center.longitude - current.longitude, ); - _selectedMarker.update( - new MarkerOptions( - position: new LatLng( + _updateSelectedMarker( + MarkerOptions( + position: LatLng( center.latitude + offset.dy, center.longitude + offset.dx, ), @@ -102,32 +116,30 @@ void _changeAnchor() { final Offset currentAnchor = _selectedMarker.options.anchor; - final Offset newAnchor = - new Offset(1.0 - currentAnchor.dy, currentAnchor.dx); - _selectedMarker.update(new MarkerOptions(anchor: newAnchor)); + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + _updateSelectedMarker(MarkerOptions(anchor: newAnchor)); } Future<void> _changeInfoAnchor() async { final Offset currentAnchor = _selectedMarker.options.infoWindowAnchor; - final Offset newAnchor = - new Offset(1.0 - currentAnchor.dy, currentAnchor.dx); - _selectedMarker.update(new MarkerOptions(infoWindowAnchor: newAnchor)); + final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); + _updateSelectedMarker(MarkerOptions(infoWindowAnchor: newAnchor)); } Future<void> _toggleDraggable() async { - _selectedMarker.update( - new MarkerOptions(draggable: !_selectedMarker.options.draggable)); + _updateSelectedMarker( + MarkerOptions(draggable: !_selectedMarker.options.draggable), + ); } Future<void> _toggleFlat() async { - _selectedMarker - .update(new MarkerOptions(flat: !_selectedMarker.options.flat)); + _updateSelectedMarker(MarkerOptions(flat: !_selectedMarker.options.flat)); } Future<void> _changeInfo() async { final InfoWindowText currentInfo = _selectedMarker.options.infoWindowText; - _selectedMarker.update(new MarkerOptions( - infoWindowText: new InfoWindowText( + _updateSelectedMarker(MarkerOptions( + infoWindowText: InfoWindowText( currentInfo.title, currentInfo.snippet + '*', ), @@ -136,100 +148,101 @@ Future<void> _changeAlpha() async { final double current = _selectedMarker.options.alpha; - _selectedMarker.update( - new MarkerOptions(alpha: current < 0.1 ? 1.0 : current * 0.75), + _updateSelectedMarker( + MarkerOptions(alpha: current < 0.1 ? 1.0 : current * 0.75), ); } Future<void> _changeRotation() async { final double current = _selectedMarker.options.rotation; - _selectedMarker.update( - new MarkerOptions(rotation: current == 330.0 ? 0.0 : current + 30.0), + _updateSelectedMarker( + MarkerOptions(rotation: current == 330.0 ? 0.0 : current + 30.0), ); } Future<void> _toggleVisible() async { - _selectedMarker - .update(new MarkerOptions(visible: !_selectedMarker.options.visible)); + _updateSelectedMarker( + MarkerOptions(visible: !_selectedMarker.options.visible), + ); } Future<void> _changeZIndex() async { final double current = _selectedMarker.options.zIndex; - _selectedMarker.update( - new MarkerOptions(zIndex: current == 12.0 ? 0.0 : current + 1.0), + _updateSelectedMarker( + MarkerOptions(zIndex: current == 12.0 ? 0.0 : current + 1.0), ); } @override Widget build(BuildContext context) { - return new Column( + return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - new Center(child: new GoogleMapOverlay(controller: widget.controller)), - new Row( + Center(child: GoogleMapOverlay(controller: widget.controller)), + Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ - new Row( + Row( children: <Widget>[ - new Column( + Column( children: <Widget>[ - new FlatButton( + FlatButton( child: const Text('add'), onPressed: (_markerCount == 12) ? null : _add, ), - new FlatButton( + FlatButton( child: const Text('remove'), onPressed: (_selectedMarker == null) ? null : _remove, ), - new FlatButton( + FlatButton( child: const Text('change info'), onPressed: (_selectedMarker == null) ? null : _changeInfo, ), - new FlatButton( + FlatButton( child: const Text('change info anchor'), onPressed: (_selectedMarker == null) ? null : _changeInfoAnchor, ), ], ), - new Column( + Column( children: <Widget>[ - new FlatButton( + FlatButton( child: const Text('change alpha'), onPressed: (_selectedMarker == null) ? null : _changeAlpha, ), - new FlatButton( + FlatButton( child: const Text('change anchor'), onPressed: (_selectedMarker == null) ? null : _changeAnchor, ), - new FlatButton( + FlatButton( child: const Text('toggle draggable'), onPressed: (_selectedMarker == null) ? null : _toggleDraggable, ), - new FlatButton( + FlatButton( child: const Text('toggle flat'), onPressed: (_selectedMarker == null) ? null : _toggleFlat, ), - new FlatButton( + FlatButton( child: const Text('change position'), onPressed: (_selectedMarker == null) ? null : _changePosition, ), - new FlatButton( + FlatButton( child: const Text('change rotation'), onPressed: (_selectedMarker == null) ? null : _changeRotation, ), - new FlatButton( + FlatButton( child: const Text('toggle visible'), onPressed: (_selectedMarker == null) ? null : _toggleVisible, ), - new FlatButton( + FlatButton( child: const Text('change zIndex'), onPressed: (_selectedMarker == null) ? null : _changeZIndex,
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h index 16ea81d..a111525 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h
@@ -41,6 +41,7 @@ - (void)hide; - (void)animateWithCameraUpdate:(GMSCameraUpdate*)cameraUpdate; - (void)moveWithCameraUpdate:(GMSCameraUpdate*)cameraUpdate; +- (GMSCameraPosition*)cameraPosition; - (NSString*)addMarkerWithPosition:(CLLocationCoordinate2D)position; - (FLTGoogleMapMarkerController*)markerWithId:(NSString*)markerId; - (void)removeMarkerWithId:(NSString*)markerId;
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index 4391106..0cfae0b 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
@@ -59,6 +59,14 @@ [_mapView moveCamera:cameraUpdate]; } +- (GMSCameraPosition*)cameraPosition { + if (_trackCameraPosition) { + return _mapView.camera; + } else { + return nil; + } +} + - (NSString*)addMarkerWithPosition:(CLLocationCoordinate2D)position { FLTGoogleMapMarkerController* markerController = [[FLTGoogleMapMarkerController alloc] initWithPosition:position mapView:_mapView];
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h index ee724b5..2e5b80a 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h
@@ -15,6 +15,7 @@ - (void)setIcon:(UIImage*)icon; - (void)setInfoWindowAnchor:(CGPoint)anchor; - (void)setInfoWindowTitle:(NSString*)title snippet:(NSString*)snippet; +- (void)setPosition:(CLLocationCoordinate2D)position; - (void)setRotation:(CLLocationDegrees)rotation; - (void)setVisible:(BOOL)visible; - (void)setZIndex:(int)zIndex;
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m index d552db9..600c755 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m
@@ -48,6 +48,9 @@ _marker.title = title; _marker.snippet = snippet; } +- (void)setPosition:(CLLocationCoordinate2D)position { + _marker.position = position; +} - (void)setRotation:(CLLocationDegrees)rotation { _marker.rotation = rotation; }
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m index 29063e9..a251ceb 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.m
@@ -53,7 +53,7 @@ } [_mapControllers removeAllObjects]; result(nil); - } else if ([call.method isEqualToString:@"createMap"]) { + } else if ([call.method isEqualToString:@"map#create"]) { NSDictionary* options = call.arguments[@"options"]; GMSCameraPosition* camera = toOptionalCameraPosition(options[@"cameraPosition"]); FLTGoogleMapController* controller = @@ -73,22 +73,22 @@ result(error); return; } - if ([call.method isEqualToString:@"showMapOverlay"]) { + if ([call.method isEqualToString:@"map#show"]) { [controller showAtX:toDouble(call.arguments[@"x"]) Y:toDouble(call.arguments[@"y"])]; result(nil); - } else if ([call.method isEqualToString:@"hideMapOverlay"]) { + } else if ([call.method isEqualToString:@"map#hide"]) { [controller hide]; result(nil); - } else if ([call.method isEqualToString:@"animateCamera"]) { + } else if ([call.method isEqualToString:@"camera#animate"]) { [controller animateWithCameraUpdate:toCameraUpdate(call.arguments[@"cameraUpdate"])]; result(nil); - } else if ([call.method isEqualToString:@"moveCamera"]) { + } else if ([call.method isEqualToString:@"camera#move"]) { [controller moveWithCameraUpdate:toCameraUpdate(call.arguments[@"cameraUpdate"])]; result(nil); - } else if ([call.method isEqualToString:@"updateMapOptions"]) { + } else if ([call.method isEqualToString:@"map#update"]) { interpretMapOptions(call.arguments[@"options"], controller); - result(nil); - } else if ([call.method isEqualToString:@"addMarker"]) { + result(positionToJson([controller cameraPosition])); + } else if ([call.method isEqualToString:@"marker#add"]) { NSDictionary* options = call.arguments[@"options"]; NSString* markerId = [controller addMarkerWithPosition:toLocation(options[@"position"])]; interpretMarkerOptions(options, [controller markerWithId:markerId], _registrar); @@ -118,7 +118,7 @@ #pragma mark - FLTGoogleMapsDelegate methods, used to send platform messages to Flutter - (void)onCameraMoveStartedOnMap:(id)mapId gesture:(BOOL)gesture { - [_channel invokeMethod:@"map#onCameraMoveStarted" + [_channel invokeMethod:@"camera#onMoveStarted" arguments:@{ @"map" : mapId, @"isGesture" : @(gesture) @@ -126,12 +126,12 @@ } - (void)onCameraMoveOnMap:(id)mapId cameraPosition:(GMSCameraPosition*)cameraPosition { - [_channel invokeMethod:@"map#onCameraMove" + [_channel invokeMethod:@"camera#onMove" arguments:@{@"map" : mapId, @"position" : positionToJson(cameraPosition)}]; } - (void)onCameraIdleOnMap:(id)mapId { - [_channel invokeMethod:@"map#onCameraIdle" arguments:@{@"map" : mapId}]; + [_channel invokeMethod:@"camera#onIdle" arguments:@{@"map" : mapId}]; } - (void)onMarkerTappedOnMap:(id)mapId marker:(NSString*)markerId { @@ -146,6 +146,9 @@ } static id positionToJson(GMSCameraPosition* position) { + if (!position) { + return nil; + } return @{ @"target" : locationToJson([position target]), @"zoom" : @([position zoom]), @@ -338,6 +341,10 @@ NSString* snippet = (infoWindowTextData[1] == [NSNull null]) ? nil : infoWindowTextData[1]; [sink setInfoWindowTitle:title snippet:snippet]; } + id position = data[@"position"]; + if (position) { + [sink setPosition:toLocation(position)]; + } id rotation = data[@"rotation"]; if (rotation) { [sink setRotation:toDouble(rotation)];
diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart index f07c568..8f98368 100644 --- a/packages/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart
@@ -18,7 +18,7 @@ part 'src/callbacks.dart'; part 'src/camera.dart'; part 'src/controller.dart'; -part 'src/ui.dart'; part 'src/marker.dart'; part 'src/location.dart'; part 'src/platform_overlay.dart'; +part 'src/ui.dart';
diff --git a/packages/google_maps_flutter/lib/src/callbacks.dart b/packages/google_maps_flutter/lib/src/callbacks.dart index eb2ab86..d8e906f 100644 --- a/packages/google_maps_flutter/lib/src/callbacks.dart +++ b/packages/google_maps_flutter/lib/src/callbacks.dart
@@ -11,10 +11,17 @@ /// /// Additions and removals happening during a single [call] invocation do not /// change who gets a callback until the next such invocation. +/// +/// Optimized for the singleton case. class ArgumentCallbacks<T> { final List<ArgumentCallback<T>> _callbacks = <ArgumentCallback<T>>[]; - VoidCallback _onEmptyChanged; + /// Callback method. Invokes the corresponding method on each callback + /// in this collection. + /// + /// The list of callbacks being invoked is computed at the start of the + /// method and is unaffected by any changes subsequently made to this + /// collection. void call(T argument) { final int length = _callbacks.length; if (length == 1) { @@ -27,18 +34,22 @@ } } + /// Adds a callback to this collection. void add(ArgumentCallback<T> callback) { + assert(callback != null); _callbacks.add(callback); - if (_onEmptyChanged != null && _callbacks.length == 1) _onEmptyChanged(); } + /// Removes a callback from this collection. + /// + /// Does nothing, if the callback was not present. void remove(ArgumentCallback<T> callback) { - final bool removed = _callbacks.remove(callback); - if (_onEmptyChanged != null && removed && _callbacks.isEmpty) - _onEmptyChanged(); + _callbacks.remove(callback); } + /// Whether this collection is empty. bool get isEmpty => _callbacks.isEmpty; + /// Whether this collection is non-empty. bool get isNotEmpty => _callbacks.isNotEmpty; }
diff --git a/packages/google_maps_flutter/lib/src/camera.dart b/packages/google_maps_flutter/lib/src/camera.dart index e105980..3362a27 100644 --- a/packages/google_maps_flutter/lib/src/camera.dart +++ b/packages/google_maps_flutter/lib/src/camera.dart
@@ -4,6 +4,9 @@ part of google_maps_flutter; +/// The position of the map "camera", the view point from which the world is +/// shown in the map view. Aggregates the camera's [target] geographical +/// location, its [zoom] level, [tilt] angle, and [bearing]. class CameraPosition { const CameraPosition({ this.bearing = 0.0, @@ -15,9 +18,37 @@ assert(tilt != null), assert(zoom != null); + /// The camera's bearing in degrees, measured clockwise from north. + /// + /// A bearing of 0.0, the default, means the camera points north. + /// A bearing of 90.0 means the camera points east. final double bearing; + + /// The geographical location that the camera is pointing at. final LatLng target; + + /// The angle, in degrees, of the camera angle from the nadir. + /// + /// A tilt of 0.0, the default and minimum supported value, means the camera + /// is directly facing the Earth. + /// + /// The maximum tilt value depends on the current zoom level. Values beyond + /// the supported range are allowed, but on applying them to a map they will + /// be silently clamped to the supported range. final double tilt; + + /// The zoom level of the camera. + /// + /// A zoom of 0.0, the default, means the screen width of the world is 256. + /// Adding 1.0 to the zoom level doubles the screen width of the map. So at + /// zoom level 3.0, the screen width of the world is 2³x256=2048. + /// + /// Larger zoom levels thus means the camera is placed closer to the surface + /// of the Earth, revealing more detail in a narrower geographical region. + /// + /// The supported zoom level range depends on the map data and device. Values + /// beyond the supported range are allowed, but on applying them to a map they + /// will be silently clamped to the supported range. final double zoom; dynamic _toJson() => <String, dynamic>{ @@ -40,21 +71,28 @@ } } +/// Defines a camera move, supporting absolute moves as well as moves relative +/// the current position. class CameraUpdate { CameraUpdate._(this._json); - final dynamic _json; - + /// Returns a camera update that moves the camera to the specified position. static CameraUpdate newCameraPosition(CameraPosition cameraPosition) { return new CameraUpdate._( <dynamic>['newCameraPosition', cameraPosition._toJson()], ); } + /// Returns a camera update that moves the camera target to the specified + /// geographical location. static CameraUpdate newLatLng(LatLng latLng) { return new CameraUpdate._(<dynamic>['newLatLng', latLng._toJson()]); } + /// Returns a camera update that transforms the camera so that the specified + /// geographical bounding box is centered in the map view at the greatest + /// possible zoom level. A non-zero [padding] insets the bounding box from the + /// map view's edges. The camera's new tilt and bearing will both be 0.0. static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) { return new CameraUpdate._(<dynamic>[ 'newLatLngBounds', @@ -63,18 +101,29 @@ ]); } + /// Returns a camera update that moves the camera target to the specified + /// geographical location and zoom level. static CameraUpdate newLatLngZoom(LatLng latLng, double zoom) { return new CameraUpdate._( <dynamic>['newLatLngZoom', latLng._toJson(), zoom], ); } + /// Returns a camera update that moves the camera target the specified screen + /// distance. + /// + /// For a camera with bearing 0.0 (pointing north), scrolling by 50,75 moves + /// the camera's target to a geographical location that is 50 to the east and + /// 75 to the south of the current location, measured in screen coordinates. static CameraUpdate scrollBy(double dx, double dy) { return new CameraUpdate._( <dynamic>['scrollBy', dx, dy], ); } + /// Returns a camera update that modifies the camera zoom level by the + /// specified amount. The optional [focus] is a screen point whose underlying + /// geographical location should be invariant, if possible, by the movement. static CameraUpdate zoomBy(double amount, [Offset focus]) { if (focus == null) { return new CameraUpdate._(<dynamic>['zoomBy', amount]); @@ -87,17 +136,28 @@ } } + /// Returns a camera update that zooms the camera in, bringing the camera + /// closer to the surface of the Earth. + /// + /// Equivalent to the result of calling `zoomBy(1.0)`. static CameraUpdate zoomIn() { return new CameraUpdate._(<dynamic>['zoomIn']); } + /// Returns a camera update that zooms the camera out, bringing the camera + /// further away from the surface of the Earth. + /// + /// Equivalent to the result of calling `zoomBy(-1.0)`. static CameraUpdate zoomOut() { return new CameraUpdate._(<dynamic>['zoomOut']); } + /// Returns a camera update that sets the camera zoom level. static CameraUpdate zoomTo(double zoom) { return new CameraUpdate._(<dynamic>['zoomTo', zoom]); } + final dynamic _json; + dynamic _toJson() => _json; }
diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart index dd5d8fd..032766c 100644 --- a/packages/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/lib/src/controller.dart
@@ -7,25 +7,26 @@ final MethodChannel _channel = const MethodChannel('plugins.flutter.io/google_maps'); -/// Controller for a single GoogleMap instance. -/// -/// Used for programmatically controlling a platform-specific GoogleMap view. +/// Controller for a single GoogleMap instance running on the host platform. /// /// Change listeners are notified upon changes to any of /// -/// * the [options] property, +/// * the [options] property /// * the collection of [Marker]s added to this map -/// * the [cameraPosition] property, +/// * the [isCameraMoving] property +/// * the [cameraPosition] property /// -/// Listeners are notified when changes have been applied on the platform side. +/// Listeners are notified after changes have been applied on the platform side. /// /// Marker tap events can be received by adding callbacks to [onMarkerTapped]. class GoogleMapController extends ChangeNotifier { - GoogleMapController._({ - this.id, - GoogleMapOptions options, - }) : _options = options { - id.then((int id) { + @visibleForTesting + GoogleMapController(this._id, GoogleMapOptions options) + : assert(_id != null), + assert(options != null), + assert(options.cameraPosition != null), + _options = options { + _id.then((int id) { _controllers[id] = this; }); if (options.trackCameraPosition) { @@ -33,9 +34,7 @@ } } - /// An ID identifying the GoogleMaps instance, once created. - final Future<int> id; - + /// Callbacks to receive tap events for markers placed on this map. final ArgumentCallbacks<Marker> onMarkerTapped = new ArgumentCallbacks<Marker>(); @@ -44,9 +43,13 @@ GoogleMapOptions get options => _options; GoogleMapOptions _options; + /// The current set of markers on this map. + /// + /// The returned set will be a detached snapshot of the markers collection. Set<Marker> get markers => new Set<Marker>.from(_markers.values); final Map<String, Marker> _markers = <String, Marker>{}; + /// True if the map camera is currently moving. bool get isCameraMoving => _isCameraMoving; bool _isCameraMoving = false; @@ -56,11 +59,18 @@ CameraPosition get cameraPosition => _cameraPosition; CameraPosition _cameraPosition; + final Future<int> _id; + static Map<int, GoogleMapController> _controllers = <int, GoogleMapController>{}; + /// Initializes the GoogleMaps plugin. Should be called from the Flutter + /// application's main entry point. + // Clears any existing platform-side map instances after hot restart. + // Sets up method call handlers for receiving map events. static Future<void> init() async { await _channel.invokeMethod('init'); + _controllers.clear(); _channel.setMethodCallHandler((MethodCall call) { final int mapId = call.arguments['map']; final GoogleMapController controller = _controllers[mapId]; @@ -79,15 +89,15 @@ onMarkerTapped(marker); } break; - case 'map#onCameraMoveStarted': + case 'camera#onMoveStarted': _isCameraMoving = true; notifyListeners(); break; - case 'map#onCameraMove': + case 'camera#onMove': _cameraPosition = CameraPosition._fromJson(call.arguments['position']); notifyListeners(); break; - case 'map#onCameraIdle': + case 'camera#onIdle': _isCameraMoving = false; notifyListeners(); break; @@ -96,108 +106,149 @@ } } - Future<void> updateMapOptions(GoogleMapOptions options) async { - assert(options != null); - final int id = await this.id; - await _channel.invokeMethod('updateMapOptions', <String, dynamic>{ - 'map': id, - 'options': options._toJson(), - }); - _options = _options._updateWith(options); - if (!_options.trackCameraPosition) { - _cameraPosition = null; - } + /// Updates configuration options of the map user interface. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future<void> updateMapOptions(GoogleMapOptions changes) async { + assert(changes != null); + final int id = await _id; + final dynamic json = await _channel.invokeMethod( + 'map#update', + <String, dynamic>{ + 'map': id, + 'options': changes._toJson(), + }, + ); + _options = _options.copyWith(changes); + _cameraPosition = CameraPosition._fromJson(json); notifyListeners(); } + /// Starts an animated change of the map camera position. + /// + /// The returned [Future] completes after the change has been started on the + /// platform side. Future<void> animateCamera(CameraUpdate cameraUpdate) async { - final int id = await this.id; - await _channel.invokeMethod('animateCamera', <String, dynamic>{ + final int id = await _id; + await _channel.invokeMethod('camera#animate', <String, dynamic>{ 'map': id, 'cameraUpdate': cameraUpdate._toJson(), }); } + /// Changes the map camera position. + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. Future<void> moveCamera(CameraUpdate cameraUpdate) async { - final int id = await this.id; - await _channel.invokeMethod('moveCamera', <String, dynamic>{ + final int id = await _id; + await _channel.invokeMethod('camera#move', <String, dynamic>{ 'map': id, 'cameraUpdate': cameraUpdate._toJson(), }); } + /// Adds a marker to the map, configured using the specified custom [options]. + /// + /// Change listeners are notified once the marker has been added on the + /// platform side. + /// + /// The returned [Future] completes with the added marker once listeners have + /// been notified. Future<Marker> addMarker(MarkerOptions options) async { - assert(options != null); - assert(options.position != null); - final int id = await this.id; + final int id = await _id; final MarkerOptions effectiveOptions = - MarkerOptions.defaultOptions._updateWith(options); + MarkerOptions.defaultOptions.copyWith(options); final String markerId = await _channel.invokeMethod( - 'addMarker', + 'marker#add', <String, dynamic>{ 'map': id, 'options': effectiveOptions._toJson(), }, ); - final Marker marker = new Marker._(this, markerId, effectiveOptions); + final Marker marker = new Marker(markerId, effectiveOptions); _markers[markerId] = marker; notifyListeners(); return marker; } - Future<void> _updateMarker(Marker marker, MarkerOptions changes) async { - assert(_markers[marker.id] == marker); + /// Updates the specified [marker] with the given [changes]. The marker must + /// be a current member of the [markers] set. + /// + /// Change listeners are notified once the marker has been updated on the + /// platform side. + /// + /// The returned [Future] completes once listeners have been notified. + Future<void> updateMarker(Marker marker, MarkerOptions changes) async { + assert(marker != null); + assert(_markers[marker._id] == marker); assert(changes != null); - final int id = await this.id; + final int id = await _id; await _channel.invokeMethod('marker#update', <String, dynamic>{ 'map': id, - 'marker': marker.id, + 'marker': marker._id, 'options': changes._toJson(), }); - marker._options = marker._options._updateWith(changes); + marker._options = marker._options.copyWith(changes); notifyListeners(); } - Future<void> _removeMarker(Marker marker) async { - assert(_markers[marker.id] == marker); - final int id = await this.id; + /// Removes the specified [marker] from the map. The marker must be a current + /// member of the [markers] set. + /// + /// Change listeners are notified once the marker has been removed on the + /// platform side. + /// + /// The returned [Future] completes once listeners have been notified. + Future<void> removeMarker(Marker marker) async { + assert(marker != null); + assert(_markers[marker._id] == marker); + final int id = await _id; await _channel.invokeMethod('marker#remove', <String, dynamic>{ 'map': id, - 'marker': marker.id, + 'marker': marker._id, }); - _markers.remove(marker.id); + _markers.remove(marker._id); notifyListeners(); } } -/// Controller for a GoogleMap instance that is integrated as a +/// Controller pair for a GoogleMap instance that is integrated as a /// platform overlay. /// +/// The [mapController] programmatically controls the platform GoogleMap view +/// and supports event handling. +/// +/// The [overlayController] is used to hide and show the platform overlay at +/// appropriate times to avoid rendering artifacts when the necessary conditions +/// for correctly displaying a platform overlay are not met: the underlying +/// widget must be stationary and rendered on top of all other widgets within +/// bounds. +/// /// *Warning*: Platform overlays cannot be freely composed with /// other widgets. See [PlatformOverlayController] for caveats and /// limitations. class GoogleMapOverlayController { GoogleMapOverlayController._(this.mapController, this.overlayController); - /// Creates a controller for a GoogleMaps of the specified size in - /// logical pixels. + /// Creates a controller for a GoogleMaps of the specified size and with the + /// specified custom [options], if any. factory GoogleMapOverlayController.fromSize({ @required double width, @required double height, - GoogleMapOptions options = const GoogleMapOptions(), + GoogleMapOptions options, }) { assert(width != null); assert(height != null); - assert(options != null); final GoogleMapOptions effectiveOptions = - GoogleMapOptions.defaultOptions._updateWith(options); + GoogleMapOptions.defaultOptions.copyWith(options); final _GoogleMapsPlatformOverlay overlay = new _GoogleMapsPlatformOverlay(effectiveOptions); return new GoogleMapOverlayController._( - new GoogleMapController._( - id: overlay._textureId.future, - options: effectiveOptions, - ), + new GoogleMapController(overlay._textureId.future, effectiveOptions), new PlatformOverlayController(width, height, overlay), ); } @@ -221,7 +272,7 @@ @override Future<int> create(Size size) { - _textureId.complete(_channel.invokeMethod('createMap', <String, dynamic>{ + _textureId.complete(_channel.invokeMethod('map#create', <String, dynamic>{ 'width': size.width, 'height': size.height, 'options': options._toJson(), @@ -232,7 +283,7 @@ @override Future<void> show(Offset offset) async { final int id = await _textureId.future; - _channel.invokeMethod('showMapOverlay', <String, dynamic>{ + _channel.invokeMethod('map#show', <String, dynamic>{ 'map': id, 'x': offset.dx, 'y': offset.dy, @@ -242,7 +293,7 @@ @override Future<void> hide() async { final int id = await _textureId.future; - _channel.invokeMethod('hideMapOverlay', <String, dynamic>{ + _channel.invokeMethod('map#hide', <String, dynamic>{ 'map': id, }); } @@ -250,13 +301,25 @@ @override Future<void> dispose() async { final int id = await _textureId.future; - _channel.invokeMethod('disposeMap', <String, dynamic>{ + _channel.invokeMethod('map#dispose', <String, dynamic>{ 'map': id, }); } } -/// A Widget covered by a GoogleMaps platform overlay. +/// A widget covered by a GoogleMap platform overlay. +/// +/// The overlay is intended to be shown only while the map is interactive, +/// stationary, and the widget is rendered on top of all other widgets. In all +/// other situations, the overlay should be hidden to avoid rendering artifacts. +/// While the overlay is hidden, the widget shows a Texture with the most recent +/// bitmap snapshot extracted from the GoogleMap view. That bitmap will be +/// slightly delayed compared to the actual platform view which will be visible, +/// if a map animation is started and the overlay then hidden. +/// +/// *Warning*: Platform overlays cannot be freely composed with +/// other widgets. See [PlatformOverlayController] for caveats and +/// limitations. class GoogleMapOverlay extends StatefulWidget { final GoogleMapOverlayController controller; @@ -283,7 +346,7 @@ Widget build(BuildContext context) { return new SizedBox( child: new FutureBuilder<int>( - future: widget.controller.mapController.id, + future: widget.controller.mapController._id, builder: (_, AsyncSnapshot<int> snapshot) { if (snapshot.hasData) { return new Texture(textureId: snapshot.data);
diff --git a/packages/google_maps_flutter/lib/src/location.dart b/packages/google_maps_flutter/lib/src/location.dart index f459d70..9a2b4a1 100644 --- a/packages/google_maps_flutter/lib/src/location.dart +++ b/packages/google_maps_flutter/lib/src/location.dart
@@ -6,12 +6,25 @@ /// A pair of latitude and longitude coordinates, stored as degrees. class LatLng { + /// The latitude in degrees between -90.0 and 90.0, both inclusive. final double latitude; + + /// The longitude in degrees between -180.0 (inclusive) and 180.0 (exclusive). final double longitude; - const LatLng(this.latitude, this.longitude) + /// Creates a geographical location specified in degrees [latitude] and + /// [longitude]. + /// + /// The latitude is clamped to the inclusive interval from -90.0 to +90.0. + /// + /// The longitude is normalized to the half-open interval from -180.0 + /// (inclusive) to +180.0 (exclusive) + const LatLng(double latitude, double longitude) : assert(latitude != null), - assert(longitude != null); + assert(longitude != null), + latitude = + (latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude)), + longitude = (longitude + 180.0) % 360.0 - 180.0; dynamic _toJson() { return <double>[latitude, longitude]; @@ -39,13 +52,28 @@ } /// A latitude/longitude aligned rectangle. +/// +/// The rectangle conceptually includes all points (lat, lng) where +/// * lat ∈ [`southwest.latitude`, `northeast.latitude`] +/// * lng ∈ [`southwest.longitude`, `northeast.longitude`], +/// if `southwest.longitude` ≤ `northeast.longitude`, +/// * lng ∈ [-180, `northeast.longitude`] ∪ [`southwest.longitude`, 180[, +/// if `northeast.longitude` < `southwest.longitude` class LatLngBounds { + /// The southwest corner of the rectangle. final LatLng southwest; + + /// The northeast corner of the rectangle. final LatLng northeast; - const LatLngBounds({@required this.southwest, @required this.northeast}) + /// Creates geographical bounding box with the specified corners. + /// + /// The latitude of the southwest corner cannot be larger than the + /// latitude of the northeast corner. + LatLngBounds({@required this.southwest, @required this.northeast}) : assert(southwest != null), - assert(northeast != null); + assert(northeast != null), + assert(southwest.latitude <= northeast.latitude); dynamic _toJson() { return <dynamic>[southwest._toJson(), northeast._toJson()];
diff --git a/packages/google_maps_flutter/lib/src/marker.dart b/packages/google_maps_flutter/lib/src/marker.dart index 4552cf8..c830adc 100644 --- a/packages/google_maps_flutter/lib/src/marker.dart +++ b/packages/google_maps_flutter/lib/src/marker.dart
@@ -4,32 +4,25 @@ part of google_maps_flutter; -/// An icon placed at a particular point on the map's surface. A marker icon is -/// drawn oriented against the device's screen rather than the map's surface; -/// that is, it will not necessarily change orientation due to map rotations, -/// tilting, or zooming. +/// An icon placed at a particular geographical location on the map's surface. +/// A marker icon is drawn oriented against the device's screen rather than the +/// map's surface; that is, it will not necessarily change orientation due to +/// map rotations, tilting, or zooming. /// -/// Markers are owned by a single [GoogleMapController] which fires change -/// events when markers are added, updated, or removed. +/// Markers are owned by a single [GoogleMapController] which fires events +/// as markers are added, updated, tapped, and removed. class Marker { - Marker._(this._mapController, this.id, this._options); + @visibleForTesting + Marker(this._id, this._options); - final GoogleMapController _mapController; - final String id; + final String _id; MarkerOptions _options; - Future<void> remove() { - return _mapController._removeMarker(this); - } - - Future<void> update(MarkerOptions changes) { - return _mapController._updateMarker(this, changes); - } - - /// The configuration options most recently applied programmatically. + /// The marker configuration options most recently applied programmatically + /// via the map controller. /// /// The returned value does not reflect any changes made to the marker through - /// touch events. Add listeners to track those. + /// touch events. Add listeners to the owning map controller to track those. MarkerOptions get options => _options; } @@ -44,9 +37,17 @@ class InfoWindowText { const InfoWindowText(this.title, this.snippet); + /// Text labels specifying that no text is to be displayed. static const InfoWindowText noText = const InfoWindowText(null, null); + /// Text displayed in an info window when the user taps the marker. + /// + /// A null value means no title. final String title; + + /// Additional text displayed below the [title]. + /// + /// A null value means no additional text. final String snippet; dynamic _toJson() => <dynamic>[title, snippet]; @@ -55,21 +56,66 @@ /// Configuration options for [Marker] instances. /// /// When used to change configuration, null values will be interpreted as -/// "do not change this configuration item". +/// "do not change this configuration option". class MarkerOptions { + /// The opacity of the marker, between 0.0 and 1.0 inclusive. + /// + /// 0.0 means fully transparent, 1.0 means fully opaque. final double alpha; + + /// The icon image point that will be placed at the [position] of the marker. + /// + /// The image point is specified in normalized coordinates: An anchor of + /// (0.0, 0.0) means the top left corner of the image. An anchor + /// of (1.0, 1.0) means the bottom right corner of the image. final Offset anchor; + + /// True if the marker icon consumes tap events. If not, the map will perform + /// default tap handling by centering the map on the marker and displaying its + /// info window. final bool consumeTapEvents; + + /// True if the marker is draggable by user touch events. final bool draggable; + + /// True if the marker is rendered flatly against the surface of the Earth, so + /// that it will rotate and tilt along with map camera movements. final bool flat; + + /// A description of the bitmap used to draw the marker icon. final BitmapDescriptor icon; + + /// The icon image point that will be the anchor of the info window when + /// displayed. + /// + /// The image point is specified in normalized coordinates: An anchor of + /// (0.0, 0.0) means the top left corner of the image. An anchor + /// of (1.0, 1.0) means the bottom right corner of the image. final Offset infoWindowAnchor; + + /// Text content for the info window. final InfoWindowText infoWindowText; + + /// Geographical location of the marker. final LatLng position; + + /// Rotation of the marker image in degrees clockwise from the [anchor] point. final double rotation; + + /// True if the marker is visible. final bool visible; + + /// The z-index of the marker, used to determine relative drawing order of + /// map overlays. + /// + /// Overlays are drawn in order of z-index, so that lower values means drawn + /// earlier, and thus appearing to be closer to the surface of the Earth. final double zIndex; + /// Creates a set of marker configuration options. + /// + /// By default, every non-specified field is null, meaning no desire to change + /// marker defaults or current configuration. const MarkerOptions({ this.alpha, this.anchor, @@ -83,9 +129,24 @@ this.rotation, this.visible, this.zIndex, - }); + }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); - static const MarkerOptions defaultOptions = const MarkerOptions( + /// Default marker options. + /// + /// Specifies a marker that + /// * is fully opaque; [alpha] is 1.0 + /// * uses icon bottom center to indicate map position; [anchor] is (0.5, 1.0) + /// * has default tap handling; [consumeTapEvents] is false + /// * is stationary; [draggable] is false + /// * is drawn against the screen, not the map; [flat] is false + /// * has a default icon; [icon] is `BitmapDescriptor.defaultMarker` + /// * anchors the info window at top center; [infoWindowAnchor] is (0.5, 0.0) + /// * has no info window text; [infoWindowText] is `InfoWindowText.noText` + /// * is positioned at 0, 0; [position] is `LatLng(0.0, 0.0)` + /// * has an axis-aligned icon; [rotation] is 0.0 + /// * is visible; [visible] is true + /// * is placed at the base of the drawing order; [zIndex] is 0.0 + static const MarkerOptions defaultOptions = MarkerOptions( alpha: 1.0, anchor: const Offset(0.5, 1.0), consumeTapEvents: false, @@ -94,12 +155,20 @@ icon: BitmapDescriptor.defaultMarker, infoWindowAnchor: const Offset(0.5, 0.0), infoWindowText: InfoWindowText.noText, + position: LatLng(0.0, 0.0), rotation: 0.0, visible: true, zIndex: 0.0, ); - MarkerOptions _updateWith(MarkerOptions changes) { + /// Creates a new options object whose values are the same as this instance, + /// unless overwritten by the specified [changes]. + /// + /// Returns this instance, if [changes] is null. + MarkerOptions copyWith(MarkerOptions changes) { + if (changes == null) { + return this; + } return new MarkerOptions( alpha: changes.alpha ?? alpha, anchor: changes.anchor ?? anchor,
diff --git a/packages/google_maps_flutter/lib/src/platform_overlay.dart b/packages/google_maps_flutter/lib/src/platform_overlay.dart index f56d4bb..5f61e30 100644 --- a/packages/google_maps_flutter/lib/src/platform_overlay.dart +++ b/packages/google_maps_flutter/lib/src/platform_overlay.dart
@@ -4,25 +4,48 @@ part of google_maps_flutter; -/// Controller of platform overlays, supporting a very limited form -/// of compositing with Flutter Widgets. +/// Controller of platform overlays used for creating the illusion, in *very +/// limited situations*, of in-line compositing of platform views with Flutter +/// widgets. /// -/// Platform overlays are normal platform-specific views that are -/// created, shown on top of the Flutter view, or hidden below it, -/// under control of the Flutter app. The platform overlay is -/// typically placed on top of a [Texture] widget acting as stand-in -/// while Flutter movement or transformations are ongoing. +/// Platform overlays are normal platform views that are displayed on top of the +/// Flutter view when so directed by the Flutter app's Dart code. The platform +/// overlay is placed on top of a [Texture] widget acting as a non-interactive +/// stand in while the conditions for correctly displaying the overlay are not +/// met. Those conditions are: /// -/// Overlays are attached to a [BuildContext] when used in a Widget and -/// are deactivated when the ambient ModalRoute (if any) is not on top of the -/// navigator stack. +/// * the widget must be stationary +/// * the widget must be rendered on top of all other widgets within bounds +/// * touch events originating within the widget's bounds can be safely ignored +/// by Flutter code (they will be intercepted by the platform overlay) /// -/// *Warning*: Platform overlays cannot be freely composed with -/// over widgets. +/// These conditions severely restrict the contexts in which platform overlays +/// can be used. Worse, there is no easy way of learning if a given widget +/// currently satisfies those conditions, so they must be explicitly enforced +/// by the app author. Examples include avoiding placing the widget on a +/// scrollable view; hiding the overlay during animated transitions or while a +/// drawer is being shown on top; avoiding placing the widget at the edge of +/// the screen where the platform view would interfere with edge swipes; etc. +/// The app author should expect little help from existing widgets in this +/// endeavor; some widgets (Material Scaffold being a prime example) do not +/// offer to notify clients before and after they display Flutter overlays or +/// animate to new configurations. Using platform overlays may require custom +/// implementations of such widgets. /// -/// Limitations and caveats: +/// *Warning*: Platform overlays cannot be freely composed with other widgets. /// -/// * TODO(mravn) +/// For the above reasons, *the use of platform overlays is generally +/// discouraged*. Still, overlays provide an interim solution in situations +/// where one wants to create the illusion of in-line compositing of native +/// platform views (such as GoogleMaps) for which no API exists for connecting a +/// Texture widget directly to the native OpenGL rendering pipeline. +/// +/// Overlays may be attached to the [BuildContext] in which the Texture widget +/// is built and are then automatically hidden when the ambient ModalRoute (if +/// any) is not on top of the navigator stack. This is currently the *only* +/// built-in mechanism for helping the app author ensure that the overlay +/// conditions mentioned above are met. Making use of this mechanism requires +/// the overlay controller to be added as an observer of the main Navigator. class PlatformOverlayController extends NavigatorObserver with WidgetsBindingObserver { final double width; @@ -184,11 +207,6 @@ } } - @override - void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { - // TODO(mravn) - } - void dispose() { if (!_disposed) { overlay.dispose(); @@ -204,12 +222,14 @@ } } -/// Platform overlay. +/// Interface of platform overlay implementations. Typical implementation use +/// a [MethodChannel] to communicate with platform-specific code and have it +/// manage a collection of related platform overlays. abstract class PlatformOverlay { /// Creates a platform view of the specified [size]. /// /// The platform view should remain hidden until explicitly shown by calling - /// [showOverlay]. + /// [show]. Future<int> create(Size size); /// Shows the platform view at the specified [offset].
diff --git a/packages/google_maps_flutter/lib/src/ui.dart b/packages/google_maps_flutter/lib/src/ui.dart index 496d45b..fcc870e 100644 --- a/packages/google_maps_flutter/lib/src/ui.dart +++ b/packages/google_maps_flutter/lib/src/ui.dart
@@ -5,41 +5,61 @@ part of google_maps_flutter; /// Type of map tiles to display. -/// -/// Enum constants must be indexed to match the corresponding int constants of -/// the platform APIs, see -/// <https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap.html#MAP_TYPE_NORMAL> +// Enum constants must be indexed to match the corresponding int constants of +// the Android platform API, see +// <https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap.html#MAP_TYPE_NORMAL> enum MapType { + /// Do not display map tiles. none, + + /// Normal tiles (traffic and labels, subtle terrain information). normal, + + /// Satellite imaging tiles (aerial photos) satellite, + + /// Terrain tiles (indicates type and height of terrain) terrain, + + /// Hybrid tiles (satellite images with some labels/overlays) hybrid, } /// Bounds for the map camera target. +// Used with [GoogleMapOptions] to wrap a [LatLngBounds] value. This allows +// distinguishing between specifying an unbounded target (null `LatLngBounds`) +// from not specifying anything (null `CameraTargetBounds`). class CameraTargetBounds { + /// Creates a camera target bounds with the specified bounding box, or null + /// to indicate that the camera target is not bounded. const CameraTargetBounds(this.bounds); - /// The current bounds or null, if the camera target is unbounded. + /// The geographical bounding box for the map camera target. + /// + /// A null value means the camera target is unbounded. final LatLngBounds bounds; + /// Unbounded camera target. static const CameraTargetBounds unbounded = const CameraTargetBounds(null); dynamic _toJson() => <dynamic>[bounds?._toJson()]; } /// Preferred bounds for map camera zoom level. +// Used with [GoogleMapOptions] to wrap min and max zoom. This allows +// distinguishing between specifying unbounded zooming (null `minZoom` and +// `maxZoom`) from not specifying anything (null `MinMaxZoomPreference`). class MinMaxZoomPreference { const MinMaxZoomPreference(this.minZoom, this.maxZoom) : assert(minZoom == null || maxZoom == null || minZoom <= maxZoom); - /// The current minimum zoom level or null, if unbounded from below. + /// The preferred minimum zoom level or null, if unbounded from below. final double minZoom; - /// The current maximum zoom level or null, if unbounded from above. + /// The preferred maximum zoom level or null, if unbounded from above. final double maxZoom; + /// Unbounded zooming. static const MinMaxZoomPreference unbounded = const MinMaxZoomPreference(null, null); @@ -49,20 +69,50 @@ /// Configuration options for the GoogleMaps user interface. /// /// When used to change configuration, null values will be interpreted as -/// "do not change this configuration item". +/// "do not change this configuration option". class GoogleMapOptions { + /// The desired position of the map camera. + /// + /// This field is used to indicate initial camera position and to update that + /// position programmatically along with other changes to the map user + /// interface. It does not track the camera position through animations or + /// reflect movements caused by user touch events. final CameraPosition cameraPosition; + + /// True if the map should show a compass when rotated. final bool compassEnabled; + + /// Geographical bounding box for the camera target. final CameraTargetBounds cameraTargetBounds; + + /// Type of map tiles to be rendered. final MapType mapType; + + /// 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 respond to tilt gestures. final bool tiltGesturesEnabled; + + /// True if the map view should relay camera move events to Flutter. final bool trackCameraPosition; + + /// True if the map view should respond to zoom gestures. final bool zoomGesturesEnabled; - const GoogleMapOptions({ + /// Creates a set of map user interface configuration options. + /// + /// By default, every non-specified field is null, meaning no desire to change + /// user interface defaults or current configuration. + GoogleMapOptions({ this.cameraPosition, this.compassEnabled, this.cameraTargetBounds, @@ -75,8 +125,22 @@ this.zoomGesturesEnabled, }); - static const GoogleMapOptions defaultOptions = const GoogleMapOptions( + /// Default user interface options. + /// + /// Specifies a map view that + /// * displays a compass when rotated; [compassEnabled] is true + /// * positions the camera at 0,0; [cameraPosition] has target `LatLng(0.0, 0.0)` + /// * does not bound the camera target; [cameraTargetBounds] is `CameraTargetBounds.unbounded` + /// * uses normal map tiles; [mapType] is `MapType.normal` + /// * does not bound zooming; [minMaxZoomPreference] is `MinMaxZoomPreference.unbounded` + /// * responds to rotate gestures; [rotateGesturesEnabled] is true + /// * responds to scroll gestures; [scrollGesturesEnabled] is true + /// * responds to tilt gestures; [tiltGesturesEnabled] is true + /// * is silent about camera movement; [trackCameraPosition] is false + /// * responds to zoom gestures; [zoomGesturesEnabled] is true + static final GoogleMapOptions defaultOptions = GoogleMapOptions( compassEnabled: true, + cameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)), cameraTargetBounds: CameraTargetBounds.unbounded, mapType: MapType.normal, minMaxZoomPreference: MinMaxZoomPreference.unbounded, @@ -87,7 +151,14 @@ zoomGesturesEnabled: true, ); - GoogleMapOptions _updateWith(GoogleMapOptions change) { + /// Creates a new options object whose values are the same as this instance, + /// unless overwritten by the specified [changes]. + /// + /// Returns this instance, if [changes] is null. + GoogleMapOptions copyWith(GoogleMapOptions change) { + if (change == null) { + return this; + } return new GoogleMapOptions( cameraPosition: change.cameraPosition ?? cameraPosition, compassEnabled: change.compassEnabled ?? compassEnabled,