Skip to content

Commit 0bcfc74

Browse files
Emmanuel Garciamauricioluz
authored andcommitted
Fix issue where map updates don't take effect in Flutter v3.0.0 (flutter#5787)
1 parent a0bf7e3 commit 0bcfc74

File tree

4 files changed

+164
-6
lines changed

4 files changed

+164
-6
lines changed

packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 2.1.6
22

3+
* Fixes issue in Flutter v3.0.0 where some updates to the map don't take effect on Android.
34
* Fixes iOS native unit tests on M1 devices.
45
* Minor fixes for new analysis options.
56

packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import android.graphics.Point;
1313
import android.os.Bundle;
1414
import android.util.Log;
15+
import android.view.Choreographer;
1516
import android.view.View;
1617
import androidx.annotation.NonNull;
1718
import androidx.annotation.Nullable;
19+
import androidx.annotation.VisibleForTesting;
1820
import androidx.lifecycle.DefaultLifecycleObserver;
1921
import androidx.lifecycle.Lifecycle;
2022
import androidx.lifecycle.LifecycleOwner;
@@ -105,6 +107,11 @@ public View getView() {
105107
return mapView;
106108
}
107109

110+
@VisibleForTesting
111+
/*package*/ void setView(MapView view) {
112+
mapView = view;
113+
}
114+
108115
void init() {
109116
lifecycleProvider.getLifecycle().addObserver(this);
110117
mapView.getMapAsync(this);
@@ -122,6 +129,58 @@ private CameraPosition getCameraPosition() {
122129
return trackCameraPosition ? googleMap.getCameraPosition() : null;
123130
}
124131

132+
private boolean loadedCallbackPending = false;
133+
134+
/**
135+
* Invalidates the map view after the map has finished rendering.
136+
*
137+
* <p>gmscore GL renderer uses a {@link android.view.TextureView}. Android platform views that are
138+
* displayed as a texture after Flutter v3.0.0. require that the view hierarchy is notified after
139+
* all drawing operations have been flushed.
140+
*
141+
* <p>Since the GL renderer doesn't use standard Android views, and instead uses GL directly, we
142+
* notify the view hierarchy by invalidating the view.
143+
*
144+
* <p>Unfortunately, when {@link GoogleMap.OnMapLoadedCallback} is fired, the texture may not have
145+
* been updated yet.
146+
*
147+
* <p>To workaround this limitation, wait two frames. This ensures that at least the frame budget
148+
* (16.66ms at 60hz) have passed since the drawing operation was issued.
149+
*/
150+
private void invalidateMapIfNeeded() {
151+
if (googleMap == null || loadedCallbackPending) {
152+
return;
153+
}
154+
loadedCallbackPending = true;
155+
googleMap.setOnMapLoadedCallback(
156+
new GoogleMap.OnMapLoadedCallback() {
157+
@Override
158+
public void onMapLoaded() {
159+
loadedCallbackPending = false;
160+
postFrameCallback(
161+
() -> {
162+
postFrameCallback(
163+
() -> {
164+
if (mapView != null) {
165+
mapView.invalidate();
166+
}
167+
});
168+
});
169+
}
170+
});
171+
}
172+
173+
private static void postFrameCallback(Runnable f) {
174+
Choreographer.getInstance()
175+
.postFrameCallback(
176+
new Choreographer.FrameCallback() {
177+
@Override
178+
public void doFrame(long frameTimeNanos) {
179+
f.run();
180+
}
181+
});
182+
}
183+
125184
@Override
126185
public void onMapReady(GoogleMap googleMap) {
127186
this.googleMap = googleMap;
@@ -242,6 +301,7 @@ public void onSnapshotReady(Bitmap bitmap) {
242301
}
243302
case "markers#update":
244303
{
304+
invalidateMapIfNeeded();
245305
List<Object> markersToAdd = call.argument("markersToAdd");
246306
markersController.addMarkers(markersToAdd);
247307
List<Object> markersToChange = call.argument("markersToChange");
@@ -271,6 +331,7 @@ public void onSnapshotReady(Bitmap bitmap) {
271331
}
272332
case "polygons#update":
273333
{
334+
invalidateMapIfNeeded();
274335
List<Object> polygonsToAdd = call.argument("polygonsToAdd");
275336
polygonsController.addPolygons(polygonsToAdd);
276337
List<Object> polygonsToChange = call.argument("polygonsToChange");
@@ -282,6 +343,7 @@ public void onSnapshotReady(Bitmap bitmap) {
282343
}
283344
case "polylines#update":
284345
{
346+
invalidateMapIfNeeded();
285347
List<Object> polylinesToAdd = call.argument("polylinesToAdd");
286348
polylinesController.addPolylines(polylinesToAdd);
287349
List<Object> polylinesToChange = call.argument("polylinesToChange");
@@ -293,6 +355,7 @@ public void onSnapshotReady(Bitmap bitmap) {
293355
}
294356
case "circles#update":
295357
{
358+
invalidateMapIfNeeded();
296359
List<Object> circlesToAdd = call.argument("circlesToAdd");
297360
circlesController.addCircles(circlesToAdd);
298361
List<Object> circlesToChange = call.argument("circlesToChange");
@@ -383,12 +446,17 @@ public void onSnapshotReady(Bitmap bitmap) {
383446
}
384447
case "map#setStyle":
385448
{
386-
String mapStyle = (String) call.arguments;
449+
invalidateMapIfNeeded();
387450
boolean mapStyleSet;
388-
if (mapStyle == null) {
389-
mapStyleSet = googleMap.setMapStyle(null);
451+
if (call.arguments instanceof String) {
452+
String mapStyle = (String) call.arguments;
453+
if (mapStyle == null) {
454+
mapStyleSet = googleMap.setMapStyle(null);
455+
} else {
456+
mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle));
457+
}
390458
} else {
391-
mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle));
459+
mapStyleSet = googleMap.setMapStyle(null);
392460
}
393461
ArrayList<Object> mapStyleResult = new ArrayList<>(2);
394462
mapStyleResult.add(mapStyleSet);
@@ -401,6 +469,7 @@ public void onSnapshotReady(Bitmap bitmap) {
401469
}
402470
case "tileOverlays#update":
403471
{
472+
invalidateMapIfNeeded();
404473
List<Map<String, ?>> tileOverlaysToAdd = call.argument("tileOverlaysToAdd");
405474
tileOverlaysController.addTileOverlays(tileOverlaysToAdd);
406475
List<Map<String, ?>> tileOverlaysToChange = call.argument("tileOverlaysToChange");
@@ -412,6 +481,7 @@ public void onSnapshotReady(Bitmap bitmap) {
412481
}
413482
case "tileOverlays#clearTileCache":
414483
{
484+
invalidateMapIfNeeded();
415485
String tileOverlayId = call.argument("tileOverlayId");
416486
tileOverlaysController.clearTileCache(tileOverlayId);
417487
result.success(null);

packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,24 @@
66

77
import static org.junit.Assert.assertNull;
88
import static org.junit.Assert.assertTrue;
9+
import static org.mockito.Mockito.mock;
10+
import static org.mockito.Mockito.never;
11+
import static org.mockito.Mockito.verify;
912

1013
import android.content.Context;
1114
import android.os.Build;
1215
import androidx.activity.ComponentActivity;
1316
import androidx.test.core.app.ApplicationProvider;
1417
import com.google.android.gms.maps.GoogleMap;
18+
import com.google.android.gms.maps.MapView;
1519
import io.flutter.plugin.common.BinaryMessenger;
20+
import io.flutter.plugin.common.MethodCall;
21+
import io.flutter.plugin.common.MethodChannel;
22+
import java.util.HashMap;
1623
import org.junit.Before;
1724
import org.junit.Test;
1825
import org.junit.runner.RunWith;
26+
import org.mockito.ArgumentCaptor;
1927
import org.mockito.Mock;
2028
import org.mockito.MockitoAnnotations;
2129
import org.robolectric.Robolectric;
@@ -58,4 +66,83 @@ public void OnDestroyReleaseTheMap() throws InterruptedException {
5866
googleMapController.onDestroy(activity);
5967
assertNull(googleMapController.getView());
6068
}
69+
70+
@Test
71+
public void InvalidateMapAfterMethodCalls() throws InterruptedException {
72+
String[] methodsThatTriggerInvalidation = {
73+
"markers#update",
74+
"polygons#update",
75+
"polylines#update",
76+
"circles#update",
77+
"map#setStyle",
78+
"tileOverlays#update",
79+
"tileOverlays#clearTileCache"
80+
};
81+
82+
for (String methodName : methodsThatTriggerInvalidation) {
83+
googleMapController =
84+
new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null);
85+
googleMapController.init();
86+
87+
mockGoogleMap = mock(GoogleMap.class);
88+
googleMapController.onMapReady(mockGoogleMap);
89+
90+
MethodChannel.Result result = mock(MethodChannel.Result.class);
91+
System.out.println(methodName);
92+
googleMapController.onMethodCall(
93+
new MethodCall(methodName, new HashMap<String, Object>()), result);
94+
95+
ArgumentCaptor<GoogleMap.OnMapLoadedCallback> argument =
96+
ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class);
97+
verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture());
98+
99+
MapView mapView = mock(MapView.class);
100+
googleMapController.setView(mapView);
101+
102+
verify(mapView, never()).invalidate();
103+
argument.getValue().onMapLoaded();
104+
verify(mapView).invalidate();
105+
}
106+
}
107+
108+
@Test
109+
public void InvalidateMapOnceAfterMethodCall() throws InterruptedException {
110+
googleMapController.onMapReady(mockGoogleMap);
111+
112+
MethodChannel.Result result = mock(MethodChannel.Result.class);
113+
googleMapController.onMethodCall(
114+
new MethodCall("markers#update", new HashMap<String, Object>()), result);
115+
googleMapController.onMethodCall(
116+
new MethodCall("polygons#update", new HashMap<String, Object>()), result);
117+
118+
ArgumentCaptor<GoogleMap.OnMapLoadedCallback> argument =
119+
ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class);
120+
verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture());
121+
122+
MapView mapView = mock(MapView.class);
123+
googleMapController.setView(mapView);
124+
125+
verify(mapView, never()).invalidate();
126+
argument.getValue().onMapLoaded();
127+
verify(mapView).invalidate();
128+
}
129+
130+
@Test
131+
public void MethodCalledAfterControllerIsDestroyed() throws InterruptedException {
132+
googleMapController.onMapReady(mockGoogleMap);
133+
MethodChannel.Result result = mock(MethodChannel.Result.class);
134+
googleMapController.onMethodCall(
135+
new MethodCall("markers#update", new HashMap<String, Object>()), result);
136+
137+
ArgumentCaptor<GoogleMap.OnMapLoadedCallback> argument =
138+
ArgumentCaptor.forClass(GoogleMap.OnMapLoadedCallback.class);
139+
verify(mockGoogleMap).setOnMapLoadedCallback(argument.capture());
140+
141+
MapView mapView = mock(MapView.class);
142+
googleMapController.setView(mapView);
143+
googleMapController.onDestroy(activity);
144+
145+
argument.getValue().onMapLoaded();
146+
verify(mapView, never()).invalidate();
147+
}
61148
}

packages/google_maps_flutter/google_maps_flutter/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: google_maps_flutter
22
description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
33
repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
5-
version: 2.1.5
5+
version: 2.1.6
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"

0 commit comments

Comments
 (0)