Skip to content

Commit

Permalink
Fix issue where map updates don't take effect in Flutter v3.0.0 (flut…
Browse files Browse the repository at this point in the history
  • Loading branch information
Emmanuel Garcia authored and yutaaraki-toydium committed Jun 4, 2022
1 parent 1b77b47 commit 5c8e8b0
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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.
*
* <p>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.
*
* <p>Since the GL renderer doesn't use standard Android views, and instead uses GL directly, we
* notify the view hierarchy by invalidating the view.
*
* <p>Unfortunately, when {@link GoogleMap.OnMapLoadedCallback} is fired, the texture may not have
* been updated yet.
*
* <p>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;
Expand Down Expand Up @@ -244,6 +303,7 @@ public void onSnapshotReady(Bitmap bitmap) {
}
case "markers#update":
{
invalidateMapIfNeeded();
List<Object> markersToAdd = call.argument("markersToAdd");
markersController.addMarkers(markersToAdd);
List<Object> markersToChange = call.argument("markersToChange");
Expand Down Expand Up @@ -273,6 +333,7 @@ public void onSnapshotReady(Bitmap bitmap) {
}
case "polygons#update":
{
invalidateMapIfNeeded();
List<Object> polygonsToAdd = call.argument("polygonsToAdd");
polygonsController.addPolygons(polygonsToAdd);
List<Object> polygonsToChange = call.argument("polygonsToChange");
Expand All @@ -284,6 +345,7 @@ public void onSnapshotReady(Bitmap bitmap) {
}
case "polylines#update":
{
invalidateMapIfNeeded();
List<Object> polylinesToAdd = call.argument("polylinesToAdd");
polylinesController.addPolylines(polylinesToAdd);
List<Object> polylinesToChange = call.argument("polylinesToChange");
Expand All @@ -295,6 +357,7 @@ public void onSnapshotReady(Bitmap bitmap) {
}
case "circles#update":
{
invalidateMapIfNeeded();
List<Object> circlesToAdd = call.argument("circlesToAdd");
circlesController.addCircles(circlesToAdd);
List<Object> circlesToChange = call.argument("circlesToChange");
Expand Down Expand Up @@ -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<Object> mapStyleResult = new ArrayList<>(2);
mapStyleResult.add(mapStyleSet);
Expand All @@ -392,6 +460,7 @@ public void onSnapshotReady(Bitmap bitmap) {
}
case "tileOverlays#update":
{
invalidateMapIfNeeded();
List<Map<String, ?>> tileOverlaysToAdd = call.argument("tileOverlaysToAdd");
tileOverlaysController.addTileOverlays(tileOverlaysToAdd);
List<Map<String, ?>> tileOverlaysToChange = call.argument("tileOverlaysToChange");
Expand All @@ -403,6 +472,7 @@ public void onSnapshotReady(Bitmap bitmap) {
}
case "tileOverlays#clearTileCache":
{
invalidateMapIfNeeded();
String tileOverlayId = call.argument("tileOverlayId");
tileOverlaysController.clearTileCache(tileOverlayId);
result.success(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object>()), result);

ArgumentCaptor<GoogleMap.OnMapLoadedCallback> 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<String, Object>()), result);
googleMapController.onMethodCall(
new MethodCall("polygons#update", new HashMap<String, Object>()), result);

ArgumentCaptor<GoogleMap.OnMapLoadedCallback> 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<String, Object>()), result);

ArgumentCaptor<GoogleMap.OnMapLoadedCallback> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 5c8e8b0

Please sign in to comment.