@@ -135,6 +135,91 @@ class AnnotationResult<T> {
135135/// * [RenderView.compositeFrame] , which implements this recomposition protocol
136136/// for painting [RenderObject] trees on the display.
137137abstract 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.
9171021class 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] .
0 commit comments