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

Commit 5942a85

Browse files
author
Emmanuel Garcia
authored
Fix issue where map updates don't take effect in Flutter v3.0.0 (#5787)
1 parent c3696c1 commit 5942a85

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;
@@ -109,6 +111,11 @@ public View getView() {
109111
return mapView;
110112
}
111113

114+
@VisibleForTesting
115+
/*package*/ void setView(MapView view) {
116+
mapView = view;
117+
}
118+
112119
void init() {
113120
lifecycleProvider.getLifecycle().addObserver(this);
114121
mapView.getMapAsync(this);
@@ -126,6 +133,58 @@ private CameraPosition getCameraPosition() {
126133
return trackCameraPosition ? googleMap.getCameraPosition() : null;
127134
}
128135

136+
private boolean loadedCallbackPending = false;
137+
138+
/**
139+
* Invalidates the map view after the map has finished rendering.
140+
*
141+
* <p>gmscore GL renderer uses a {@link android.view.TextureView}. Android platform views that are
142+
* displayed as a texture after Flutter v3.0.0. require that the view hierarchy is notified after
143+
* all drawing operations have been flushed.
144+
*
145+
* <p>Since the GL renderer doesn't use standard Android views, and instead uses GL directly, we
146+
* notify the view hierarchy by invalidating the view.
147+
*
148+
* <p>Unfortunately, when {@link GoogleMap.OnMapLoadedCallback} is fired, the texture may not have
149+
* been updated yet.
150+
*
151+
* <p>To workaround this limitation, wait two frames. This ensures that at least the frame budget
152+
* (16.66ms at 60hz) have passed since the drawing operation was issued.
153+
*/
154+
private void invalidateMapIfNeeded() {
155+
if (googleMap == null || loadedCallbackPending) {
156+
return;
157+
}
158+
loadedCallbackPending = true;
159+
googleMap.setOnMapLoadedCallback(
160+
new GoogleMap.OnMapLoadedCallback() {
161+
@Override
162+
public void onMapLoaded() {
163+
loadedCallbackPending = false;
164+
postFrameCallback(
165+
() -> {
166+
postFrameCallback(
167+
() -> {
168+
if (mapView != null) {
169+
mapView.invalidate();
170+
}
171+
});
172+
});
173+
}
174+
});
175+
}
176+
177+
private static void postFrameCallback(Runnable f) {
178+
Choreographer.getInstance()
179+
.postFrameCallback(
180+
new Choreographer.FrameCallback() {
181+
@Override
182+
public void doFrame(long frameTimeNanos) {
183+
f.run();
184+
}
185+
});
186+
}
187+
129188
@Override
130189
public void onMapReady(GoogleMap googleMap) {
131190
this.googleMap = googleMap;
@@ -244,6 +303,7 @@ public void onSnapshotReady(Bitmap bitmap) {
244303
}
245304
case "markers#update":
246305
{
306+
invalidateMapIfNeeded();
247307
List<Object> markersToAdd = call.argument("markersToAdd");
248308
markersController.addMarkers(markersToAdd);
249309
List<Object> markersToChange = call.argument("markersToChange");
@@ -273,6 +333,7 @@ public void onSnapshotReady(Bitmap bitmap) {
273333
}
274334
case "polygons#update":
275335
{
336+
invalidateMapIfNeeded();
276337
List<Object> polygonsToAdd = call.argument("polygonsToAdd");
277338
polygonsController.addPolygons(polygonsToAdd);
278339
List<Object> polygonsToChange = call.argument("polygonsToChange");
@@ -284,6 +345,7 @@ public void onSnapshotReady(Bitmap bitmap) {
284345
}
285346
case "polylines#update":
286347
{
348+
invalidateMapIfNeeded();
287349
List<Object> polylinesToAdd = call.argument("polylinesToAdd");
288350
polylinesController.addPolylines(polylinesToAdd);
289351
List<Object> polylinesToChange = call.argument("polylinesToChange");
@@ -295,6 +357,7 @@ public void onSnapshotReady(Bitmap bitmap) {
295357
}
296358
case "circles#update":
297359
{
360+
invalidateMapIfNeeded();
298361
List<Object> circlesToAdd = call.argument("circlesToAdd");
299362
circlesController.addCircles(circlesToAdd);
300363
List<Object> circlesToChange = call.argument("circlesToChange");
@@ -374,12 +437,17 @@ public void onSnapshotReady(Bitmap bitmap) {
374437
}
375438
case "map#setStyle":
376439
{
377-
String mapStyle = (String) call.arguments;
440+
invalidateMapIfNeeded();
378441
boolean mapStyleSet;
379-
if (mapStyle == null) {
380-
mapStyleSet = googleMap.setMapStyle(null);
442+
if (call.arguments instanceof String) {
443+
String mapStyle = (String) call.arguments;
444+
if (mapStyle == null) {
445+
mapStyleSet = googleMap.setMapStyle(null);
446+
} else {
447+
mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle));
448+
}
381449
} else {
382-
mapStyleSet = googleMap.setMapStyle(new MapStyleOptions(mapStyle));
450+
mapStyleSet = googleMap.setMapStyle(null);
383451
}
384452
ArrayList<Object> mapStyleResult = new ArrayList<>(2);
385453
mapStyleResult.add(mapStyleSet);
@@ -392,6 +460,7 @@ public void onSnapshotReady(Bitmap bitmap) {
392460
}
393461
case "tileOverlays#update":
394462
{
463+
invalidateMapIfNeeded();
395464
List<Map<String, ?>> tileOverlaysToAdd = call.argument("tileOverlaysToAdd");
396465
tileOverlaysController.addTileOverlays(tileOverlaysToAdd);
397466
List<Map<String, ?>> tileOverlaysToChange = call.argument("tileOverlaysToChange");
@@ -403,6 +472,7 @@ public void onSnapshotReady(Bitmap bitmap) {
403472
}
404473
case "tileOverlays#clearTileCache":
405474
{
475+
invalidateMapIfNeeded();
406476
String tileOverlayId = call.argument("tileOverlayId");
407477
tileOverlaysController.clearTileCache(tileOverlayId);
408478
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)