Skip to content

Commit 26bb33b

Browse files
gmackallGray Mackall
andauthored
[HCPP] Clean up overlay layer when last frame had overlay content and current doesn't (flutter#173881)
Fixes flutter#173635 My current understanding of the issue is that we are never pushing a new empty frame for this layer, so the texture gets stuck as shown in the bug. If we keep track of when we had content last frame, and don't in the current frame, we can push a single empty frame for that layer when needed. See example in flutter#173635 (comment) ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Gray Mackall <[email protected]>
1 parent 7130981 commit 26bb33b

File tree

4 files changed

+216
-12
lines changed

4 files changed

+216
-12
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:android_driver_extensions/extension.dart';
8+
import 'package:flutter/foundation.dart';
9+
import 'package:flutter/gestures.dart';
10+
import 'package:flutter/material.dart';
11+
import 'package:flutter/rendering.dart';
12+
import 'package:flutter/services.dart';
13+
import 'package:flutter_driver/driver_extension.dart';
14+
15+
import '../src/allow_list_devices.dart';
16+
17+
void main() {
18+
ensureAndroidDevice();
19+
enableFlutterDriverExtension(
20+
handler: (String? command) async {
21+
return json.encode(<String, Object?>{
22+
'supported': await HybridAndroidViewController.checkIfSupported(),
23+
});
24+
},
25+
commands: <CommandExtension>[nativeDriverCommands],
26+
);
27+
28+
runApp(const MyApp());
29+
}
30+
31+
class MyApp extends StatefulWidget {
32+
const MyApp({super.key});
33+
34+
@override
35+
State<MyApp> createState() => _MyAppState();
36+
}
37+
38+
class _MyAppState extends State<MyApp> {
39+
bool _showTexture = true;
40+
41+
void _toggleTexture() {
42+
setState(() {
43+
_showTexture = !_showTexture;
44+
});
45+
}
46+
47+
@override
48+
Widget build(BuildContext context) {
49+
return MaterialApp(
50+
home: Scaffold(
51+
appBar: AppBar(title: const Text('HCPP Platform View Bug Demo')),
52+
body: Column(
53+
children: <Widget>[
54+
Padding(
55+
padding: const EdgeInsets.all(16.0),
56+
child: ElevatedButton(
57+
key: const ValueKey<String>('ToggleTexture'),
58+
onPressed: _toggleTexture,
59+
child: Text(_showTexture ? 'Hide Texture' : 'Show Texture'),
60+
),
61+
),
62+
Expanded(
63+
child: Center(
64+
child: Stack(
65+
children: <Widget>[
66+
Center(
67+
child: SizedBox(
68+
width: 300,
69+
height: 300,
70+
child: PlatformViewLink(
71+
viewType: 'changing_color_button_platform_view',
72+
surfaceFactory:
73+
(BuildContext context, PlatformViewController controller) {
74+
return AndroidViewSurface(
75+
controller: controller as AndroidViewController,
76+
hitTestBehavior: PlatformViewHitTestBehavior.transparent,
77+
gestureRecognizers:
78+
const <Factory<OneSequenceGestureRecognizer>>{},
79+
);
80+
},
81+
onCreatePlatformView: (PlatformViewCreationParams params) {
82+
return PlatformViewsService.initHybridAndroidView(
83+
id: params.id,
84+
viewType: 'changing_color_button_platform_view',
85+
layoutDirection: TextDirection.ltr,
86+
creationParamsCodec: const StandardMessageCodec(),
87+
)
88+
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
89+
..create();
90+
},
91+
),
92+
),
93+
),
94+
95+
if (_showTexture)
96+
const Center(
97+
child: SizedBox(
98+
width: 275,
99+
height: 275,
100+
child: Opacity(
101+
opacity: 0.5,
102+
child: Texture(
103+
// Intentionally use an unknown texture ID: this
104+
// results a black rectangle which is good enough
105+
// for our purposes.
106+
textureId: 1,
107+
),
108+
),
109+
),
110+
),
111+
],
112+
),
113+
),
114+
),
115+
],
116+
),
117+
),
118+
);
119+
}
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:android_driver_extensions/native_driver.dart';
8+
import 'package:android_driver_extensions/skia_gold.dart';
9+
import 'package:flutter_driver/flutter_driver.dart';
10+
import 'package:test/test.dart';
11+
12+
import '../_luci_skia_gold_prelude.dart';
13+
14+
/// For local debugging, a (local) golden-file is required as a baseline:
15+
///
16+
/// ```sh
17+
/// # Checkout HEAD, i.e. *before* changes you want to test.
18+
/// UPDATE_GOLDENS=1 flutter drive lib/hcpp/platform_view_overlay_layer_cleared.dart
19+
///
20+
/// # Make your changes.
21+
///
22+
/// # Run the test against baseline.
23+
/// flutter drive lib/hcpp/platform_view_overlay_layer_cleared.dart
24+
/// ```
25+
///
26+
/// For a convenient way to deflake a test, see `tool/deflake.dart`.
27+
void main() async {
28+
const String goldenPrefix = 'hybrid_composition_pp_platform_view';
29+
30+
late final FlutterDriver flutterDriver;
31+
late final NativeDriver nativeDriver;
32+
33+
setUpAll(() async {
34+
if (isLuci) {
35+
await enableSkiaGoldComparator(namePrefix: 'android_engine_test$goldenVariant');
36+
}
37+
flutterDriver = await FlutterDriver.connect();
38+
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
39+
await nativeDriver.configureForScreenshotTesting();
40+
await flutterDriver.waitUntilFirstFrameRasterized();
41+
});
42+
43+
tearDownAll(() async {
44+
await nativeDriver.close();
45+
await flutterDriver.close();
46+
});
47+
48+
test('verify that HCPP is supported and enabled', () async {
49+
final Map<String, Object?> response =
50+
json.decode(await flutterDriver.requestData('')) as Map<String, Object?>;
51+
52+
expect(response['supported'], true);
53+
}, timeout: Timeout.none);
54+
55+
test('should start with texture, and toggle to no texture', () async {
56+
await expectLater(nativeDriver.screenshot(), matchesGoldenFile('$goldenPrefix.texture.png'));
57+
await flutterDriver.tap(find.byValueKey('ToggleTexture'));
58+
await expectLater(nativeDriver.screenshot(), matchesGoldenFile('$goldenPrefix.no_texture.png'));
59+
}, timeout: Timeout.none);
60+
}

engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.cc

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,11 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView(
7373
if (!FrameHasPlatformLayers()) {
7474
frame->Submit();
7575
// If the previous frame had platform views, hide the overlay surface.
76-
if (previous_frame_view_count_ > 0) {
77-
jni_facade_->hideOverlaySurface2();
78-
}
76+
HideOverlayLayerIfNeeded();
7977
jni_facade_->applyTransaction();
8078
return;
8179
}
8280

83-
bool prev_frame_no_platform_views = previous_frame_view_count_ == 0;
8481
std::unordered_map<int64_t, DlRect> view_rects;
8582
for (auto platform_id : composition_order_) {
8683
view_rects[platform_id] = GetViewRect(platform_id, view_params_);
@@ -146,20 +143,27 @@ void AndroidExternalViewEmbedder2::SubmitFlutterView(
146143
overlay_canvas->RestoreToCount(restore_count);
147144
}
148145
}
146+
bool overlay_layer_has_content_this_frame_;
149147
if (overlay_frame != nullptr) {
150148
overlay_frame->set_submit_info({.frame_boundary = false});
151149
overlay_frame->Submit();
150+
overlay_layer_has_content_this_frame_ = true;
151+
} else {
152+
overlay_layer_has_content_this_frame_ = false;
152153
}
153154
frame->Submit();
154155

155156
task_runners_.GetPlatformTaskRunner()->PostTask(fml::MakeCopyable(
156157
[&, composition_order = composition_order_, view_params = view_params_,
157158
jni_facade = jni_facade_, device_pixel_ratio = device_pixel_ratio_,
158-
slices = std::move(slices_), prev_frame_no_platform_views]() -> void {
159+
slices = std::move(slices_),
160+
overlay_layer_has_content_this_frame_]() -> void {
159161
jni_facade->swapTransaction();
160162

161-
if (prev_frame_no_platform_views) {
162-
jni_facade_->showOverlaySurface2();
163+
if (overlay_layer_has_content_this_frame_) {
164+
ShowOverlayLayerIfNeeded();
165+
} else {
166+
HideOverlayLayerIfNeeded();
163167
}
164168

165169
for (int64_t view_id : composition_order) {
@@ -197,8 +201,6 @@ DlCanvas* AndroidExternalViewEmbedder2::GetRootCanvas() {
197201
}
198202

199203
void AndroidExternalViewEmbedder2::Reset() {
200-
previous_frame_view_count_ = composition_order_.size();
201-
202204
composition_order_.clear();
203205
slices_.clear();
204206
}
@@ -257,6 +259,21 @@ void AndroidExternalViewEmbedder2::DestroySurfaces() {
257259
latch.Signal();
258260
});
259261
latch.Wait();
262+
overlay_layer_is_shown_ = false;
263+
}
264+
265+
void AndroidExternalViewEmbedder2::ShowOverlayLayerIfNeeded() {
266+
if (!overlay_layer_is_shown_) {
267+
jni_facade_->showOverlaySurface2();
268+
overlay_layer_is_shown_ = true;
269+
}
270+
}
271+
272+
void AndroidExternalViewEmbedder2::HideOverlayLayerIfNeeded() {
273+
if (overlay_layer_is_shown_) {
274+
jni_facade_->hideOverlaySurface2();
275+
overlay_layer_is_shown_ = false;
276+
}
260277
}
261278

262279
} // namespace flutter

engine/src/flutter/shell/platform/android/external_view_embedder/external_view_embedder_2.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ class AndroidExternalViewEmbedder2 final : public ExternalViewEmbedder {
112112
// The task runners.
113113
const TaskRunners task_runners_;
114114

115+
// If the overlay layer is currently shown.
116+
bool overlay_layer_is_shown_ = false;
117+
115118
// The size of the root canvas.
116119
DlISize frame_size_;
117120

@@ -132,9 +135,6 @@ class AndroidExternalViewEmbedder2 final : public ExternalViewEmbedder {
132135
// mutation stack.
133136
std::unordered_map<int64_t, EmbeddedViewParams> view_params_;
134137

135-
// The number of platform views in the previous frame.
136-
int64_t previous_frame_view_count_;
137-
138138
// Destroys the surfaces created from the surface factory.
139139
// This method schedules a task on the platform thread, and waits for
140140
// the task until it completes.
@@ -145,6 +145,13 @@ class AndroidExternalViewEmbedder2 final : public ExternalViewEmbedder {
145145

146146
// Whether the layer tree in the current frame has platform layers.
147147
bool FrameHasPlatformLayers();
148+
149+
// Shows the overlay layer if it has content and the previous frame did not.
150+
void ShowOverlayLayerIfNeeded();
151+
152+
// Hides the overlay layer if it does not have content and the previous
153+
// frame did have content.
154+
void HideOverlayLayerIfNeeded();
148155
};
149156

150157
} // namespace flutter

0 commit comments

Comments
 (0)