Unverified Commit d54cdf9e authored by Dan Field's avatar Dan Field Committed by GitHub

Add a mechanism to observe layer tree composition. (#103378)

parent c62a7d41
...@@ -12,7 +12,6 @@ import 'package:flutter/painting.dart'; ...@@ -12,7 +12,6 @@ import 'package:flutter/painting.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'binding.dart';
import 'debug.dart'; import 'debug.dart';
import 'layer.dart'; import 'layer.dart';
...@@ -326,6 +325,22 @@ class PaintingContext extends ClipContext { ...@@ -326,6 +325,22 @@ class PaintingContext extends ClipContext {
_containerLayer.append(_currentLayer!); _containerLayer.append(_currentLayer!);
} }
/// Adds a [CompositionCallback] for the current [ContainerLayer] used by this
/// context.
///
/// Composition callbacks are called whenever the layer tree containing the
/// current layer of this painting context gets composited, or when it gets
/// detached and will not be rendered again. This happens regardless of
/// whether the layer is added via retained rendering or not.
///
/// {@macro flutter.rendering.Layer.compositionCallbacks}
///
/// See also:
/// * [Layer.addCompositionCallback].
VoidCallback addCompositionCallback(CompositionCallback callback) {
return _containerLayer.addCompositionCallback(callback);
}
/// Stop recording to a canvas if recording has started. /// Stop recording to a canvas if recording has started.
/// ///
/// Do not call this function directly: functions in this class will call /// Do not call this function directly: functions in this class will call
......
...@@ -716,6 +716,255 @@ void main() { ...@@ -716,6 +716,255 @@ void main() {
layer.addToScene(builder); layer.addToScene(builder);
expect(layer.engineLayer, null); expect(layer.engineLayer, null);
}); });
test('Layers describe clip bounds', () {
ContainerLayer layer = ContainerLayer();
expect(layer.describeClipBounds(), null);
const Rect bounds = Rect.fromLTRB(10, 10, 20, 20);
final RRect rbounds = RRect.fromRectXY(bounds, 2, 2);
layer = ClipRectLayer(clipRect: bounds);
expect(layer.describeClipBounds(), bounds);
layer = ClipRRectLayer(clipRRect: rbounds);
expect(layer.describeClipBounds(), rbounds.outerRect);
layer = ClipPathLayer(clipPath: Path()..addRect(bounds));
expect(layer.describeClipBounds(), bounds);
});
test('Subtree has composition callbacks', () {
final ContainerLayer root = ContainerLayer();
expect(root.subtreeHasCompositionCallbacks, false);
final List<VoidCallback> cancellationCallbacks = <VoidCallback>[];
cancellationCallbacks.add(root.addCompositionCallback((_) {}));
expect(root.subtreeHasCompositionCallbacks, true);
final ContainerLayer a1 = ContainerLayer();
final ContainerLayer a2 = ContainerLayer();
final ContainerLayer b1 = ContainerLayer();
root.append(a1);
root.append(a2);
a1.append(b1);
expect(root.subtreeHasCompositionCallbacks, true);
expect(a1.subtreeHasCompositionCallbacks, false);
expect(a2.subtreeHasCompositionCallbacks, false);
expect(b1.subtreeHasCompositionCallbacks, false);
cancellationCallbacks.add(b1.addCompositionCallback((_) {}));
expect(root.subtreeHasCompositionCallbacks, true);
expect(a1.subtreeHasCompositionCallbacks, true);
expect(a2.subtreeHasCompositionCallbacks, false);
expect(b1.subtreeHasCompositionCallbacks, true);
cancellationCallbacks.removeAt(0)();
expect(root.subtreeHasCompositionCallbacks, true);
expect(a1.subtreeHasCompositionCallbacks, true);
expect(a2.subtreeHasCompositionCallbacks, false);
expect(b1.subtreeHasCompositionCallbacks, true);
cancellationCallbacks.removeAt(0)();
expect(root.subtreeHasCompositionCallbacks, false);
expect(a1.subtreeHasCompositionCallbacks, false);
expect(a2.subtreeHasCompositionCallbacks, false);
expect(b1.subtreeHasCompositionCallbacks, false);
});
test('Subtree has composition callbacks - removeChild', () {
final ContainerLayer root = ContainerLayer();
expect(root.subtreeHasCompositionCallbacks, false);
final ContainerLayer a1 = ContainerLayer();
final ContainerLayer a2 = ContainerLayer();
final ContainerLayer b1 = ContainerLayer();
root.append(a1);
root.append(a2);
a1.append(b1);
expect(b1.subtreeHasCompositionCallbacks, false);
expect(a1.subtreeHasCompositionCallbacks, false);
expect(root.subtreeHasCompositionCallbacks, false);
expect(a2.subtreeHasCompositionCallbacks, false);
b1.addCompositionCallback((_) { });
expect(b1.subtreeHasCompositionCallbacks, true);
expect(a1.subtreeHasCompositionCallbacks, true);
expect(root.subtreeHasCompositionCallbacks, true);
expect(a2.subtreeHasCompositionCallbacks, false);
b1.remove();
expect(b1.subtreeHasCompositionCallbacks, true);
expect(a1.subtreeHasCompositionCallbacks, false);
expect(root.subtreeHasCompositionCallbacks, false);
expect(a2.subtreeHasCompositionCallbacks, false);
});
test('No callback if removed', () {
final ContainerLayer root = ContainerLayer();
final ContainerLayer a1 = ContainerLayer();
final ContainerLayer a2 = ContainerLayer();
final ContainerLayer b1 = ContainerLayer();
root.append(a1);
root.append(a2);
a1.append(b1);
// Add and immediately remove the callback.
b1.addCompositionCallback((Layer layer) {
fail('Should not have called back');
})();
root.buildScene(SceneBuilder()).dispose();
});
test('Observe layer tree composition - not retained', () {
final ContainerLayer root = ContainerLayer();
final ContainerLayer a1 = ContainerLayer();
final ContainerLayer a2 = ContainerLayer();
final ContainerLayer b1 = ContainerLayer();
root.append(a1);
root.append(a2);
a1.append(b1);
bool compositedB1 = false;
b1.addCompositionCallback((Layer layer) {
expect(layer, b1);
compositedB1 = true;
});
expect(compositedB1, false);
root.buildScene(SceneBuilder()).dispose();
expect(compositedB1, true);
});
test('Observe layer tree composition - retained', () {
final ContainerLayer root = ContainerLayer();
final ContainerLayer a1 = ContainerLayer();
final ContainerLayer a2 = ContainerLayer();
final ContainerLayer b1 = ContainerLayer();
root.append(a1);
root.append(a2);
a1.append(b1);
// Actually build the retained layer so that the engine sees it as real and
// reusable.
SceneBuilder builder = SceneBuilder();
b1.engineLayer = builder.pushOffset(0, 0);
builder.build().dispose();
builder = SceneBuilder();
// Force the layer to appear clean and have an engine layer for retained
// rendering.
expect(b1.engineLayer, isNotNull);
b1.debugMarkClean();
expect(b1.debugSubtreeNeedsAddToScene, false);
bool compositedB1 = false;
b1.addCompositionCallback((Layer layer) {
expect(layer, b1);
compositedB1 = true;
});
expect(compositedB1, false);
root.buildScene(builder).dispose();
expect(compositedB1, true);
});
test('Observe layer tree composition - asserts on mutation', () {
final ContainerLayer root = ContainerLayer();
final ContainerLayer a1 = ContainerLayer();
final ContainerLayer a2 = ContainerLayer();
final ContainerLayer b1 = ContainerLayer();
root.append(a1);
root.append(a2);
a1.append(b1);
bool compositedB1 = false;
b1.addCompositionCallback((Layer layer) {
expect(layer, b1);
expect(() => layer.remove(), throwsAssertionError);
expect(() => layer.dispose(), throwsAssertionError);
expect(() => layer.markNeedsAddToScene(), throwsAssertionError);
expect(() => layer.debugMarkClean(), throwsAssertionError);
expect(() => layer.updateSubtreeNeedsAddToScene(), throwsAssertionError);
expect(() => layer.dropChild(ContainerLayer()), throwsAssertionError);
expect(() => layer.adoptChild(ContainerLayer()), throwsAssertionError);
expect(() => (layer as ContainerLayer).append(ContainerLayer()), throwsAssertionError);
expect(() => layer.engineLayer = null, throwsAssertionError);
compositedB1 = true;
});
expect(compositedB1, false);
root.buildScene(SceneBuilder()).dispose();
expect(compositedB1, true);
});
test('Observe layer tree composition - detach triggers callback', () {
final ContainerLayer root = ContainerLayer();
final ContainerLayer a1 = ContainerLayer();
final ContainerLayer a2 = ContainerLayer();
final ContainerLayer b1 = ContainerLayer();
root.append(a1);
root.append(a2);
a1.append(b1);
bool compositedB1 = false;
b1.addCompositionCallback((Layer layer) {
expect(layer, b1);
compositedB1 = true;
});
root.attach(Object());
expect(compositedB1, false);
root.detach();
expect(compositedB1, true);
});
test('Observe layer tree composition - observer count correctly maintained', () {
final ContainerLayer root = ContainerLayer();
final ContainerLayer a1 = ContainerLayer();
root.append(a1);
expect(root.subtreeHasCompositionCallbacks, false);
expect(a1.subtreeHasCompositionCallbacks, false);
final VoidCallback remover1 = a1.addCompositionCallback((_) { });
final VoidCallback remover2 = a1.addCompositionCallback((_) { });
expect(root.subtreeHasCompositionCallbacks, true);
expect(a1.subtreeHasCompositionCallbacks, true);
remover1();
expect(root.subtreeHasCompositionCallbacks, true);
expect(a1.subtreeHasCompositionCallbacks, true);
remover2();
expect(root.subtreeHasCompositionCallbacks, false);
expect(a1.subtreeHasCompositionCallbacks, false);
});
} }
class FakeEngineLayer extends Fake implements EngineLayer { class FakeEngineLayer extends Fake implements EngineLayer {
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -246,8 +248,39 @@ void main() { ...@@ -246,8 +248,39 @@ void main() {
renderObject.dispose(); renderObject.dispose();
expect(renderObject.debugLayer, null); expect(renderObject.debugLayer, null);
}); });
test('Add composition callback works', () {
final ContainerLayer root = ContainerLayer();
final PaintingContext context = PaintingContext(root, Rect.zero);
bool calledBack = false;
final TestObservingRenderObject object = TestObservingRenderObject((Layer layer) {
expect(layer, root);
calledBack = true;
});
expect(calledBack, false);
object.paint(context, Offset.zero);
expect(calledBack, false);
root.buildScene(ui.SceneBuilder()).dispose();
expect(calledBack, true);
});
} }
class TestObservingRenderObject extends RenderBox {
TestObservingRenderObject(this.callback);
final CompositionCallback callback;
@override
bool get sizedByParent => true;
@override
void paint(PaintingContext context, Offset offset) {
context.addCompositionCallback(callback);
}
}
// Tests the create-update cycle by pumping two frames. The first frame has no // Tests the create-update cycle by pumping two frames. The first frame has no
// prior layer and forces the painting context to create a new one. The second // prior layer and forces the painting context to create a new one. The second
// frame reuses the layer painted on the first frame. // frame reuses the layer painted on the first frame.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment