[Android] Remove overlay when platform views are removed from screen. (#162908)

When there are no more platform views, make sure the overlay layer is
hidden. When a platform view is added again, show the overlay. The
show/hide allows us to avoid continually recreating and destroying the
overlay surface + swapchain when a platform view slides in and out of
frame.

To further reduce memory usage, we could do a delayed de-allocation of
the overlay layer (say after 50 frames of no platform view, destroy it).
But I'm leaving this to a follow up.
This commit is contained in:
Jonah Williams 2025-02-10 18:29:07 -08:00 committed by GitHub
parent 2d39a739ea
commit 7cd9e0f640
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 191 additions and 32 deletions

View File

@ -12,6 +12,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_driver/driver_extension.dart';
import '../platform_view/_shared.dart';
import '../src/allow_list_devices.dart';
void main() async {
@ -27,32 +28,11 @@ void main() async {
// Run on full screen.
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
runApp(const MainApp());
}
final class MainApp extends StatelessWidget {
const MainApp({super.key});
// This should appear as the yellow line over a blue box. The
// green box should not be visible unless the platform view has not loaded yet.
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
SizedBox(width: 190, height: 190, child: ColoredBox(color: Colors.green)),
SizedBox(
width: 200,
height: 200,
child: _HybridCompositionAndroidPlatformView(viewType: 'box_platform_view'),
),
SizedBox(width: 800, height: 25, child: ColoredBox(color: Colors.yellow)),
],
),
);
}
runApp(
const MainApp(
platformView: _HybridCompositionAndroidPlatformView(viewType: 'box_platform_view'),
),
);
}
final class _HybridCompositionAndroidPlatformView extends StatelessWidget {
@ -60,7 +40,6 @@ final class _HybridCompositionAndroidPlatformView extends StatelessWidget {
final String viewType;
// TODO(jonahwilliams): swap this out with new platform view APIs.
@override
Widget build(BuildContext context) {
return PlatformViewLink(

View File

@ -4,18 +4,50 @@
import 'package:flutter/material.dart';
final class MainApp extends StatelessWidget {
// This should appear as the yellow line over a blue box. The
// green box should not be visible unless the platform view has not loaded yet.
final class MainApp extends StatefulWidget {
const MainApp({super.key, required this.platformView});
final Widget platformView;
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
bool showPlatformView = true;
void _togglePlatformView() {
setState(() {
showPlatformView = !showPlatformView;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
platformView,
Center(child: Container(width: 100, height: 100, color: Colors.red)),
TextButton(
key: const ValueKey<String>('AddOverlay'),
onPressed: _togglePlatformView,
child: const SizedBox(width: 190, height: 190, child: ColoredBox(color: Colors.green)),
),
if (showPlatformView) ...<Widget>[
SizedBox(width: 200, height: 200, child: widget.platformView),
TextButton(
key: const ValueKey<String>('RemoveOverlay'),
onPressed: _togglePlatformView,
child: const SizedBox(
width: 800,
height: 25,
child: ColoredBox(color: Colors.yellow),
),
),
],
],
),
);

View File

@ -41,6 +41,8 @@ void main() async {
});
tearDownAll(() async {
await flutterDriver.tap(find.byValueKey('AddOverlay'));
await nativeDriver.close();
await flutterDriver.close();
});
@ -72,4 +74,16 @@ void main() async {
matchesGoldenFile('$goldenPrefix.platform_view_portait_rotated_back.png'),
);
}, timeout: Timeout.none);
// Note: this doesn't reset the app so if additional test cases are added
// make sure to press the button again.
test('should remove overlay when platform view is removed', () async {
await flutterDriver.tap(find.byValueKey('RemoveOverlay'));
await Future<void>.delayed(const Duration(seconds: 1));
await expectLater(
nativeDriver.screenshot(),
matchesGoldenFile('$goldenPrefix.removed_overlay.png'),
);
}, timeout: Timeout.none);
}

View File

@ -45,6 +45,8 @@ void main() async {
});
tearDownAll(() async {
await flutterDriver.tap(find.byValueKey('AddOverlay'));
await nativeDriver.close();
await flutterDriver.close();
});
@ -69,4 +71,14 @@ void main() async {
matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portait_rotated_back.png'),
);
}, timeout: Timeout.none);
test('should hide overlay layer', () async {
await flutterDriver.tap(find.byValueKey('RemoveOverlay'));
await Future<void>.delayed(const Duration(seconds: 1));
await expectLater(
nativeDriver.screenshot(),
matchesGoldenFile('$goldenPrefix.hide_overlay.png'),
);
}, timeout: Timeout.none);
}

View File

@ -56,6 +56,8 @@ void main() async {
});
tearDownAll(() async {
await flutterDriver.tap(find.byValueKey('AddOverlay'));
await nativeDriver.close();
await flutterDriver.close();
});
@ -86,4 +88,14 @@ void main() async {
),
);
}, timeout: Timeout.none);
test('should hide overlay layer', () async {
await flutterDriver.tap(find.byValueKey('RemoveOverlay'));
await Future<void>.delayed(const Duration(seconds: 1));
await expectLater(
nativeDriver.screenshot(),
matchesGoldenFile('$goldenPrefix.hide_overlay.png'),
);
}, timeout: Timeout.none);
}

View File

@ -111,6 +111,8 @@ class MockPlatformViewAndroidJNI : public PlatformViewAndroidJNI {
MutatorsStack mutators_stack),
(override));
MOCK_METHOD(void, onEndFrame2, (), (override));
MOCK_METHOD(void, showOverlaySurface2, (), (override));
MOCK_METHOD(void, hideOverlaySurface2, (), (override));
MOCK_METHOD(std::unique_ptr<std::vector<std::string>>,
FlutterViewComputePlatformResolvedLocale,
(std::vector<std::string> supported_locales_data),

View File

@ -76,10 +76,15 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView(
if (!FrameHasPlatformLayers()) {
frame->Submit();
// If the previous frame had platform views, hide the overlay surface.
if (previous_frame_view_count_ > 0) {
jni_facade_->hideOverlaySurface2();
}
jni_facade_->applyTransaction();
return;
}
bool prev_frame_no_platform_views = previous_frame_view_count_ == 0;
std::unordered_map<int64_t, SkRect> view_rects;
for (auto platform_id : composition_order_) {
view_rects[platform_id] = GetViewRect(platform_id, view_params_);
@ -143,9 +148,13 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView(
task_runners_.GetPlatformTaskRunner()->PostTask(fml::MakeCopyable(
[&, composition_order = composition_order_, view_params = view_params_,
jni_facade = jni_facade_, device_pixel_ratio = device_pixel_ratio_,
slices = std::move(slices_)]() -> void {
slices = std::move(slices_), prev_frame_no_platform_views]() -> void {
jni_facade->swapTransaction();
if (prev_frame_no_platform_views) {
jni_facade_->showOverlaySurface2();
}
for (int64_t view_id : composition_order) {
SkRect view_rect = GetViewRect(view_id, view_params);
const EmbeddedViewParams& params = view_params.at(view_id);

View File

@ -1322,6 +1322,28 @@ public class FlutterJNI {
return platformViewsController2.createOverlaySurface();
}
@SuppressWarnings("unused")
@SuppressLint("NewApi")
@UiThread
public void showOverlaySurface2() {
if (platformViewsController2 == null) {
throw new RuntimeException(
"platformViewsController must be set before attempting to destroy an overlay surface");
}
platformViewsController2.showOverlaySurface();
}
@SuppressWarnings("unused")
@SuppressLint("NewApi")
@UiThread
public void hideOverlaySurface2() {
if (platformViewsController2 == null) {
throw new RuntimeException(
"platformViewsController must be set before attempting to destroy an overlay surface");
}
platformViewsController2.hideOverlaySurface();
}
@SuppressWarnings("unused")
@SuppressLint("NewApi")
@UiThread

View File

@ -7,7 +7,7 @@ package io.flutter.plugin.platform;
import java.util.HashMap;
import java.util.Map;
class PlatformViewRegistryImpl implements PlatformViewRegistry {
public class PlatformViewRegistryImpl implements PlatformViewRegistry {
PlatformViewRegistryImpl() {
viewFactories = new HashMap<>();

View File

@ -65,6 +65,7 @@ public class PlatformViewsController2 implements PlatformViewsAccessibilityDeleg
private final ArrayList<SurfaceControl.Transaction> pendingTransactions;
private final ArrayList<SurfaceControl.Transaction> activeTransactions;
private Surface overlayerSurface = null;
private SurfaceControl overlaySurfaceControl = null;
public PlatformViewsController2() {
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
@ -577,6 +578,7 @@ public class PlatformViewsController2 implements PlatformViewsAccessibilityDeleg
tx.setLayer(surfaceControl, 1000);
tx.apply();
overlayerSurface = new Surface(surfaceControl);
overlaySurfaceControl = surfaceControl;
}
return new FlutterOverlaySurface(0, overlayerSurface);
@ -586,9 +588,32 @@ public class PlatformViewsController2 implements PlatformViewsAccessibilityDeleg
if (overlayerSurface != null) {
overlayerSurface.release();
overlayerSurface = null;
overlaySurfaceControl = null;
}
}
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
public void showOverlaySurface() {
if (overlaySurfaceControl == null) {
return;
}
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
tx.setVisibility(overlaySurfaceControl, /*visible=*/ true);
tx.apply();
}
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
public void hideOverlaySurface() {
if (overlaySurfaceControl == null) {
return;
}
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
tx.setVisibility(overlaySurfaceControl, /*visible=*/ false);
tx.apply();
}
//// Message Handler ///////
private final PlatformViewsChannel2.PlatformViewsHandler channelHandler =

View File

@ -140,6 +140,8 @@ class JNIMock final : public PlatformViewAndroidJNI {
(override));
MOCK_METHOD(void, onEndFrame2, (), (override));
MOCK_METHOD(void, hideOverlaySurface2, (), (override));
MOCK_METHOD(void, showOverlaySurface2, (), (override));
MOCK_METHOD(std::unique_ptr<std::vector<std::string>>,
FlutterViewComputePlatformResolvedLocale,

View File

@ -239,6 +239,10 @@ class PlatformViewAndroidJNI {
int32_t viewHeight,
MutatorsStack mutators_stack) = 0;
virtual void showOverlaySurface2() = 0;
virtual void hideOverlaySurface2() = 0;
//----------------------------------------------------------------------------
/// @brief Computes the locale Android would select.
///

View File

@ -160,6 +160,10 @@ static jmethodID g_on_display_platform_view2_method = nullptr;
static jmethodID g_on_end_frame2_method = nullptr;
static jmethodID g_show_overlay_surface2_method = nullptr;
static jmethodID g_hide_overlay_surface2_method = nullptr;
// Mutators
static fml::jni::ScopedJavaGlobalRef<jclass>* g_mutators_stack_class = nullptr;
static jmethodID g_mutators_stack_init_method = nullptr;
@ -1052,6 +1056,20 @@ bool RegisterApi(JNIEnv* env) {
FML_LOG(ERROR) << "Could not locate onEndFrame2 method";
return false;
}
g_show_overlay_surface2_method = env->GetMethodID(
g_flutter_jni_class->obj(), "showOverlaySurface2", "()V");
if (g_on_end_frame2_method == nullptr) {
FML_LOG(ERROR) << "Could not locate showOverlaySurface2 method";
return false;
}
g_hide_overlay_surface2_method = env->GetMethodID(
g_flutter_jni_class->obj(), "hideOverlaySurface2", "()V");
if (g_on_end_frame2_method == nullptr) {
FML_LOG(ERROR) << "Could not locate hideOverlaySurface2 method";
return false;
}
//
fml::jni::ScopedJavaLocalRef<jclass> overlay_surface_class(
@ -2184,4 +2202,28 @@ void PlatformViewAndroidJNIImpl::onEndFrame2() {
FML_CHECK(fml::jni::CheckException(env));
}
void PlatformViewAndroidJNIImpl::showOverlaySurface2() {
JNIEnv* env = fml::jni::AttachCurrentThread();
auto java_object = java_object_.get(env);
if (java_object.is_null()) {
return;
}
env->CallVoidMethod(java_object.obj(), g_show_overlay_surface2_method);
FML_CHECK(fml::jni::CheckException(env));
}
void PlatformViewAndroidJNIImpl::hideOverlaySurface2() {
JNIEnv* env = fml::jni::AttachCurrentThread();
auto java_object = java_object_.get(env);
if (java_object.is_null()) {
return;
}
env->CallVoidMethod(java_object.obj(), g_hide_overlay_surface2_method);
FML_CHECK(fml::jni::CheckException(env));
}
} // namespace flutter

View File

@ -124,6 +124,10 @@ class PlatformViewAndroidJNIImpl final : public PlatformViewAndroidJNI {
int32_t viewHeight,
MutatorsStack mutators_stack) override;
void showOverlaySurface2() override;
void hideOverlaySurface2() override;
void onEndFrame2() override;
private: