diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md
index 2fe148c5090d..3b4a2f4c4381 100644
--- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md
+++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md
@@ -1,5 +1,7 @@
-## NEXT
+## 2.6.0
+* Fixes missing updates in TLHC mode.
+* Switched default display mode to TLHC mode.
* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0.
## 2.5.3
diff --git a/packages/google_maps_flutter/google_maps_flutter_android/README.md b/packages/google_maps_flutter/google_maps_flutter_android/README.md
index 836ab2c9835a..8c482864dfdf 100644
--- a/packages/google_maps_flutter/google_maps_flutter_android/README.md
+++ b/packages/google_maps_flutter/google_maps_flutter_android/README.md
@@ -30,26 +30,26 @@ void main() {
final GoogleMapsFlutterPlatform mapsImplementation =
GoogleMapsFlutterPlatform.instance;
if (mapsImplementation is GoogleMapsFlutterAndroid) {
+ // Force Hybrid Composition mode.
mapsImplementation.useAndroidViewSurface = true;
}
// ยทยทยท
}
```
-### Hybrid Composition
+### Texture Layer Hybrid Composition
-This is the current default mode, and corresponds to
-`useAndroidViewSurface = true`. It ensures that the map display will work as
-expected, at the cost of some performance.
+This is the the current default mode and corresponds to `useAndroidViewSurface = false`.
+This mode is more performant than Hybrid Composition and we recommend that you use this mode.
-### Texture Layer Hybrid Composition
+### Hybrid Composition
-This is a new display mode used by most plugins starting with Flutter 3.0, and
-corresponds to `useAndroidViewSurface = false`. This is more performant than
-Hybrid Composition, but currently [misses certain map updates][4].
+This mode is available for backwards compatability and corresponds to `useAndroidViewSurface = true`.
+We do not recommend its use as it is less performant than Texture Layer Hybrid Composition and
+certain flutter rendering effects are not supported.
-This mode will likely become the default in future versions if/when the
-missed updates issue can be resolved.
+If you require this mode for correctness, please file a bug so we can investigate and fix
+the issue in the TLHC mode.
## Map renderer
@@ -70,8 +70,13 @@ AndroidMapRenderer mapRenderer = AndroidMapRenderer.platformDefault;
}
```
-Available values are `AndroidMapRenderer.latest`, `AndroidMapRenderer.legacy`, `AndroidMapRenderer.platformDefault`.
-Note that getting the requested renderer as a response is not guaranteed.
+`AndroidMapRenderer.platformDefault` corresponds to `AndroidMapRenderer.latest`.
+
+You are not guaranteed to get the requested renderer. For example, on emulators without
+Google Play the latest renderer will not be available and the legacy renderer will always be used.
+
+WARNING: `AndroidMapRenderer.legacy` is known to crash apps and is no longer supported by the Google Maps team
+and therefore cannot be supported by the Flutter team.
[1]: https://pub.dev/packages/google_maps_flutter
[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
index c3a5ae6a88a0..917d45218345 100644
--- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
+++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -10,10 +10,13 @@
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Point;
+import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.util.Log;
-import android.view.Choreographer;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -135,61 +138,13 @@ 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(
- () -> {
- 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;
this.googleMap.setIndoorEnabled(this.indoorEnabled);
this.googleMap.setTrafficEnabled(this.trafficEnabled);
this.googleMap.setBuildingsEnabled(this.buildingsEnabled);
+ installInvalidator();
googleMap.setOnInfoWindowClickListener(this);
if (mapReadyResult != null) {
mapReadyResult.success(null);
@@ -216,6 +171,71 @@ public void onMapReady(GoogleMap googleMap) {
}
}
+ // Returns the first TextureView found in the view hierarchy.
+ private static TextureView findTextureView(ViewGroup group) {
+ final int n = group.getChildCount();
+ for (int i = 0; i < n; i++) {
+ View view = group.getChildAt(i);
+ if (view instanceof TextureView) {
+ return (TextureView) view;
+ }
+ if (view instanceof ViewGroup) {
+ TextureView r = findTextureView((ViewGroup) view);
+ if (r != null) {
+ return r;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void installInvalidator() {
+ if (mapView == null) {
+ // This should only happen in tests.
+ return;
+ }
+ TextureView textureView = findTextureView(mapView);
+ if (textureView == null) {
+ Log.i(TAG, "No TextureView found. Likely using the LEGACY renderer.");
+ return;
+ }
+ Log.i(TAG, "Installing custom TextureView driven invalidator.");
+ SurfaceTextureListener internalListener = textureView.getSurfaceTextureListener();
+ // Override the Maps internal SurfaceTextureListener with our own. Our listener
+ // mostly just invokes the internal listener callbacks but in onSurfaceTextureUpdated
+ // the mapView is invalidated which ensures that all map updates are presented to the
+ // screen.
+ final MapView mapView = this.mapView;
+ textureView.setSurfaceTextureListener(
+ new TextureView.SurfaceTextureListener() {
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ if (internalListener != null) {
+ internalListener.onSurfaceTextureAvailable(surface, width, height);
+ }
+ }
+
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ if (internalListener != null) {
+ return internalListener.onSurfaceTextureDestroyed(surface);
+ }
+ return true;
+ }
+
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ if (internalListener != null) {
+ internalListener.onSurfaceTextureSizeChanged(surface, width, height);
+ }
+ }
+
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ if (internalListener != null) {
+ internalListener.onSurfaceTextureUpdated(surface);
+ }
+ mapView.invalidate();
+ }
+ });
+ }
+
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method) {
@@ -309,7 +329,6 @@ public void onSnapshotReady(Bitmap bitmap) {
}
case "markers#update":
{
- invalidateMapIfNeeded();
List