Skip to content

Commit

Permalink
Fix Google Maps rendering issues in TLHC mode when using LATEST renderer
Browse files Browse the repository at this point in the history
The Google Maps LATEST renderer always uses a TextureView. We can use a signal
from this TextureView to perform view invalidation that fixes the rendering
glitches (missing updates) in TLHC mode.

NOTE: We have an internal bug 311013682 requesting an official way of achieving
this functionality but if the bug is ever acted on it will take many months/years
before we can rely on this functionality.

In the meantime, chain the internal SurfaceTextureListener with our own and
piggyback on the OnSurfaceTextureUpdated callback to invalidate the MapView.

Fixes flutter/flutter#103686

Tested on an emulator and a physical device (Pixel 6 Pro).
  • Loading branch information
johnmccutchan committed Nov 16, 2023
1 parent 0cd2378 commit 2fef5b7
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 16 deletions.
@@ -1,3 +1,8 @@
## 2.5.4

* Fix missing updates in TLHC mode.
* Switch default display mode to TLHC mode.

## 2.5.3

* Updates `com.google.android.gms:play-services-maps` to 18.2.0.
Expand Down
30 changes: 18 additions & 12 deletions packages/google_maps_flutter/google_maps_flutter_android/README.md
Expand Up @@ -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

Expand All @@ -70,8 +70,14 @@ 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.
NOTE: `AndroidMapRenderer.platformDefault` corresponds to `AndroidMapRenderer.latest`.

WARNING: `AndroidMapRenderer.legacy` is known to crash apps and is no longer supported.

NOTE: Getting the requested renderer as a response is not guaranteed.

NOTE: On emulators without Google Play the latest renderer will not be available and the legacy
renderer will always be used.

[1]: https://pub.dev/packages/google_maps_flutter
[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
Expand Down
Expand Up @@ -10,10 +10,14 @@
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;
Expand Down Expand Up @@ -190,6 +194,7 @@ public void onMapReady(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);
Expand All @@ -216,6 +221,70 @@ public void onMapReady(GoogleMap googleMap) {
}
}

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) {
Expand Down
Expand Up @@ -504,7 +504,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
/// for more information.
///
/// Currently defaults to true, but the default is subject to change.
bool useAndroidViewSurface = true;
bool useAndroidViewSurface = false;

/// Requests Google Map Renderer with [AndroidMapRenderer] type.
///
Expand Down
Expand Up @@ -2,7 +2,7 @@ name: google_maps_flutter_android
description: Android implementation of the google_maps_flutter plugin.
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
version: 2.5.3
version: 2.5.4

environment:
sdk: ">=2.19.0 <4.0.0"
Expand Down
Expand Up @@ -158,7 +158,7 @@ void main() {
expect(widget, isA<PlatformViewLink>());
});

testWidgets('Defaults to surface view', (WidgetTester tester) async {
testWidgets('Defaults to AndroidView', (WidgetTester tester) async {
final GoogleMapsFlutterAndroid maps = GoogleMapsFlutterAndroid();

final Widget widget = maps.buildViewWithConfiguration(1, (int _) {},
Expand All @@ -167,7 +167,7 @@ void main() {
CameraPosition(target: LatLng(0, 0), zoom: 1),
textDirection: TextDirection.ltr));

expect(widget, isA<PlatformViewLink>());
expect(widget, isA<AndroidView>());
});

testWidgets('cloudMapId is passed', (WidgetTester tester) async {
Expand Down

0 comments on commit 2fef5b7

Please sign in to comment.