diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 57d24e2196a5..b4fb6622edc2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.1.6 +* Fixes issue in Flutter v3.0.0 where some updates to the map don't take effect on Android. * Fixes iOS native unit tests on M1 devices. * Minor fixes for new analysis options. 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 9b8810354b8f..2c2287cf59d4 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 @@ -12,9 +12,11 @@ import android.graphics.Point; import android.os.Bundle; import android.util.Log; +import android.view.Choreographer; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; @@ -109,6 +111,11 @@ public View getView() { return mapView; } + @VisibleForTesting + /*package*/ void setView(MapView view) { + mapView = view; + } + void init() { lifecycleProvider.getLifecycle().addObserver(this); mapView.getMapAsync(this); @@ -126,6 +133,58 @@ 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( + new GoogleMap.OnMapLoadedCallback() { + @Override + public void onMapLoaded() { + 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; @@ -244,6 +303,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "markers#update": { + invalidateMapIfNeeded(); List markersToAdd = call.argument("markersToAdd"); markersController.addMarkers(markersToAdd); List markersToChange = call.argument("markersToChange"); @@ -273,6 +333,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polygons#update": { + invalidateMapIfNeeded(); List polygonsToAdd = call.argument("polygonsToAdd"); polygonsController.addPolygons(polygonsToAdd); List polygonsToChange = call.argument("polygonsToChange"); @@ -284,6 +345,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polylines#update": { + invalidateMapIfNeeded(); List polylinesToAdd = call.argument("polylinesToAdd"); polylinesController.addPolylines(polylinesToAdd); List polylinesToChange = call.argument("polylinesToChange"); @@ -295,6 +357,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "circles#update": { + invalidateMapIfNeeded(); List circlesToAdd = call.argument("circlesToAdd"); circlesController.addCircles(circlesToAdd); List circlesToChange = call.argument("circlesToChange"); @@ -374,12 +437,17 @@ public void onSnapshotReady(Bitmap bitmap) { } case "map#setStyle": { - String mapStyle = (String) call.arguments; + invalidateMapIfNeeded(); boolean mapStyleSet; - if (mapStyle == null) { - mapStyleSet = googleMap.setMapStyle(null); + if (call.arguments instanceof String) { + String mapStyle = (String) call.arguments; + if (mapStyle == null) { + mapStyleSet = googleMap.setMapStyle(null); + } else { + mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle)); + } } else { - mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle)); + mapStyleSet = googleMap.setMapStyle(null); } ArrayList mapStyleResult = new ArrayList<>(2); mapStyleResult.add(mapStyleSet); @@ -392,6 +460,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "tileOverlays#update": { + invalidateMapIfNeeded(); List> tileOverlaysToAdd = call.argument("tileOverlaysToAdd"); tileOverlaysController.addTileOverlays(tileOverlaysToAdd); List> tileOverlaysToChange = call.argument("tileOverlaysToChange"); @@ -403,6 +472,7 @@ 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/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index 6bda085caf46..d8082b57e3db 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -6,16 +6,24 @@ 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.verify; import android.content.Context; import android.os.Build; 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.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; @@ -58,4 +66,83 @@ public void OnDestroyReleaseTheMap() throws InterruptedException { googleMapController.onDestroy(activity); 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(); + 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(); + 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(); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 831f3ccd2963..59ee23d0b260 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.1.5 +version: 2.1.6 environment: sdk: ">=2.14.0 <3.0.0"