Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Fix issue where map updates don't take effect in Flutter v3.0.0 #5787

Merged
merged 12 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
blasten marked this conversation as resolved.
Show resolved Hide resolved
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