diff --git a/AUTHORS b/AUTHORS index 47038c256401..fe6fcee1c6bc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -50,4 +50,5 @@ Luigi Agosti Quentin Le Guennec Koushik Ravikumar Nissim Dsilva -Giancarlo Rocha \ No newline at end of file +Giancarlo Rocha +Ryo Miyake diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index fbbc3cc885a9..bf00be49c300 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.25+1 + +* Add takeSnapshot that takes a snapshot of the map. + ## 0.5.25 * Add an optional param `mipmaps` for `BitmapDescriptor.fromAssetImage`. diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index b1e5ec0bb768..9aea19696456 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -17,6 +17,7 @@ import android.app.Application; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.Bitmap; import android.graphics.Point; import android.os.Bundle; import android.util.Log; @@ -27,6 +28,7 @@ import androidx.lifecycle.LifecycleOwner; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.GoogleMap.SnapshotReadyCallback; import com.google.android.gms.maps.GoogleMapOptions; import com.google.android.gms.maps.MapView; import com.google.android.gms.maps.OnMapReadyCallback; @@ -44,6 +46,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.platform.PlatformView; +import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -276,6 +279,26 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { } break; } + case "map#takeSnapshot": + { + if (googleMap != null) { + final MethodChannel.Result _result = result; + googleMap.snapshot( + new SnapshotReadyCallback() { + @Override + public void onSnapshotReady(Bitmap bitmap) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + byte[] byteArray = stream.toByteArray(); + bitmap.recycle(); + _result.success(byteArray); + } + }); + } else { + result.error("GoogleMap uninitialized", "takeSnapshot", null); + } + break; + } case "camera#move": { final CameraUpdate cameraUpdate = diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index f4b8776559ec..a5a0f199abe4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -18,6 +18,7 @@ import 'place_marker.dart'; import 'place_polygon.dart'; import 'place_polyline.dart'; import 'scrolling_map.dart'; +import 'snapshot.dart'; final List _allPages = [ MapUiPage(), @@ -32,6 +33,7 @@ final List _allPages = [ PlacePolygonPage(), PlaceCirclePage(), PaddingPage(), + SnapshotPage(), ]; class MapsDemo extends StatelessWidget { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart new file mode 100644 index 000000000000..55d2a78b7d68 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart @@ -0,0 +1,72 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class SnapshotPage extends Page { + SnapshotPage() + : super(const Icon(Icons.camera_alt), 'Take a snapshot of the map'); + + @override + Widget build(BuildContext context) { + return _SnapshotBody(); + } +} + +class _SnapshotBody extends StatefulWidget { + @override + _SnapshotBodyState createState() => _SnapshotBodyState(); +} + +class _SnapshotBodyState extends State<_SnapshotBody> { + GoogleMapController _mapController; + Uint8List _imageBytes; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 180, + child: GoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + ), + ), + FlatButton( + child: Text('Take a snapshot'), + onPressed: () async { + final imageBytes = await _mapController?.takeSnapshot(); + setState(() { + _imageBytes = imageBytes; + }); + }, + ), + Container( + decoration: BoxDecoration(color: Colors.blueGrey[50]), + height: 180, + child: _imageBytes != null ? Image.memory(_imageBytes) : null, + ), + ], + ), + ); + } + + void onMapCreated(GoogleMapController controller) { + _mapController = controller; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_map_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_map_inspector.dart index 1f87a8787a86..a21e75ca9717 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_map_inspector.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_map_inspector.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -63,4 +64,8 @@ class GoogleMapInspector { Future isBuildingsEnabled() async { return await _channel.invokeMethod('map#isBuildingsEnabled'); } + + Future takeSnapshot() async { + return await _channel.invokeMethod('map#takeSnapshot'); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e.dart b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e.dart index 77621693bca6..347dde9bedd9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e.dart @@ -4,12 +4,13 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; +import 'package:e2e/e2e.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; -import 'package:e2e/e2e.dart'; import 'google_map_inspector.dart'; @@ -838,4 +839,30 @@ void main() { // ignore: invalid_use_of_visible_for_testing_member expect(scaled.toJson()[2], 2); }); + + testWidgets('testTakeSnapshot', (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel); + inspectorCompleter.complete(inspector); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + final Uint8List bytes = await inspector.takeSnapshot(); + expect(bytes?.isNotEmpty, true); + }); } diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m index d694f6f50cee..9b191979fe3d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -171,6 +171,22 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } } else if ([call.method isEqualToString:@"map#waitForMap"]) { result(nil); + } else if ([call.method isEqualToString:@"map#takeSnapshot"]) { + if (_mapView != nil) { + UIGraphicsImageRendererFormat* format = [UIGraphicsImageRendererFormat defaultFormat]; + format.scale = [[UIScreen mainScreen] scale]; + UIGraphicsImageRenderer* renderer = + [[UIGraphicsImageRenderer alloc] initWithSize:_mapView.frame.size format:format]; + + UIImage* image = [renderer imageWithActions:^(UIGraphicsImageRendererContext* context) { + [_mapView.layer renderInContext:context.CGContext]; + }]; + result([FlutterStandardTypedData typedDataWithBytes:UIImagePNGRepresentation(image)]); + } else { + result([FlutterError errorWithCode:@"GoogleMap uninitialized" + message:@"takeSnapshot called prior to map initialization" + details:nil]); + } } else if ([call.method isEqualToString:@"markers#update"]) { id markersToAdd = call.arguments[@"markersToAdd"]; if ([markersToAdd isKindOfClass:[NSArray class]]) { diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index 3dcea9c2bbac..750bb69393e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -285,4 +285,9 @@ class GoogleMapController { await channel.invokeMethod('map#getZoomLevel'); return zoomLevel; } + + /// Returns the image bytes of the map + Future takeSnapshot() async { + return await channel.invokeMethod('map#takeSnapshot'); + } }