Skip to content

Commit d54cdf9

Browse files
authored
Add a mechanism to observe layer tree composition. (#103378)
1 parent c62a7d4 commit d54cdf9

File tree

4 files changed

+442
-4
lines changed

4 files changed

+442
-4
lines changed

packages/flutter/lib/src/rendering/layer.dart

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,91 @@ class AnnotationResult<T> {
135135
/// * [RenderView.compositeFrame], which implements this recomposition protocol
136136
/// for painting [RenderObject] trees on the display.
137137
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
138+
final Map<int, VoidCallback> _callbacks = <int, VoidCallback>{};
139+
static int _nextCallbackId = 0;
140+
141+
/// Whether the subtree rooted at this layer has any composition callback
142+
/// observers.
143+
///
144+
/// This only evaluates to true if the subtree rooted at this node has
145+
/// observers. For example, it may evaluate to true on a parent node but false
146+
/// on a child if the parent has observers but the child does not.
147+
///
148+
/// See also:
149+
///
150+
/// * [Layer.addCompositionCallback].
151+
bool get subtreeHasCompositionCallbacks => _compositionCallbackCount > 0;
152+
153+
int _compositionCallbackCount = 0;
154+
void _updateSubtreeCompositionObserverCount(int delta) {
155+
assert(delta != 0);
156+
_compositionCallbackCount += delta;
157+
assert(_compositionCallbackCount >= 0);
158+
if (parent != null) {
159+
parent!._updateSubtreeCompositionObserverCount(delta);
160+
}
161+
}
162+
163+
void _fireCompositionCallbacks({required bool includeChildren}) {
164+
for (final VoidCallback callback in List<VoidCallback>.of(_callbacks.values)) {
165+
callback();
166+
}
167+
}
168+
169+
bool _debugMutationsLocked = false;
170+
171+
/// Describes the clip that would be applied to contents of this layer,
172+
/// if any.
173+
Rect? describeClipBounds() => null;
174+
175+
/// Adds a callback for when the layer tree that this layer is part of gets
176+
/// composited, or when it is detached and will not be rendered again.
177+
///
178+
/// This callback will fire even if an ancestor layer is added with retained
179+
/// rendering, meaning that it will fire even if this layer gets added to the
180+
/// scene via some call to [ui.SceneBuilder.addRetained] on one of its
181+
/// ancestor layers.
182+
///
183+
/// The callback receives a reference to this layer. The recipient must not
184+
/// mutate the layer during the scope of the callback, but may traverse the
185+
/// tree to find information about the current transform or clip. The layer
186+
/// may not be [attached] anymore in this state, but even if it is detached it
187+
/// may still have an also detached parent it can visit.
188+
///
189+
/// If new callbacks are added or removed within the [callback], the new
190+
/// callbacks will fire (or stop firing) on the _next_ compositing event.
191+
///
192+
/// {@template flutter.rendering.Layer.compositionCallbacks}
193+
/// Composition callbacks are useful in place of pushing a layer that would
194+
/// otherwise try to observe the layer tree without actually affecting
195+
/// compositing. For example, a composition callback may be used to observe
196+
/// the total transform and clip of the current container layer to determine
197+
/// whether a render object drawn into it is visible or not.
198+
///
199+
/// Calling the returned callback will remove [callback] from the composition
200+
/// callbacks.
201+
/// {@endtemplate}
202+
VoidCallback addCompositionCallback(CompositionCallback callback) {
203+
_updateSubtreeCompositionObserverCount(1);
204+
final int callbackId = _nextCallbackId += 1;
205+
_callbacks[callbackId] = () {
206+
assert(() {
207+
_debugMutationsLocked = true;
208+
return true;
209+
}());
210+
callback(this);
211+
assert(() {
212+
_debugMutationsLocked = false;
213+
return true;
214+
}());
215+
};
216+
return () {
217+
assert(_callbacks.containsKey(callbackId));
218+
_callbacks.remove(callbackId);
219+
_updateSubtreeCompositionObserverCount(-1);
220+
};
221+
}
222+
138223
/// If asserts are enabled, returns whether [dispose] has
139224
/// been called since the last time any retained resources were created.
140225
///
@@ -164,6 +249,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
164249

165250
/// Called by [LayerHandle].
166251
void _unref() {
252+
assert(!_debugMutationsLocked);
167253
assert(_refCount > 0);
168254
_refCount -= 1;
169255
if (_refCount == 0) {
@@ -205,6 +291,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
205291
@protected
206292
@visibleForTesting
207293
void dispose() {
294+
assert(!_debugMutationsLocked);
208295
assert(
209296
!_debugDisposed,
210297
'Layers must only be disposed once. This is typically handled by '
@@ -261,6 +348,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
261348
@protected
262349
@visibleForTesting
263350
void markNeedsAddToScene() {
351+
assert(!_debugMutationsLocked);
264352
assert(
265353
!alwaysNeedsAddToScene,
266354
'$runtimeType with alwaysNeedsAddToScene set called markNeedsAddToScene.\n'
@@ -282,6 +370,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
282370
/// this method has no effect.
283371
@visibleForTesting
284372
void debugMarkClean() {
373+
assert(!_debugMutationsLocked);
285374
assert(() {
286375
_needsAddToScene = false;
287376
return true;
@@ -331,6 +420,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
331420
@protected
332421
@visibleForTesting
333422
set engineLayer(ui.EngineLayer? value) {
423+
assert(!_debugMutationsLocked);
334424
assert(!_debugDisposed);
335425

336426
_engineLayer?.dispose();
@@ -375,6 +465,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
375465
@protected
376466
@visibleForTesting
377467
void updateSubtreeNeedsAddToScene() {
468+
assert(!_debugMutationsLocked);
378469
_needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
379470
}
380471

@@ -387,18 +478,26 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
387478
Layer? _previousSibling;
388479

389480
@override
390-
void dropChild(AbstractNode child) {
481+
void dropChild(Layer child) {
482+
assert(!_debugMutationsLocked);
391483
if (!alwaysNeedsAddToScene) {
392484
markNeedsAddToScene();
393485
}
486+
if (child._compositionCallbackCount != 0) {
487+
_updateSubtreeCompositionObserverCount(-child._compositionCallbackCount);
488+
}
394489
super.dropChild(child);
395490
}
396491

397492
@override
398-
void adoptChild(AbstractNode child) {
493+
void adoptChild(Layer child) {
494+
assert(!_debugMutationsLocked);
399495
if (!alwaysNeedsAddToScene) {
400496
markNeedsAddToScene();
401497
}
498+
if (child._compositionCallbackCount != 0) {
499+
_updateSubtreeCompositionObserverCount(child._compositionCallbackCount);
500+
}
402501
super.adoptChild(child);
403502
}
404503

@@ -407,6 +506,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
407506
/// This has no effect if the layer's parent is already null.
408507
@mustCallSuper
409508
void remove() {
509+
assert(!_debugMutationsLocked);
410510
parent?._removeChild(this);
411511
}
412512

@@ -529,6 +629,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
529629
void addToScene(ui.SceneBuilder builder);
530630

531631
void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
632+
assert(!_debugMutationsLocked);
532633
// There can't be a loop by adding a retained layer subtree whose
533634
// _needsAddToScene is false.
534635
//
@@ -909,12 +1010,28 @@ class PerformanceOverlayLayer extends Layer {
9091010
}
9101011
}
9111012

1013+
/// The signature of the callback added in [Layer.addCompositionCallback].
1014+
typedef CompositionCallback = void Function(Layer);
1015+
9121016
/// A composited layer that has a list of children.
9131017
///
9141018
/// A [ContainerLayer] instance merely takes a list of children and inserts them
9151019
/// into the composited rendering in order. There are subclasses of
9161020
/// [ContainerLayer] which apply more elaborate effects in the process.
9171021
class ContainerLayer extends Layer {
1022+
@override
1023+
void _fireCompositionCallbacks({required bool includeChildren}) {
1024+
super._fireCompositionCallbacks(includeChildren: includeChildren);
1025+
if (!includeChildren) {
1026+
return;
1027+
}
1028+
Layer? child = firstChild;
1029+
while (child != null) {
1030+
child._fireCompositionCallbacks(includeChildren: includeChildren);
1031+
child = child.nextSibling;
1032+
}
1033+
}
1034+
9181035
/// The first composited layer in this layer's child list.
9191036
Layer? get firstChild => _firstChild;
9201037
Layer? _firstChild;
@@ -935,6 +1052,9 @@ class ContainerLayer extends Layer {
9351052
ui.Scene buildScene(ui.SceneBuilder builder) {
9361053
updateSubtreeNeedsAddToScene();
9371054
addToScene(builder);
1055+
if (subtreeHasCompositionCallbacks) {
1056+
_fireCompositionCallbacks(includeChildren: true);
1057+
}
9381058
// Clearing the flag _after_ calling `addToScene`, not _before_. This is
9391059
// because `addToScene` calls children's `addToScene` methods, which may
9401060
// mark this layer as dirty.
@@ -966,6 +1086,7 @@ class ContainerLayer extends Layer {
9661086
@override
9671087
void dispose() {
9681088
removeAllChildren();
1089+
_callbacks.clear();
9691090
super.dispose();
9701091
}
9711092

@@ -994,6 +1115,7 @@ class ContainerLayer extends Layer {
9941115

9951116
@override
9961117
void attach(Object owner) {
1118+
assert(!_debugMutationsLocked);
9971119
super.attach(owner);
9981120
Layer? child = firstChild;
9991121
while (child != null) {
@@ -1004,16 +1126,25 @@ class ContainerLayer extends Layer {
10041126

10051127
@override
10061128
void detach() {
1129+
assert(!_debugMutationsLocked);
10071130
super.detach();
10081131
Layer? child = firstChild;
10091132
while (child != null) {
10101133
child.detach();
10111134
child = child.nextSibling;
10121135
}
1136+
// Detach indicates that we may never be composited again. Clients
1137+
// interested in observing composition need to get an update here because
1138+
// they might otherwise never get another one even though the layer is no
1139+
// longer visible.
1140+
//
1141+
// Children fired them already in child.detach().
1142+
_fireCompositionCallbacks(includeChildren: false);
10131143
}
10141144

10151145
/// Adds the given layer to the end of this layer's child list.
10161146
void append(Layer child) {
1147+
assert(!_debugMutationsLocked);
10171148
assert(child != this);
10181149
assert(child != firstChild);
10191150
assert(child != lastChild);
@@ -1072,6 +1203,7 @@ class ContainerLayer extends Layer {
10721203

10731204
/// Removes all of this layer's children from its child list.
10741205
void removeAllChildren() {
1206+
assert(!_debugMutationsLocked);
10751207
Layer? child = firstChild;
10761208
while (child != null) {
10771209
final Layer? next = child.nextSibling;
@@ -1221,7 +1353,7 @@ class OffsetLayer extends ContainerLayer {
12211353
void applyTransform(Layer? child, Matrix4 transform) {
12221354
assert(child != null);
12231355
assert(transform != null);
1224-
transform.multiply(Matrix4.translationValues(offset.dx, offset.dy, 0.0));
1356+
transform.translate(offset.dx, offset.dy);
12251357
}
12261358

12271359
@override
@@ -1321,6 +1453,9 @@ class ClipRectLayer extends ContainerLayer {
13211453
}
13221454
}
13231455

1456+
@override
1457+
Rect? describeClipBounds() => clipRect;
1458+
13241459
/// {@template flutter.rendering.ClipRectLayer.clipBehavior}
13251460
/// Controls how to clip.
13261461
///
@@ -1408,6 +1543,9 @@ class ClipRRectLayer extends ContainerLayer {
14081543
}
14091544
}
14101545

1546+
@override
1547+
Rect? describeClipBounds() => clipRRect?.outerRect;
1548+
14111549
/// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
14121550
///
14131551
/// Defaults to [Clip.antiAlias].
@@ -1491,6 +1629,9 @@ class ClipPathLayer extends ContainerLayer {
14911629
}
14921630
}
14931631

1632+
@override
1633+
Rect? describeClipBounds() => clipPath?.getBounds();
1634+
14941635
/// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
14951636
///
14961637
/// Defaults to [Clip.antiAlias].

packages/flutter/lib/src/rendering/object.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import 'package:flutter/painting.dart';
1212
import 'package:flutter/semantics.dart';
1313
import 'package:vector_math/vector_math_64.dart';
1414

15-
import 'binding.dart';
1615
import 'debug.dart';
1716
import 'layer.dart';
1817

@@ -326,6 +325,22 @@ class PaintingContext extends ClipContext {
326325
_containerLayer.append(_currentLayer!);
327326
}
328327

328+
/// Adds a [CompositionCallback] for the current [ContainerLayer] used by this
329+
/// context.
330+
///
331+
/// Composition callbacks are called whenever the layer tree containing the
332+
/// current layer of this painting context gets composited, or when it gets
333+
/// detached and will not be rendered again. This happens regardless of
334+
/// whether the layer is added via retained rendering or not.
335+
///
336+
/// {@macro flutter.rendering.Layer.compositionCallbacks}
337+
///
338+
/// See also:
339+
/// * [Layer.addCompositionCallback].
340+
VoidCallback addCompositionCallback(CompositionCallback callback) {
341+
return _containerLayer.addCompositionCallback(callback);
342+
}
343+
329344
/// Stop recording to a canvas if recording has started.
330345
///
331346
/// Do not call this function directly: functions in this class will call

0 commit comments

Comments
 (0)