diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 2fe148c5090d..3b4a2f4c4381 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 2.6.0 +* Fixes missing updates in TLHC mode. +* Switched default display mode to TLHC mode. * Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 2.5.3 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/README.md b/packages/google_maps_flutter/google_maps_flutter_android/README.md index 836ab2c9835a..8c482864dfdf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/README.md @@ -30,26 +30,26 @@ void main() { final GoogleMapsFlutterPlatform mapsImplementation = GoogleMapsFlutterPlatform.instance; if (mapsImplementation is GoogleMapsFlutterAndroid) { + // Force Hybrid Composition mode. mapsImplementation.useAndroidViewSurface = true; } // ยทยทยท } ``` -### Hybrid Composition +### Texture Layer Hybrid Composition -This is the current default mode, and corresponds to -`useAndroidViewSurface = true`. It ensures that the map display will work as -expected, at the cost of some performance. +This is the the current default mode and corresponds to `useAndroidViewSurface = false`. +This mode is more performant than Hybrid Composition and we recommend that you use this mode. -### Texture Layer Hybrid Composition +### Hybrid Composition -This is a new display mode used by most plugins starting with Flutter 3.0, and -corresponds to `useAndroidViewSurface = false`. This is more performant than -Hybrid Composition, but currently [misses certain map updates][4]. +This mode is available for backwards compatability and corresponds to `useAndroidViewSurface = true`. +We do not recommend its use as it is less performant than Texture Layer Hybrid Composition and +certain flutter rendering effects are not supported. -This mode will likely become the default in future versions if/when the -missed updates issue can be resolved. +If you require this mode for correctness, please file a bug so we can investigate and fix +the issue in the TLHC mode. ## Map renderer @@ -70,8 +70,13 @@ AndroidMapRenderer mapRenderer = AndroidMapRenderer.platformDefault; } ``` -Available values are `AndroidMapRenderer.latest`, `AndroidMapRenderer.legacy`, `AndroidMapRenderer.platformDefault`. -Note that getting the requested renderer as a response is not guaranteed. +`AndroidMapRenderer.platformDefault` corresponds to `AndroidMapRenderer.latest`. + +You are not guaranteed to get the requested renderer. For example, on emulators without +Google Play the latest renderer will not be available and the legacy renderer will always be used. + +WARNING: `AndroidMapRenderer.legacy` is known to crash apps and is no longer supported by the Google Maps team +and therefore cannot be supported by the Flutter team. [1]: https://pub.dev/packages/google_maps_flutter [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index c3a5ae6a88a0..917d45218345 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -10,10 +10,13 @@ import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Point; +import android.graphics.SurfaceTexture; import android.os.Bundle; import android.util.Log; -import android.view.Choreographer; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -135,61 +138,13 @@ private CameraPosition getCameraPosition() { return trackCameraPosition ? googleMap.getCameraPosition() : null; } - private boolean loadedCallbackPending = false; - - /** - * Invalidates the map view after the map has finished rendering. - * - *

gmscore GL renderer uses a {@link android.view.TextureView}. Android platform views that are - * displayed as a texture after Flutter v3.0.0. require that the view hierarchy is notified after - * all drawing operations have been flushed. - * - *

Since the GL renderer doesn't use standard Android views, and instead uses GL directly, we - * notify the view hierarchy by invalidating the view. - * - *

Unfortunately, when {@link GoogleMap.OnMapLoadedCallback} is fired, the texture may not have - * been updated yet. - * - *

To workaround this limitation, wait two frames. This ensures that at least the frame budget - * (16.66ms at 60hz) have passed since the drawing operation was issued. - */ - private void invalidateMapIfNeeded() { - if (googleMap == null || loadedCallbackPending) { - return; - } - loadedCallbackPending = true; - googleMap.setOnMapLoadedCallback( - () -> { - loadedCallbackPending = false; - postFrameCallback( - () -> { - postFrameCallback( - () -> { - if (mapView != null) { - mapView.invalidate(); - } - }); - }); - }); - } - - private static void postFrameCallback(Runnable f) { - Choreographer.getInstance() - .postFrameCallback( - new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - f.run(); - } - }); - } - @Override public void onMapReady(GoogleMap googleMap) { this.googleMap = googleMap; this.googleMap.setIndoorEnabled(this.indoorEnabled); this.googleMap.setTrafficEnabled(this.trafficEnabled); this.googleMap.setBuildingsEnabled(this.buildingsEnabled); + installInvalidator(); googleMap.setOnInfoWindowClickListener(this); if (mapReadyResult != null) { mapReadyResult.success(null); @@ -216,6 +171,71 @@ public void onMapReady(GoogleMap googleMap) { } } + // Returns the first TextureView found in the view hierarchy. + private static TextureView findTextureView(ViewGroup group) { + final int n = group.getChildCount(); + for (int i = 0; i < n; i++) { + View view = group.getChildAt(i); + if (view instanceof TextureView) { + return (TextureView) view; + } + if (view instanceof ViewGroup) { + TextureView r = findTextureView((ViewGroup) view); + if (r != null) { + return r; + } + } + } + return null; + } + + private void installInvalidator() { + if (mapView == null) { + // This should only happen in tests. + return; + } + TextureView textureView = findTextureView(mapView); + if (textureView == null) { + Log.i(TAG, "No TextureView found. Likely using the LEGACY renderer."); + return; + } + Log.i(TAG, "Installing custom TextureView driven invalidator."); + SurfaceTextureListener internalListener = textureView.getSurfaceTextureListener(); + // Override the Maps internal SurfaceTextureListener with our own. Our listener + // mostly just invokes the internal listener callbacks but in onSurfaceTextureUpdated + // the mapView is invalidated which ensures that all map updates are presented to the + // screen. + final MapView mapView = this.mapView; + textureView.setSurfaceTextureListener( + new TextureView.SurfaceTextureListener() { + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + if (internalListener != null) { + internalListener.onSurfaceTextureAvailable(surface, width, height); + } + } + + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (internalListener != null) { + return internalListener.onSurfaceTextureDestroyed(surface); + } + return true; + } + + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + if (internalListener != null) { + internalListener.onSurfaceTextureSizeChanged(surface, width, height); + } + } + + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + if (internalListener != null) { + internalListener.onSurfaceTextureUpdated(surface); + } + mapView.invalidate(); + } + }); + } + @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { switch (call.method) { @@ -309,7 +329,6 @@ public void onSnapshotReady(Bitmap bitmap) { } case "markers#update": { - invalidateMapIfNeeded(); List markersToAdd = call.argument("markersToAdd"); markersController.addMarkers(markersToAdd); List markersToChange = call.argument("markersToChange"); @@ -339,7 +358,6 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polygons#update": { - invalidateMapIfNeeded(); List polygonsToAdd = call.argument("polygonsToAdd"); polygonsController.addPolygons(polygonsToAdd); List polygonsToChange = call.argument("polygonsToChange"); @@ -351,7 +369,6 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polylines#update": { - invalidateMapIfNeeded(); List polylinesToAdd = call.argument("polylinesToAdd"); polylinesController.addPolylines(polylinesToAdd); List polylinesToChange = call.argument("polylinesToChange"); @@ -363,7 +380,6 @@ public void onSnapshotReady(Bitmap bitmap) { } case "circles#update": { - invalidateMapIfNeeded(); List circlesToAdd = call.argument("circlesToAdd"); circlesController.addCircles(circlesToAdd); List circlesToChange = call.argument("circlesToChange"); @@ -443,7 +459,6 @@ public void onSnapshotReady(Bitmap bitmap) { } case "map#setStyle": { - invalidateMapIfNeeded(); boolean mapStyleSet; if (call.arguments instanceof String) { String mapStyle = (String) call.arguments; @@ -466,7 +481,6 @@ public void onSnapshotReady(Bitmap bitmap) { } case "tileOverlays#update": { - invalidateMapIfNeeded(); List> tileOverlaysToAdd = call.argument("tileOverlaysToAdd"); tileOverlaysController.addTileOverlays(tileOverlaysToAdd); List> tileOverlaysToChange = call.argument("tileOverlaysToChange"); @@ -478,7 +492,6 @@ public void onSnapshotReady(Bitmap bitmap) { } case "tileOverlays#clearTileCache": { - invalidateMapIfNeeded(); String tileOverlayId = call.argument("tileOverlayId"); tileOverlaysController.clearTileCache(tileOverlayId); result.success(null); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index 1ebaa361b21c..0b940b9317d7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -7,33 +7,24 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; import android.os.Build; -import android.os.Looper; import androidx.activity.ComponentActivity; import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.MapView; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.Shadows; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) @@ -86,87 +77,6 @@ public void OnDestroyReleaseTheMap() throws InterruptedException { assertNull(googleMapController.getView()); } - @Test - public void InvalidateMapAfterMethodCalls() throws InterruptedException { - String[] methodsThatTriggerInvalidation = { - "markers#update", - "polygons#update", - "polylines#update", - "circles#update", - "map#setStyle", - "tileOverlays#update", - "tileOverlays#clearTileCache" - }; - - for (String methodName : methodsThatTriggerInvalidation) { - googleMapController = - new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null); - googleMapController.init(); - - mockGoogleMap = mock(GoogleMap.class); - googleMapController.onMapReady(mockGoogleMap); - - MethodChannel.Result result = mock(MethodChannel.Result.class); - System.out.println(methodName); - googleMapController.onMethodCall( - new MethodCall(methodName, new HashMap()), result); - - ArgumentCaptor argument = - ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class); - verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture()); - - MapView mapView = mock(MapView.class); - googleMapController.setView(mapView); - - verify(mapView, never()).invalidate(); - argument.getValue().onMapLoaded(); - Shadows.shadowOf(Looper.getMainLooper()).idle(); - verify(mapView).invalidate(); - } - } - - @Test - public void InvalidateMapOnceAfterMethodCall() throws InterruptedException { - googleMapController.onMapReady(mockGoogleMap); - - MethodChannel.Result result = mock(MethodChannel.Result.class); - googleMapController.onMethodCall( - new MethodCall("markers#update", new HashMap()), result); - googleMapController.onMethodCall( - new MethodCall("polygons#update", new HashMap()), result); - - ArgumentCaptor argument = - ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class); - verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture()); - - MapView mapView = mock(MapView.class); - googleMapController.setView(mapView); - - verify(mapView, never()).invalidate(); - argument.getValue().onMapLoaded(); - Shadows.shadowOf(Looper.getMainLooper()).idle(); - verify(mapView).invalidate(); - } - - @Test - public void MethodCalledAfterControllerIsDestroyed() throws InterruptedException { - googleMapController.onMapReady(mockGoogleMap); - MethodChannel.Result result = mock(MethodChannel.Result.class); - googleMapController.onMethodCall( - new MethodCall("markers#update", new HashMap()), result); - - ArgumentCaptor argument = - ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class); - verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture()); - - MapView mapView = mock(MapView.class); - googleMapController.setView(mapView); - googleMapController.onDestroy(activity); - - argument.getValue().onMapLoaded(); - verify(mapView, never()).invalidate(); - } - @Test public void OnMapReadySetsPaddingIfInitialPaddingIsThere() { float padding = 10f; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart index e2864eb3351b..97544420da15 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart @@ -14,6 +14,7 @@ void main() { final GoogleMapsFlutterPlatform mapsImplementation = GoogleMapsFlutterPlatform.instance; if (mapsImplementation is GoogleMapsFlutterAndroid) { + // Force Hybrid Composition mode. mapsImplementation.useAndroidViewSurface = true; } // #enddocregion DisplayMode diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index d5a79f3e4a0c..283b4abd38ce 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -503,8 +503,8 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { /// See https://pub.dev/packages/google_maps_flutter_android#display-mode /// for more information. /// - /// Currently defaults to true, but the default is subject to change. - bool useAndroidViewSurface = true; + /// Currently defaults to false, but the default is subject to change. + bool useAndroidViewSurface = false; /// Requests Google Map Renderer with [AndroidMapRenderer] type. /// @@ -610,6 +610,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { viewType: viewType, onPlatformViewCreated: onPlatformViewCreated, gestureRecognizers: widgetConfiguration.gestureRecognizers, + layoutDirection: widgetConfiguration.textDirection, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index 30c473b2a9f7..1ffb8fbbf08b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.5.3 +version: 2.6.0 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart index a7b1b9590956..1338bdb93d3f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart @@ -158,7 +158,7 @@ void main() { expect(widget, isA()); }); - testWidgets('Defaults to surface view', (WidgetTester tester) async { + testWidgets('Defaults to AndroidView', (WidgetTester tester) async { final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid(); final Widget widget = maps.buildViewWithConfiguration(1, (int _) {}, @@ -167,7 +167,7 @@ void main() { CameraPosition(target: LatLng(0, 0), zoom: 1), textDirection: TextDirection.ltr)); - expect(widget, isA()); + expect(widget, isA()); }); testWidgets('cloudMapId is passed', (WidgetTester tester) async { @@ -177,7 +177,7 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( SystemChannels.platform_views, - (MethodCall methodCall) { + (MethodCall methodCall) async { if (methodCall.method == 'create') { final Map args = Map.from( methodCall.arguments as Map); @@ -198,7 +198,7 @@ void main() { } } } - return null; + return 0; }, );