[web] Fixes invisible platform view compositing edge cases. (#39506)
* Somewhat works... Remove first overlay though.
* needNewOverlay is equivalent to isVisible in the algorithm.
* Only consider groups with a visible view to need an overlay.
* Update tests with new logic. Make test output easier to understand and find in the test file.
* Introduce OverlayGroup helper class.
diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
index 2769494..f2ae09c 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
@@ -133,15 +133,9 @@
'displayed at once. '
'You may experience incorrect rendering.');
}
- // We need an overlay for the first platform view no matter what. The first
- // visible platform view doesn't need to create a new one if we already
- // created one.
- final bool needNewOverlay = (platformViewManager.isVisible(viewId) &&
- _context.seenFirstVisibleViewInPreroll) ||
- _context.pictureRecordersCreatedDuringPreroll.isEmpty;
- if (platformViewManager.isVisible(viewId)) {
- _context.seenFirstVisibleViewInPreroll = true;
- }
+ // We need an overlay for each visible platform view. Invisible platform
+ // views will be grouped with (at most) one visible platform view later.
+ final bool needNewOverlay = platformViewManager.isVisible(viewId);
if (needNewOverlay && hasAvailableOverlay) {
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
pictureRecorder.beginRecording(ui.Offset.zero & _frameSize);
@@ -169,17 +163,12 @@
CkCanvas? compositeEmbeddedView(int viewId) {
final int overlayIndex = _context.visibleViewCount;
_compositionOrder.add(viewId);
- if (platformViewManager.isVisible(viewId) ||
- _context.pictureRecorders.isEmpty) {
+ // Keep track of the number of visible platform views.
+ if (platformViewManager.isVisible(viewId)) {
_context.visibleViewCount++;
}
- // We need a new overlay if this is a visible view or if we don't have one yet.
- final bool needNewOverlay = (platformViewManager.isVisible(viewId) &&
- _context.seenFirstVisibleView) ||
- _context.pictureRecorders.isEmpty;
- if (platformViewManager.isVisible(viewId)) {
- _context.seenFirstVisibleView = true;
- }
+ // We need a new overlay if this is a visible view.
+ final bool needNewOverlay = platformViewManager.isVisible(viewId);
CkPictureRecorder? recorderToUseForRendering;
if (needNewOverlay) {
if (overlayIndex < _context.pictureRecordersCreatedDuringPreroll.length) {
@@ -424,7 +413,11 @@
? null
: diffViewList(_activeCompositionOrder, _compositionOrder);
_updateOverlays(diffResult);
- assert(_context.pictureRecorders.length == _overlays.length);
+ assert(
+ _context.pictureRecorders.length == _overlays.length,
+ 'There should be the same number of picture recorders '
+ '(${_context.pictureRecorders.length}) as overlays (${_overlays.length}).',
+ );
int pictureRecorderIndex = 0;
for (int i = 0; i < _compositionOrder.length; i++) {
@@ -600,9 +593,13 @@
// overlays.
return;
}
- final List<List<int>> overlayGroups = getOverlayGroups(_compositionOrder);
+ // Group platform views from their composition order.
+ // Each group contains one visible view, and any number of invisible views
+ // before or after that visible view.
+ final List<OverlayGroup> overlayGroups =
+ getOverlayGroups(_compositionOrder);
final List<int> viewsNeedingOverlays =
- overlayGroups.map((List<int> group) => group.last).toList();
+ overlayGroups.map((OverlayGroup group) => group.last).toList();
// If there were more visible views than overlays, then the last group
// doesn't have an overlay.
if (viewsNeedingOverlays.length > SurfaceFactory.instance.maximumOverlays) {
@@ -642,44 +639,48 @@
// If there are more visible views than overlays, then the views which cannot
// be assigned an overlay are grouped together and will be rendered on top of
// the rest of the scene.
- List<List<int>> getOverlayGroups(List<int> views) {
- // Visibility groups are typically a visible view followed by zero or more
- // invisible views. However, if the view list begins with one or more
- // invisible views, we can group them with the first visible view.
+ List<OverlayGroup> getOverlayGroups(List<int> views) {
final int maxOverlays = SurfaceFactory.instance.maximumOverlays;
if (maxOverlays == 0) {
- return const <List<int>>[];
+ return const <OverlayGroup>[];
}
- bool foundFirstVisibleView = false;
- final List<List<int>> result = <List<int>>[];
- List<int> currentGroup = <int>[];
- int i = 0;
- for (; i < views.length; i++) {
+ final List<OverlayGroup> result = <OverlayGroup>[];
+ OverlayGroup currentGroup = OverlayGroup(<int>[]);
+
+ for (int i = 0; i < views.length; i++) {
final int view = views[i];
if (platformViewManager.isInvisible(view)) {
+ // We add as many invisible views as we find to the current group.
currentGroup.add(view);
} else {
- if (foundFirstVisibleView) {
- result.add(currentGroup);
- // If we are out of overlays, then break let the rest of the views be
- // added to an extra group that will be rendered on top of the scene.
- if (result.length == maxOverlays) {
- currentGroup = <int>[];
- break;
- } else {
- currentGroup = <int>[view];
- }
+ // `view` is visible.
+ if (!currentGroup.hasVisibleView) {
+ // If `view` is the first visible one of the group, add it.
+ currentGroup.add(view, visible: true);
} else {
- // We hit this case if this is the first visible view.
- foundFirstVisibleView = true;
- currentGroup.add(view);
+ // There's already a visible `view` in `currentGroup`, so a new
+ // OverlayGroup will be needed.
+ // Let's decide what to do with the `currentGroup` first:
+ if (currentGroup.hasVisibleView) {
+ // We only care about groups that have one visible view.
+ result.add(currentGroup);
+ }
+ // If there are overlays still available.
+ if (result.length < maxOverlays) {
+ // Create a new group, starting with `view`.
+ currentGroup = OverlayGroup(<int>[view], visible: true);
+ } else {
+ // Add the rest of the views to a final group that will be rendered
+ // on top of the scene.
+ currentGroup = OverlayGroup(views.sublist(i), visible: true);
+ // And break out of the loop!
+ break;
+ }
}
}
}
- if (i < views.length) {
- currentGroup.addAll(views.sublist(i));
- }
- if (currentGroup.isNotEmpty) {
+ // Handle the last group to be (maybe) returned.
+ if (currentGroup.hasVisibleView) {
result.add(currentGroup);
}
return result;
@@ -727,6 +728,39 @@
}
}
+/// A group of views that will be composited together within the same overlay.
+///
+/// Each OverlayGroup is a sublist of the composition order which can share the
+/// same overlay.
+///
+/// Every overlay group is a list containing a visible view preceded or followed
+/// by zero or more invisible views.
+class OverlayGroup {
+ /// Constructor
+ OverlayGroup(
+ List<int> viewGroup, {
+ bool visible = false,
+ }) : _group = viewGroup,
+ _containsVisibleView = visible;
+
+ // The internal list of ints.
+ final List<int> _group;
+ // A boolean flag to mark if any visible view has been added to the list.
+ bool _containsVisibleView;
+
+ /// Add a [view] (maybe [visible]) to this group.
+ void add(int view, {bool visible = false}) {
+ _group.add(view);
+ _containsVisibleView |= visible;
+ }
+
+ /// Get the "last" view added to this group.
+ int get last => _group.last;
+
+ /// Returns true if this group contains any visible view.
+ bool get hasVisibleView => _group.isNotEmpty && _containsVisibleView;
+}
+
/// Represents a Clip Chain (for a view).
///
/// Objects of this class contain:
@@ -905,12 +939,6 @@
/// The state for the current frame.
class EmbedderFrameContext {
- /// Whether or not we have seen a visible platform view in this frame yet.
- bool seenFirstVisibleViewInPreroll = false;
-
- /// Whether or not we have seen a visible platform view in this frame yet.
- bool seenFirstVisibleView = false;
-
/// Picture recorders which were created during the preroll phase.
///
/// These picture recorders will be "claimed" in the paint phase by platform
diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart
index 112fc0b..fe32856 100644
--- a/lib/web_ui/test/canvaskit/embedded_views_test.dart
+++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart
@@ -926,8 +926,7 @@
_expectSceneMatches(<_EmbeddedViewMarker>[
_overlay,
_platformView,
- _overlay,
- ]);
+ ], reason: 'Invisible view alone renders on top of base overlay.');
sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
@@ -940,7 +939,7 @@
_platformView,
_platformView,
_overlay,
- ]);
+ ], reason: 'Overlay created after a group containing a visible view.');
sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
@@ -956,7 +955,7 @@
_overlay,
_platformView,
_overlay,
- ]);
+ ], reason: 'Overlays created after each group containing a visible view.');
sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
@@ -974,7 +973,7 @@
_platformView,
_platformView,
_overlay,
- ]);
+ ], reason: 'Invisible views grouped in with visible views.');
sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
@@ -1058,8 +1057,7 @@
_platformView,
_platformView,
_platformView,
- _overlay,
- ]);
+ ], reason: 'Many invisible views can be rendered on top of the base overlay.');
sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
@@ -1108,20 +1106,20 @@
_EmbeddedViewMarker get _overlay => _EmbeddedViewMarker.overlay;
_EmbeddedViewMarker get _platformView => _EmbeddedViewMarker.platformView;
-void _expectSceneMatches(List<_EmbeddedViewMarker> markers) {
- final List<DomElement> sceneElements = flutterViewEmbedder
+const Map<String, _EmbeddedViewMarker> _tagToViewMarker = <String, _EmbeddedViewMarker>{
+ 'flt-canvas-container': _EmbeddedViewMarker.overlay,
+ 'flt-platform-view-slot': _EmbeddedViewMarker.platformView,
+};
+
+void _expectSceneMatches(List<_EmbeddedViewMarker> expectedMarkers, {
+ String? reason,
+}) {
+ // Convert the scene elements to its corresponding array of _EmbeddedViewMarker
+ final List<_EmbeddedViewMarker> sceneElements = flutterViewEmbedder
.sceneElement!.children
.where((DomElement element) => element.tagName != 'svg')
+ .map((DomElement element) => _tagToViewMarker[element.tagName.toLowerCase()]!)
.toList();
- expect(markers, hasLength(sceneElements.length));
- for (int i = 0; i < markers.length; i++) {
- switch (markers[i]) {
- case _EmbeddedViewMarker.overlay:
- expect(sceneElements[i].tagName, 'FLT-CANVAS-CONTAINER');
- break;
- case _EmbeddedViewMarker.platformView:
- expect(sceneElements[i].tagName, 'FLT-PLATFORM-VIEW-SLOT');
- break;
- }
- }
+
+ expect(sceneElements, expectedMarkers, reason: reason);
}