Skip to content

Commit

Permalink
Non painting platform views (flutter#30003)
Browse files Browse the repository at this point in the history
  • Loading branch information
Harry Terkelsen authored Dec 1, 2021
1 parent fada035 commit d280475
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 22 deletions.
64 changes: 45 additions & 19 deletions lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class HtmlViewEmbedder {
/// The list of view ids that should be composited, in order.
List<int> _compositionOrder = <int>[];

/// The number of platform views in this frame which are visible.
///
/// These platform views will require overlays.
int _visibleViewCount = 0;

/// The most recent composition order.
List<int> _activeCompositionOrder = <int>[];

Expand Down Expand Up @@ -127,7 +132,7 @@ class HtmlViewEmbedder {
}

void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) {
if (!disableOverlays) {
if (!disableOverlays && platformViewManager.isVisible(viewId)) {
// We must decide in the preroll phase if a platform view will use the
// backup overlay, so that draw commands after the platform view will
// correctly paint to the backup surface.
Expand Down Expand Up @@ -170,12 +175,17 @@ class HtmlViewEmbedder {
/// If this returns a [CkCanvas], then that canvas should be the new leaf
/// node. Otherwise, keep the same leaf node.
CkCanvas? compositeEmbeddedView(int viewId) {
final int compositedViewCount = _compositionOrder.length;
final int overlayIndex = _visibleViewCount;
_compositionOrder.add(viewId);
if (!disableOverlays) {
if (compositedViewCount < _pictureRecordersCreatedDuringPreroll.length) {
if (platformViewManager.isVisible(viewId)) {
_visibleViewCount++;
}
final bool needOverlay =
!disableOverlays && platformViewManager.isVisible(viewId);
if (needOverlay) {
if (overlayIndex < _pictureRecordersCreatedDuringPreroll.length) {
_pictureRecorders[viewId] =
_pictureRecordersCreatedDuringPreroll[compositedViewCount];
_pictureRecordersCreatedDuringPreroll[overlayIndex];
} else {
_viewsUsingBackupSurface.add(viewId);
_pictureRecorders[viewId] = _backupPictureRecorder!;
Expand All @@ -184,15 +194,15 @@ class HtmlViewEmbedder {

// Do nothing if this view doesn't need to be composited.
if (!_viewsToRecomposite.contains(viewId)) {
if (!disableOverlays) {
if (needOverlay) {
return _pictureRecorders[viewId]!.recordingCanvas;
} else {
return null;
}
}
_compositeWithParams(viewId, _currentCompositionParams[viewId]!);
_viewsToRecomposite.remove(viewId);
if (!disableOverlays) {
if (needOverlay) {
return _pictureRecorders[viewId]!.recordingCanvas;
} else {
return null;
Expand Down Expand Up @@ -336,9 +346,8 @@ class HtmlViewEmbedder {
final svg.ClipPathElement newClipPath = svg.ClipPathElement();
newClipPath.id = clipId;
newClipPath.append(
svg.PathElement()
..setAttribute('d', path.toSvgString()!)
);
svg.PathElement()
..setAttribute('d', path.toSvgString()!));

pathDefs.append(newClipPath);
// Store the id of the node instead of [newClipPath] directly. For
Expand All @@ -356,9 +365,8 @@ class HtmlViewEmbedder {
final svg.ClipPathElement newClipPath = svg.ClipPathElement();
newClipPath.id = clipId;
newClipPath.append(
svg.PathElement()
..setAttribute('d', path.toSvgString()!)
);
svg.PathElement()
..setAttribute('d', path.toSvgString()!));
pathDefs.append(newClipPath);
// Store the id of the node instead of [newClipPath] directly. For
// some reason, calling `newClipPath.remove()` doesn't remove it
Expand Down Expand Up @@ -421,13 +429,22 @@ class HtmlViewEmbedder {
_compositionOrder.isEmpty ||
disableOverlays)
? null
: diffViewList(_activeCompositionOrder, _compositionOrder);
: diffViewList(
_activeCompositionOrder
.where((int viewId) => platformViewManager.isVisible(viewId))
.toList(),
_compositionOrder
.where((int viewId) => platformViewManager.isVisible(viewId))
.toList());
final Map<int, int>? insertBeforeMap = _updateOverlays(diffResult);

bool _didPaintBackupSurface = false;
if (!disableOverlays) {
for (int i = 0; i < _compositionOrder.length; i++) {
final int viewId = _compositionOrder[i];
if (platformViewManager.isInvisible(viewId)) {
continue;
}
if (_viewsUsingBackupSurface.contains(viewId)) {
// Only draw the picture to the backup surface once.
if (!_didPaintBackupSurface) {
Expand Down Expand Up @@ -455,6 +472,7 @@ class HtmlViewEmbedder {
_viewsUsingBackupSurface.clear();
if (listEquals(_compositionOrder, _activeCompositionOrder)) {
_compositionOrder.clear();
_visibleViewCount = 0;
return;
}

Expand Down Expand Up @@ -542,6 +560,7 @@ class HtmlViewEmbedder {
}

_compositionOrder.clear();
_visibleViewCount = 0;

disposeViews(unusedViews);

Expand Down Expand Up @@ -601,12 +620,15 @@ class HtmlViewEmbedder {
// to the backup surface.
SurfaceFactory.instance.releaseSurfaces();
_overlays.clear();
final List<int> viewsNeedingOverlays = _compositionOrder
.where((int viewId) => platformViewManager.isVisible(viewId))
.toList();
final int numOverlays = math.min(
SurfaceFactory.instance.maximumOverlays,
_compositionOrder.length,
viewsNeedingOverlays.length,
);
for (int i = 0; i < numOverlays; i++) {
final int viewId = _compositionOrder[i];
final int viewId = viewsNeedingOverlays[i];
assert(!_viewsUsingBackupSurface.contains(viewId));
_initializeOverlay(viewId);
}
Expand Down Expand Up @@ -662,7 +684,8 @@ class HtmlViewEmbedder {
while (overlaysToAssign > 0 && index < _compositionOrder.length) {
final bool activeView = index < lastOriginalIndex;
final int viewId = _compositionOrder[index];
if (!_overlays.containsKey(viewId)) {
if (!_overlays.containsKey(viewId) &&
platformViewManager.isVisible(viewId)) {
_initializeOverlay(viewId);
overlaysToAssign--;
if (activeView) {
Expand All @@ -686,6 +709,7 @@ class HtmlViewEmbedder {
for (int i = 0; i < _compositionOrder.length; i++) {
final int viewId = _compositionOrder[i];
assert(_viewsUsingBackupSurface.contains(viewId) ||
platformViewManager.isInvisible(viewId) ||
_overlays[viewId] != null);
}
}
Expand Down Expand Up @@ -728,6 +752,7 @@ class HtmlViewEmbedder {
_viewsToRecomposite.clear();
_activeCompositionOrder.clear();
_compositionOrder.clear();
_visibleViewCount = 0;
}
}

Expand Down Expand Up @@ -945,8 +970,9 @@ class ViewListDiffResult {
// similar to `Surface._insertChildDomNodes` to efficiently handle more cases,
// https://github.com/flutter/flutter/issues/89611.
ViewListDiffResult? diffViewList(List<int> active, List<int> next) {
assert(active.isNotEmpty && next.isNotEmpty,
'diffViewList called with empty view list');
if (active.isEmpty || next.isEmpty) {
return null;
}
// If the [active] and [next] lists are in the expected form described above,
// then either the first or last element of [next] will be in [active].
int index = active.indexOf(next.first);
Expand Down
22 changes: 21 additions & 1 deletion lib/web_ui/lib/src/engine/platform_views/content_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class PlatformViewManager {
// The references to content tags, indexed by their framework-given ID.
final Map<int, html.Element> _contents = <int, html.Element>{};

final Set<String> _invisibleViews = <String>{};
final Map<int, String> _viewIdToType = <int, String>{};

/// Returns `true` if the passed in `viewType` has been registered before.
///
/// See [registerViewFactory] to understand how factories are registered.
Expand All @@ -64,14 +67,18 @@ class PlatformViewManager {
/// it's been set.
///
/// `factoryFunction` needs to be a [PlatformViewFactory].
bool registerFactory(String viewType, Function factoryFunction) {
bool registerFactory(String viewType, Function factoryFunction,
{bool isVisible = true}) {
assert(factoryFunction is PlatformViewFactory ||
factoryFunction is ParameterizedPlatformViewFactory);

if (_factories.containsKey(viewType)) {
return false;
}
_factories[viewType] = factoryFunction;
if (!isVisible) {
_invisibleViews.add(viewType);
}
return true;
}

Expand Down Expand Up @@ -105,6 +112,7 @@ class PlatformViewManager {
'Attempted to render contents of unregistered viewType: $viewType');

final String slotName = getPlatformViewSlotName(viewId);
_viewIdToType[viewId] = viewType;

return _contents.putIfAbsent(viewId, () {
final html.Element wrapper = html.document
Expand Down Expand Up @@ -186,6 +194,16 @@ class PlatformViewManager {
}
}

/// Returns `true` if the given [viewId] is for an invisible platform view.
bool isInvisible(int viewId) {
final String? viewType = _viewIdToType[viewId];
return viewType != null && _invisibleViews.contains(viewType);
}

/// Returns `true` if the given [viewId] is a platform view with a visible
/// component.
bool isVisible(int viewId) => !isInvisible(viewId);

/// Clears the state. Used in tests.
///
/// Returns the set of know view ids, so they can be cleaned up.
Expand All @@ -194,6 +212,8 @@ class PlatformViewManager {
result.forEach(clearPlatformView);
_factories.clear();
_contents.clear();
_invisibleViews.clear();
_viewIdToType.clear();
return result;
}
}
6 changes: 4 additions & 2 deletions lib/web_ui/lib/ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ typedef PlatformViewFactory = html.Element Function(int viewId);
/// A registry for factories that create platform views.
class PlatformViewRegistry {
/// Register [viewTypeId] as being creating by the given [factory].
bool registerViewFactory(String viewTypeId, PlatformViewFactory viewFactory) {
bool registerViewFactory(String viewTypeId, PlatformViewFactory viewFactory,
{bool isVisible = true}) {
// TODO(web): Deprecate this once there's another way of calling `registerFactory` (js interop?)
return engine.platformViewManager.registerFactory(viewTypeId, viewFactory);
return engine.platformViewManager
.registerFactory(viewTypeId, viewFactory, isVisible: isVisible);
}
}

Expand Down
111 changes: 111 additions & 0 deletions lib/web_ui/test/canvaskit/embedded_views_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,117 @@ void testMain() {

HtmlViewEmbedder.debugDisableOverlays = false;
});

test('does not create overlays for invisible platform views', () async {
ui.platformViewRegistry.registerViewFactory(
'test-visible-view',
(int viewId) =>
html.DivElement()..className = 'visible-platform-view');
ui.platformViewRegistry.registerViewFactory(
'test-invisible-view',
(int viewId) =>
html.DivElement()..className = 'invisible-platform-view',
isVisible: false,
);
await createPlatformView(0, 'test-visible-view');
await createPlatformView(1, 'test-invisible-view');
await createPlatformView(2, 'test-visible-view');
await createPlatformView(3, 'test-invisible-view');
await createPlatformView(4, 'test-invisible-view');
await createPlatformView(5, 'test-invisible-view');
await createPlatformView(6, 'test-invisible-view');

final EnginePlatformDispatcher dispatcher =
ui.window.platformDispatcher as EnginePlatformDispatcher;

int countCanvases() {
return domRenderer.sceneElement!.querySelectorAll('canvas').length;
}

expect(platformViewManager.isInvisible(0), isFalse);
expect(platformViewManager.isInvisible(1), isTrue);

LayerSceneBuilder sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(1, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 1);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 2);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.addPlatformView(5, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.addPlatformView(5, width: 10, height: 10);
sb.addPlatformView(6, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.addPlatformView(5, width: 10, height: 10);
sb.addPlatformView(6, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 1);
});
// TODO(dit): https://github.com/flutter/flutter/issues/60040
}, skip: isIosSafari);
}

0 comments on commit d280475

Please sign in to comment.