Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 2d3b16e

Browse files
author
Harry Terkelsen
authored
Reland [canvaskit] Rework platform views (#34236)
1 parent 33bcc3e commit 2d3b16e

File tree

11 files changed

+942
-653
lines changed

11 files changed

+942
-653
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
10241024
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart
10251025
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart
10261026
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
1027+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views_diff.dart
10271028
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart
10281029
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart
10291030
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export 'engine/canvaskit/canvaskit_api.dart';
2525
export 'engine/canvaskit/canvaskit_canvas.dart';
2626
export 'engine/canvaskit/color_filter.dart';
2727
export 'engine/canvaskit/embedded_views.dart';
28+
export 'engine/canvaskit/embedded_views_diff.dart';
2829
export 'engine/canvaskit/font_fallbacks.dart';
2930
export 'engine/canvaskit/fonts.dart';
3031
export 'engine/canvaskit/image.dart';

lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart

Lines changed: 234 additions & 366 deletions
Large diffs are not rendered by default.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2013 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+
/// The results of diffing the current composition order with the active
6+
/// composition order.
7+
class ViewListDiffResult {
8+
/// Views which should be removed from the scene.
9+
final List<int> viewsToRemove;
10+
11+
/// Views to add to the scene.
12+
final List<int> viewsToAdd;
13+
14+
/// If `true`, [viewsToAdd] should be added at the beginning of the scene.
15+
/// Otherwise, they should be added at the end of the scene.
16+
final bool addToBeginning;
17+
18+
/// If [addToBeginning] is `true`, then this is the id of the platform view
19+
/// to insert [viewsToAdd] before.
20+
///
21+
/// `null` if [addToBeginning] is `false`.
22+
final int? viewToInsertBefore;
23+
24+
const ViewListDiffResult(
25+
this.viewsToRemove, this.viewsToAdd, this.addToBeginning,
26+
{this.viewToInsertBefore});
27+
}
28+
29+
/// Diff the composition order with the active composition order. It is
30+
/// common for the composition order and active composition order to differ
31+
/// only slightly.
32+
///
33+
/// Consider a scrolling list of platform views; from frame
34+
/// to frame the composition order will change in one of two ways, depending
35+
/// on which direction the list is scrolling. One or more views will be added
36+
/// to the beginning of the list, and one or more views will be removed from
37+
/// the end of the list, with the order of the unchanged middle views
38+
/// remaining the same.
39+
// TODO(hterkelsen): Refactor to use [longestIncreasingSubsequence] and logic
40+
// similar to `Surface._insertChildDomNodes` to efficiently handle more cases,
41+
// https://github.com/flutter/flutter/issues/89611.
42+
ViewListDiffResult? diffViewList(List<int> active, List<int> next) {
43+
if (active.isEmpty || next.isEmpty) {
44+
return null;
45+
}
46+
47+
// This is tried if the first element of the next list is contained in the
48+
// active list at `index`. If the active and next lists are in the expected
49+
// form, then we should be able to iterate from `index` to the end of the
50+
// active list where every element matches in the next list.
51+
ViewListDiffResult? lookForwards(int index) {
52+
for (int i = 0; i + index < active.length; i++) {
53+
if (active[i + index] != next[i]) {
54+
// An element in the next list didn't match. This isn't in the expected
55+
// form we can optimize.
56+
return null;
57+
}
58+
if (i == next.length - 1) {
59+
// The entire next list was contained in the active list.
60+
if (index == 0) {
61+
// If the first index of the next list is also the first index in the
62+
// active list, then the next list is the same as the active list with
63+
// views removed from the end.
64+
return ViewListDiffResult(
65+
active.sublist(i + 1), const <int>[], false);
66+
} else if (i + index == active.length - 1) {
67+
// If this is also the end of the active list, then the next list is
68+
// the same as the active list with some views removed from the
69+
// beginning.
70+
return ViewListDiffResult(
71+
active.sublist(0, index), const <int>[], false);
72+
} else {
73+
return null;
74+
}
75+
}
76+
}
77+
// We reached the end of the active list but have not reached the end of the
78+
// next list. The lists are in the expected form. We should remove the
79+
// elements from `0` to `index` in the active list from the DOM and add the
80+
// elements from `active.length - index` (the entire active list minus the
81+
// number of new elements at the beginning) to the end of the next list to
82+
// the DOM at the end of the list of platform views.
83+
final List<int> viewsToRemove = active.sublist(0, index);
84+
final List<int> viewsToAdd = next.sublist(active.length - index);
85+
86+
return ViewListDiffResult(
87+
viewsToRemove,
88+
viewsToAdd,
89+
false,
90+
);
91+
}
92+
93+
// This is tried if the last element of the next list is contained in the
94+
// active list at `index`. If the lists are in the expected form, we should be
95+
// able to iterate backwards from index to the beginning of the active list
96+
// and have every element match the corresponding element from the next list.
97+
ViewListDiffResult? lookBackwards(int index) {
98+
for (int i = 0; index - i >= 0; i++) {
99+
if (active[index - i] != next[next.length - 1 - i]) {
100+
// An element from the next list didn't match the coresponding element
101+
// from the active list. These lists aren't in the expected form.
102+
return null;
103+
}
104+
if (i == next.length - 1) {
105+
// The entire next list was contained in the active list.
106+
if (index == active.length - 1) {
107+
// If the last element of the next list is also the last element of
108+
// the active list, then the next list is just the active list with
109+
// some elements removed from the beginning.
110+
return ViewListDiffResult(
111+
active.sublist(0, active.length - i - 1), const <int>[], false);
112+
} else if (index == i) {
113+
// If we also reached the beginning of the active list, then the next
114+
// list is the same as the active list with some views removed from
115+
// the end.
116+
return ViewListDiffResult(
117+
active.sublist(index + 1), const <int>[], false);
118+
} else {
119+
return null;
120+
}
121+
}
122+
}
123+
124+
// We reached the beginning of the active list but have not exhausted the
125+
// entire next list. The lists are in the expected form. We should remove
126+
// the elements from the end of the active list which come after the element
127+
// which matches the last index of the next list (everything after `index`).
128+
// We should add the elements from the next list which we didn't reach while
129+
// iterating above (the first `next.length - index` views).
130+
final List<int> viewsToRemove = active.sublist(index + 1);
131+
final List<int> viewsToAdd = next.sublist(0, next.length - 1 - index);
132+
133+
return ViewListDiffResult(
134+
viewsToRemove,
135+
viewsToAdd,
136+
true,
137+
viewToInsertBefore: active.first,
138+
);
139+
}
140+
141+
// If the [active] and [next] lists are in the expected form described above,
142+
// then either the first or last element of [next] will be in [active].
143+
final int firstIndex = active.indexOf(next.first);
144+
final int lastIndex = active.lastIndexOf(next.last);
145+
if (firstIndex != -1 && lastIndex != -1) {
146+
// Both the first element and the last element of the next list are in the
147+
// active list. Search in the direction that will result in the least
148+
// amount of deletions.
149+
if (firstIndex <= (active.length - lastIndex)) {
150+
return lookForwards(firstIndex);
151+
} else {
152+
return lookBackwards(lastIndex);
153+
}
154+
} else if (firstIndex != -1) {
155+
return lookForwards(firstIndex);
156+
} else if (lastIndex != -1) {
157+
return lookBackwards(lastIndex);
158+
} else {
159+
return null;
160+
}
161+
}

lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,9 @@ class LayerTree {
4848
void paint(Frame frame, {bool ignoreRasterCache = false}) {
4949
final CkNWayCanvas internalNodesCanvas = CkNWayCanvas();
5050
internalNodesCanvas.addCanvas(frame.canvas);
51-
final List<CkCanvas> overlayCanvases =
51+
final Iterable<CkCanvas> overlayCanvases =
5252
frame.viewEmbedder!.getOverlayCanvases();
53-
for (int i = 0; i < overlayCanvases.length; i++) {
54-
internalNodesCanvas.addCanvas(overlayCanvases[i]);
55-
}
53+
overlayCanvases.forEach(internalNodesCanvas.addCanvas);
5654
// Clear the canvases before painting
5755
internalNodesCanvas.clear(const ui.Color(0x00000000));
5856
final PaintContext context = PaintContext(

lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4+
import 'dart:math' as math show max;
45

56
import 'package:meta/meta.dart';
67

@@ -19,12 +20,23 @@ class SurfaceFactory {
1920
/// Useful in tests for checking the lifecycle of this class.
2021
static SurfaceFactory? get debugUninitializedInstance => _instance;
2122

23+
// Override the current instance with a new one.
24+
//
25+
// This should only be used in tests.
26+
static void debugSetInstance(SurfaceFactory newInstance) {
27+
_instance = newInstance;
28+
}
29+
2230
static SurfaceFactory? _instance;
2331

24-
SurfaceFactory(this.maximumSurfaces)
25-
: assert(maximumSurfaces >= 1,
26-
'The maximum number of surfaces must be at least 1') {
32+
SurfaceFactory(int maximumSurfaces)
33+
: maximumSurfaces = math.max(maximumSurfaces, 1) {
2734
if (assertionsEnabled) {
35+
if (maximumSurfaces < 1) {
36+
printWarning('Attempted to create a $SurfaceFactory with '
37+
'$maximumSurfaces maximum surfaces. At least 1 surface is required '
38+
'for rendering.');
39+
}
2840
registerHotRestartListener(debugClear);
2941
}
3042
}
@@ -34,17 +46,14 @@ class SurfaceFactory {
3446
/// all painting commands.
3547
final Surface baseSurface = Surface();
3648

37-
/// The shared backup surface
38-
final Surface backupSurface = Surface();
39-
4049
/// The maximum number of surfaces which can be live at once.
4150
final int maximumSurfaces;
4251

4352
/// The maximum number of assignable overlays.
4453
///
45-
/// This is just `maximumSurfaces - 2` (the maximum number of surfaces minus
46-
/// the base surface and backup surface).
47-
int get maximumOverlays => maximumSurfaces - 2;
54+
/// This is just `maximumSurfaces - 1` (the maximum number of surfaces minus
55+
/// the required base surface).
56+
int get maximumOverlays => maximumSurfaces - 1;
4857

4958
/// Surfaces created by this factory which are currently in use.
5059
final List<Surface> _liveSurfaces = <Surface>[];
@@ -54,11 +63,11 @@ class SurfaceFactory {
5463
final List<Surface> _cache = <Surface>[];
5564

5665
/// The number of surfaces which have been created by this factory.
57-
int get _surfaceCount => _liveSurfaces.length + _cache.length + 2;
66+
int get _surfaceCount => _liveSurfaces.length + _cache.length + 1;
5867

5968
/// The number of available overlay surfaces.
6069
///
61-
/// This does not include the base surface or backup surface.
70+
/// This does not include the base surface.
6271
int get numAvailableOverlays => maximumOverlays - _liveSurfaces.length;
6372

6473
/// The number of surfaces created by this factory. Used for testing.
@@ -70,10 +79,6 @@ class SurfaceFactory {
7079
/// Useful in tests.
7180
int get debugCacheSize => _cache.length;
7281

73-
/// Whether or not we have already emitted a warning about creating too many
74-
/// surfaces.
75-
bool _warnedAboutTooManySurfaces = false;
76-
7782
/// Gets an overlay surface from the cache or creates a new one if it wouldn't
7883
/// exceed the maximum. If there are no available surfaces, returns `null`.
7984
Surface? getOverlay() {
@@ -90,29 +95,6 @@ class SurfaceFactory {
9095
}
9196
}
9297

93-
/// Gets a [Surface] which is ready to paint to.
94-
///
95-
/// If there are available surfaces in the cache, then this will return one of
96-
/// them. If this factory hasn't yet created [maximumSurfaces] surfaces, then a
97-
/// new one will be created. If this factory has already created [maximumSurfaces]
98-
/// surfaces, then this will return a backup surface which will be returned by
99-
/// all subsequent calls to [getSurface] until some surfaces have been
100-
/// released with [releaseSurface].
101-
Surface getSurface() {
102-
final Surface? surface = getOverlay();
103-
if (surface != null) {
104-
return surface;
105-
}
106-
if (!_warnedAboutTooManySurfaces) {
107-
_warnedAboutTooManySurfaces = true;
108-
printWarning('Flutter was unable to create enough overlay surfaces. '
109-
'This is usually caused by too many platform views being '
110-
'displayed at once. '
111-
'You may experience incorrect rendering.');
112-
}
113-
return backupSurface;
114-
}
115-
11698
/// Releases all surfaces so they can be reused in the next frame.
11799
///
118100
/// If a released surface is in the DOM, it is not removed. This allows the
@@ -130,7 +112,6 @@ class SurfaceFactory {
130112
/// the new surfaces.
131113
void removeSurfacesFromDom() {
132114
_cache.forEach(_removeFromDom);
133-
_removeFromDom(backupSurface);
134115
}
135116

136117
// Removes [surface] from the DOM.
@@ -141,11 +122,6 @@ class SurfaceFactory {
141122
/// Signals that a surface is no longer being used. It can be reused.
142123
void releaseSurface(Surface surface) {
143124
assert(surface != baseSurface, 'Attempting to release the base surface');
144-
if (surface == backupSurface) {
145-
// If it's the backup surface, just remove it from the DOM.
146-
surface.htmlElement.remove();
147-
return;
148-
}
149125
assert(
150126
_liveSurfaces.contains(surface),
151127
'Attempting to release a Surface which '
@@ -157,13 +133,12 @@ class SurfaceFactory {
157133

158134
/// Returns [true] if [surface] is currently being used to paint content.
159135
///
160-
/// The base surface and backup surface always count as live.
136+
/// The base surface always counts as live.
161137
///
162138
/// If a surface is not live, then it must be in the cache and ready to be
163139
/// reused.
164140
bool isLive(Surface surface) {
165141
if (surface == baseSurface ||
166-
surface == backupSurface ||
167142
_liveSurfaces.contains(surface)) {
168143
return true;
169144
}
@@ -180,7 +155,6 @@ class SurfaceFactory {
180155
surface.dispose();
181156
}
182157
baseSurface.dispose();
183-
backupSurface.dispose();
184158
_liveSurfaces.clear();
185159
_cache.clear();
186160
_instance = null;

0 commit comments

Comments
 (0)