Unverified Commit 34c69265 authored by Yegor's avatar Yegor Committed by GitHub

Teach render objects to reuse engine layers (#36402)

Teach Layer and its implementations, RenderObject and its implementations, and PaintingContext to reuse engine layers. The idea is that a concrete RenderObject creates a Layer and holds on to it as long as it needs it (i.e. when it is composited, and the layer type does not change). In return, each Layer object holds on to an EngineLayer and reports it to the engine via addRetained and oldLayer. This allows the Web engine to reuse DOM elements across frames. Without it, each frame drops all previously rendered HTML and regenerates it from scratch.
parent 1a5e4a5d
...@@ -242,12 +242,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ...@@ -242,12 +242,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
// Creates a [MouseTracker] which manages state about currently connected // Creates a [MouseTracker] which manages state about currently connected
// mice, for hover notification. // mice, for hover notification.
MouseTracker _createMouseTracker() { MouseTracker _createMouseTracker() {
return MouseTracker(pointerRouter, (Offset offset) { return MouseTracker(pointerRouter, renderView.hitTestMouseTrackers);
// Layer hit testing is done using device pixels, so we have to convert
// the logical coordinates of the event location back to device pixels
// here.
return renderView.layer.findAll<MouseTrackerAnnotation>(offset * window.devicePixelRatio);
});
} }
void _handleSemanticsEnabledChanged() { void _handleSemanticsEnabledChanged() {
......
...@@ -45,19 +45,49 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -45,19 +45,49 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
// Whether this layer has any changes since its last call to [addToScene]. // Whether this layer has any changes since its last call to [addToScene].
// //
// Initialized to true as a new layer has never called [addToScene]. // Initialized to true as a new layer has never called [addToScene], and is
// set to false after calling [addToScene]. The value can become true again
// if [markNeedsAddToScene] is called, or when [updateSubtreeNeedsAddToScene]
// is called on this layer or on an ancestor layer.
//
// The values of [_needsAddToScene] in a tree of layers are said to be
// _consistent_ if every layer in the tree satisfies the following:
//
// - If [alwaysNeedsAddToScene] is true, then [_needsAddToScene] is also true.
// - If [_needsAddToScene] is true and [parent] is not null, then
// `parent._needsAddToScene` is true.
//
// Typically, this value is set during the paint phase and during compositing.
// During the paint phase render objects create new layers and call
// [markNeedsAddToScene] on existing layers, causing this value to become
// true. After the paint phase the tree may be in an inconsistent state.
// During compositing [ContainerLayer.buildScene] first calls
// [updateSubtreeNeedsAddToScene] to bring this tree to a consistent state,
// then it calls [addToScene], and finally sets this field to false.
bool _needsAddToScene = true; bool _needsAddToScene = true;
/// Mark that this layer has changed and [addToScene] needs to be called. /// Mark that this layer has changed and [addToScene] needs to be called.
@protected @protected
@visibleForTesting @visibleForTesting
void markNeedsAddToScene() { void markNeedsAddToScene() {
assert(
!alwaysNeedsAddToScene,
'$runtimeType with alwaysNeedsAddToScene set called markNeedsAddToScene.\n'
'The layer\'s alwaysNeedsAddToScene is set to true, and therefore it should not call markNeedsAddToScene.',
);
// Already marked. Short-circuit.
if (_needsAddToScene) {
return;
}
_needsAddToScene = true; _needsAddToScene = true;
} }
/// Mark that this layer is in sync with engine. /// Mark that this layer is in sync with engine.
/// ///
/// This is only for debug and test purpose only. /// This is for debugging and testing purposes only. In release builds
/// this method has no effect.
@visibleForTesting @visibleForTesting
void debugMarkClean() { void debugMarkClean() {
assert(() { assert(() {
...@@ -70,9 +100,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -70,9 +100,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
@protected @protected
bool get alwaysNeedsAddToScene => false; bool get alwaysNeedsAddToScene => false;
bool _subtreeNeedsAddToScene; /// Whether this or any descendant layer in the subtree needs [addToScene].
/// Whether any layer in the subtree needs [addToScene].
/// ///
/// This is for debug and test purpose only. It only becomes valid after /// This is for debug and test purpose only. It only becomes valid after
/// calling [updateSubtreeNeedsAddToScene]. /// calling [updateSubtreeNeedsAddToScene].
...@@ -80,22 +108,77 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -80,22 +108,77 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
bool get debugSubtreeNeedsAddToScene { bool get debugSubtreeNeedsAddToScene {
bool result; bool result;
assert(() { assert(() {
result = _subtreeNeedsAddToScene; result = _needsAddToScene;
return true; return true;
}()); }());
return result; return result;
} }
/// Stores the engine layer created for this layer in order to reuse engine
/// resources across frames for better app performance.
///
/// This value may be passed to [ui.SceneBuilder.addRetained] to communicate
/// to the engine that nothing in this layer or any of its descendants
/// changed. The native engine could, for example, reuse the texture rendered
/// in a previous frame. The web engine could, for example, reuse the HTML
/// DOM nodes created for a previous frame.
///
/// This value may be passed as `oldLayer` argument to a "push" method to
/// communicate to the engine that a layer is updating a previously rendered
/// layer. The web engine could, for example, update the properties of
/// previously rendered HTML DOM nodes rather than creating new nodes.
@protected
ui.EngineLayer get engineLayer => _engineLayer;
/// Sets the engine layer used to render this layer.
///
/// Typically this field is set to the value returned by [addToScene], which
/// in turn returns the engine layer produced by one of [ui.SceneBuilder]'s
/// "push" methods, such as [ui.SceneBuilder.pushOpacity].
@protected
set engineLayer(ui.EngineLayer value) {
_engineLayer = value;
if (!alwaysNeedsAddToScene) {
// The parent must construct a new engine layer to add this layer to, and
// so we mark it as needing [addToScene].
//
// This is designed to handle two situations:
//
// 1. When rendering the complete layer tree as normal. In this case we
// call child `addToScene` methods first, then we call `set engineLayer`
// for the parent. The children will call `markNeedsAddToScene` on the
// parent to signal that they produced new engine layers and therefore
// the parent needs to update. In this case, the parent is already adding
// itself to the scene via [addToScene], and so after it's done, its
// `set engineLayer` is called and it clears the `_needsAddToScene` flag.
//
// 2. When rendering an interior layer (e.g. `OffsetLayer.toImage`). In
// this case we call `addToScene` for one of the children but not the
// parent, i.e. we produce new engine layers for children but not for the
// parent. Here the children will mark the parent as needing
// `addToScene`, but the parent does not clear the flag until some future
// frame decides to render it, at which point the parent knows that it
// cannot retain its engine layer and will call `addToScene` again.
if (parent != null && !parent.alwaysNeedsAddToScene) {
parent.markNeedsAddToScene();
}
}
}
ui.EngineLayer _engineLayer; ui.EngineLayer _engineLayer;
/// Traverse the layer tree and compute if any subtree needs [addToScene]. /// Traverses the layer subtree starting from this layer and determines whether it needs [addToScene].
///
/// A layer needs [addToScene] if any of the following is true:
/// ///
/// A subtree needs [addToScene] if any of its layers need [addToScene]. /// - [alwaysNeedsAddToScene] is true.
/// The [ContainerLayer] will override this to respect its children. /// - [markNeedsAddToScene] has been called.
/// - Any of its descendants need [addToScene].
///
/// [ContainerLayer] overrides this method to recursively call it on its children.
@protected @protected
@visibleForTesting @visibleForTesting
void updateSubtreeNeedsAddToScene() { void updateSubtreeNeedsAddToScene() {
_subtreeNeedsAddToScene = _needsAddToScene || alwaysNeedsAddToScene; _needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
} }
/// This layer's next sibling in the parent layer's child list. /// This layer's next sibling in the parent layer's child list.
...@@ -108,13 +191,17 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -108,13 +191,17 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
@override @override
void dropChild(AbstractNode child) { void dropChild(AbstractNode child) {
if (!alwaysNeedsAddToScene) {
markNeedsAddToScene(); markNeedsAddToScene();
}
super.dropChild(child); super.dropChild(child);
} }
@override @override
void adoptChild(AbstractNode child) { void adoptChild(AbstractNode child) {
if (!alwaysNeedsAddToScene) {
markNeedsAddToScene(); markNeedsAddToScene();
}
super.adoptChild(child); super.adoptChild(child);
} }
...@@ -157,23 +244,26 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -157,23 +244,26 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// Return the engine layer for retained rendering. When there's no /// Return the engine layer for retained rendering. When there's no
/// corresponding engine layer, null is returned. /// corresponding engine layer, null is returned.
@protected @protected
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]); void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]);
void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) { void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
// There can't be a loop by adding a retained layer subtree whose // There can't be a loop by adding a retained layer subtree whose
// _subtreeNeedsAddToScene is false. // _needsAddToScene is false.
// //
// Proof by contradiction: // Proof by contradiction:
// //
// If we introduce a loop, this retained layer must be appended to one of // If we introduce a loop, this retained layer must be appended to one of
// its descendant layers, say A. That means the child structure of A has // its descendant layers, say A. That means the child structure of A has
// changed so A's _needsAddToScene is true. This contradicts // changed so A's _needsAddToScene is true. This contradicts
// _subtreeNeedsAddToScene being false. // _needsAddToScene being false.
if (!_subtreeNeedsAddToScene && _engineLayer != null) { if (!_needsAddToScene && _engineLayer != null) {
builder.addRetained(_engineLayer); builder.addRetained(_engineLayer);
return; return;
} }
_engineLayer = addToScene(builder); addToScene(builder);
// Clearing the flag _after_ calling `addToScene`, not _before_. This is
// because `addToScene` calls children's `addToScene` methods, which may
// mark this layer as dirty.
_needsAddToScene = false; _needsAddToScene = false;
} }
...@@ -218,7 +308,7 @@ class PictureLayer extends Layer { ...@@ -218,7 +308,7 @@ class PictureLayer extends Layer {
ui.Picture get picture => _picture; ui.Picture get picture => _picture;
ui.Picture _picture; ui.Picture _picture;
set picture(ui.Picture picture) { set picture(ui.Picture picture) {
_needsAddToScene = true; markNeedsAddToScene();
_picture = picture; _picture = picture;
} }
...@@ -258,9 +348,8 @@ class PictureLayer extends Layer { ...@@ -258,9 +348,8 @@ class PictureLayer extends Layer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint); builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -329,7 +418,7 @@ class TextureLayer extends Layer { ...@@ -329,7 +418,7 @@ class TextureLayer extends Layer {
final bool freeze; final bool freeze;
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset); final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset);
builder.addTexture( builder.addTexture(
textureId, textureId,
...@@ -338,7 +427,6 @@ class TextureLayer extends Layer { ...@@ -338,7 +427,6 @@ class TextureLayer extends Layer {
height: shiftedRect.height, height: shiftedRect.height,
freeze: freeze, freeze: freeze,
); );
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -369,7 +457,7 @@ class PlatformViewLayer extends Layer { ...@@ -369,7 +457,7 @@ class PlatformViewLayer extends Layer {
final int viewId; final int viewId;
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset); final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset);
builder.addPlatformView( builder.addPlatformView(
viewId, viewId,
...@@ -377,7 +465,6 @@ class PlatformViewLayer extends Layer { ...@@ -377,7 +465,6 @@ class PlatformViewLayer extends Layer {
width: shiftedRect.width, width: shiftedRect.width,
height: shiftedRect.height, height: shiftedRect.height,
); );
return null;
} }
@override @override
...@@ -447,14 +534,13 @@ class PerformanceOverlayLayer extends Layer { ...@@ -447,14 +534,13 @@ class PerformanceOverlayLayer extends Layer {
final bool checkerboardOffscreenLayers; final bool checkerboardOffscreenLayers;
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(optionsMask != null); assert(optionsMask != null);
final Rect shiftedOverlayRect = layerOffset == Offset.zero ? overlayRect : overlayRect.shift(layerOffset); final Rect shiftedOverlayRect = layerOffset == Offset.zero ? overlayRect : overlayRect.shift(layerOffset);
builder.addPerformanceOverlay(optionsMask, shiftedOverlayRect); builder.addPerformanceOverlay(optionsMask, shiftedOverlayRect);
builder.setRasterizerTracingThreshold(rasterizerThreshold); builder.setRasterizerTracingThreshold(rasterizerThreshold);
builder.setCheckerboardRasterCacheImages(checkerboardRasterCacheImages); builder.setCheckerboardRasterCacheImages(checkerboardRasterCacheImages);
builder.setCheckerboardOffscreenLayers(checkerboardOffscreenLayers); builder.setCheckerboardOffscreenLayers(checkerboardOffscreenLayers);
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -478,6 +564,44 @@ class ContainerLayer extends Layer { ...@@ -478,6 +564,44 @@ class ContainerLayer extends Layer {
Layer get lastChild => _lastChild; Layer get lastChild => _lastChild;
Layer _lastChild; Layer _lastChild;
/// Returns whether this layer has at least one child layer.
bool get hasChildren => _firstChild != null;
/// Consider this layer as the root and build a scene (a tree of layers)
/// in the engine.
// The reason this method is in the `ContainerLayer` class rather than
// `PipelineOwner` or other singleton level is because this method can be used
// both to render the whole layer tree (e.g. a normal application frame) and
// to render a subtree (e.g. `OffsetLayer.toImage`).
ui.Scene buildScene(ui.SceneBuilder builder) {
List<PictureLayer> temporaryLayers;
assert(() {
if (debugCheckElevationsEnabled) {
temporaryLayers = _debugCheckElevations();
}
return true;
}());
updateSubtreeNeedsAddToScene();
addToScene(builder);
// Clearing the flag _after_ calling `addToScene`, not _before_. This is
// because `addToScene` calls children's `addToScene` methods, which may
// mark this layer as dirty.
_needsAddToScene = false;
final ui.Scene scene = builder.build();
assert(() {
// We should remove any layers that got added to highlight the incorrect
// PhysicalModelLayers. If we don't, we'll end up adding duplicate layers
// or continuing to render stale outlines.
if (temporaryLayers != null) {
for (PictureLayer temporaryLayer in temporaryLayers) {
temporaryLayer.remove();
}
}
return true;
}());
return scene;
}
bool _debugUltimatePreviousSiblingOf(Layer child, { Layer equals }) { bool _debugUltimatePreviousSiblingOf(Layer child, { Layer equals }) {
assert(child.attached == attached); assert(child.attached == attached);
while (child.previousSibling != null) { while (child.previousSibling != null) {
...@@ -598,7 +722,7 @@ class ContainerLayer extends Layer { ...@@ -598,7 +722,7 @@ class ContainerLayer extends Layer {
Layer child = firstChild; Layer child = firstChild;
while (child != null) { while (child != null) {
child.updateSubtreeNeedsAddToScene(); child.updateSubtreeNeedsAddToScene();
_subtreeNeedsAddToScene = _subtreeNeedsAddToScene || child._subtreeNeedsAddToScene; _needsAddToScene = _needsAddToScene || child._needsAddToScene;
child = child.nextSibling; child = child.nextSibling;
} }
} }
...@@ -721,9 +845,8 @@ class ContainerLayer extends Layer { ...@@ -721,9 +845,8 @@ class ContainerLayer extends Layer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
return null; // ContainerLayer does not have a corresponding engine layer
} }
/// Uploads all of this layer's children to the engine. /// Uploads all of this layer's children to the engine.
...@@ -867,45 +990,16 @@ class OffsetLayer extends ContainerLayer { ...@@ -867,45 +990,16 @@ class OffsetLayer extends ContainerLayer {
transform.multiply(Matrix4.translationValues(offset.dx, offset.dy, 0.0)); transform.multiply(Matrix4.translationValues(offset.dx, offset.dy, 0.0));
} }
/// Consider this layer as the root and build a scene (a tree of layers)
/// in the engine.
ui.Scene buildScene(ui.SceneBuilder builder) {
List<PictureLayer> temporaryLayers;
assert(() {
if (debugCheckElevationsEnabled) {
temporaryLayers = _debugCheckElevations();
}
return true;
}());
updateSubtreeNeedsAddToScene();
addToScene(builder);
final ui.Scene scene = builder.build();
assert(() {
// We should remove any layers that got added to highlight the incorrect
// PhysicalModelLayers. If we don't, we'll end up adding duplicate layers
// or potentially leaving a physical model that is now correct highlighted
// in red.
if (temporaryLayers != null) {
for (PictureLayer temporaryLayer in temporaryLayers) {
temporaryLayer.remove();
}
}
return true;
}());
return scene;
}
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
// Skia has a fast path for concatenating scale/translation only matrices. // Skia has a fast path for concatenating scale/translation only matrices.
// Hence pushing a translation-only transform layer should be fast. For // Hence pushing a translation-only transform layer should be fast. For
// retained rendering, we don't want to push the offset down to each leaf // retained rendering, we don't want to push the offset down to each leaf
// node. Otherwise, changing an offset layer on the very high level could // node. Otherwise, changing an offset layer on the very high level could
// cascade the change to too many leaves. // cascade the change to too many leaves.
final ui.EngineLayer engineLayer = builder.pushOffset(layerOffset.dx + offset.dx, layerOffset.dy + offset.dy); engineLayer = builder.pushOffset(layerOffset.dx + offset.dx, layerOffset.dy + offset.dy, oldLayer: _engineLayer);
addChildrenToScene(builder); addChildrenToScene(builder);
builder.pop(); builder.pop();
return engineLayer;
} }
@override @override
...@@ -942,6 +1036,7 @@ class OffsetLayer extends ContainerLayer { ...@@ -942,6 +1036,7 @@ class OffsetLayer extends ContainerLayer {
transform.scale(pixelRatio, pixelRatio); transform.scale(pixelRatio, pixelRatio);
builder.pushTransform(transform.storage); builder.pushTransform(transform.storage);
final ui.Scene scene = buildScene(builder); final ui.Scene scene = buildScene(builder);
try { try {
// Size is rounded up to the next pixel to make sure we don't clip off // Size is rounded up to the next pixel to make sure we don't clip off
// anything. // anything.
...@@ -963,10 +1058,10 @@ class OffsetLayer extends ContainerLayer { ...@@ -963,10 +1058,10 @@ class OffsetLayer extends ContainerLayer {
class ClipRectLayer extends ContainerLayer { class ClipRectLayer extends ContainerLayer {
/// Creates a layer with a rectangular clip. /// Creates a layer with a rectangular clip.
/// ///
/// The [clipRect] property must be non-null before the compositing phase of /// The [clipRect] and [clipBehavior] properties must be non-null before the
/// the pipeline. /// compositing phase of the pipeline.
ClipRectLayer({ ClipRectLayer({
@required Rect clipRect, Rect clipRect,
Clip clipBehavior = Clip.hardEdge, Clip clipBehavior = Clip.hardEdge,
}) : _clipRect = clipRect, }) : _clipRect = clipRect,
_clipBehavior = clipBehavior, _clipBehavior = clipBehavior,
...@@ -1017,7 +1112,9 @@ class ClipRectLayer extends ContainerLayer { ...@@ -1017,7 +1112,9 @@ class ClipRectLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(clipRect != null);
assert(clipBehavior != null);
bool enabled = true; bool enabled = true;
assert(() { assert(() {
enabled = !debugDisableClipLayers; enabled = !debugDisableClipLayers;
...@@ -1025,12 +1122,13 @@ class ClipRectLayer extends ContainerLayer { ...@@ -1025,12 +1122,13 @@ class ClipRectLayer extends ContainerLayer {
}()); }());
if (enabled) { if (enabled) {
final Rect shiftedClipRect = layerOffset == Offset.zero ? clipRect : clipRect.shift(layerOffset); final Rect shiftedClipRect = layerOffset == Offset.zero ? clipRect : clipRect.shift(layerOffset);
builder.pushClipRect(shiftedClipRect, clipBehavior: clipBehavior); engineLayer = builder.pushClipRect(shiftedClipRect, clipBehavior: clipBehavior, oldLayer: _engineLayer);
} else {
engineLayer = null;
} }
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
if (enabled) if (enabled)
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -1048,10 +1146,10 @@ class ClipRectLayer extends ContainerLayer { ...@@ -1048,10 +1146,10 @@ class ClipRectLayer extends ContainerLayer {
class ClipRRectLayer extends ContainerLayer { class ClipRRectLayer extends ContainerLayer {
/// Creates a layer with a rounded-rectangular clip. /// Creates a layer with a rounded-rectangular clip.
/// ///
/// The [clipRRect] property must be non-null before the compositing phase of /// The [clipRRect] and [clipBehavior] properties must be non-null before the
/// the pipeline. /// compositing phase of the pipeline.
ClipRRectLayer({ ClipRRectLayer({
@required RRect clipRRect, RRect clipRRect,
Clip clipBehavior = Clip.antiAlias, Clip clipBehavior = Clip.antiAlias,
}) : _clipRRect = clipRRect, }) : _clipRRect = clipRRect,
_clipBehavior = clipBehavior, _clipBehavior = clipBehavior,
...@@ -1098,7 +1196,9 @@ class ClipRRectLayer extends ContainerLayer { ...@@ -1098,7 +1196,9 @@ class ClipRRectLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(clipRRect != null);
assert(clipBehavior != null);
bool enabled = true; bool enabled = true;
assert(() { assert(() {
enabled = !debugDisableClipLayers; enabled = !debugDisableClipLayers;
...@@ -1106,12 +1206,13 @@ class ClipRRectLayer extends ContainerLayer { ...@@ -1106,12 +1206,13 @@ class ClipRRectLayer extends ContainerLayer {
}()); }());
if (enabled) { if (enabled) {
final RRect shiftedClipRRect = layerOffset == Offset.zero ? clipRRect : clipRRect.shift(layerOffset); final RRect shiftedClipRRect = layerOffset == Offset.zero ? clipRRect : clipRRect.shift(layerOffset);
builder.pushClipRRect(shiftedClipRRect, clipBehavior: clipBehavior); engineLayer = builder.pushClipRRect(shiftedClipRRect, clipBehavior: clipBehavior, oldLayer: _engineLayer);
} else {
engineLayer = null;
} }
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
if (enabled) if (enabled)
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -1129,10 +1230,10 @@ class ClipRRectLayer extends ContainerLayer { ...@@ -1129,10 +1230,10 @@ class ClipRRectLayer extends ContainerLayer {
class ClipPathLayer extends ContainerLayer { class ClipPathLayer extends ContainerLayer {
/// Creates a layer with a path-based clip. /// Creates a layer with a path-based clip.
/// ///
/// The [clipPath] property must be non-null before the compositing phase of /// The [clipPath] and [clipBehavior] properties must be non-null before the
/// the pipeline. /// compositing phase of the pipeline.
ClipPathLayer({ ClipPathLayer({
@required Path clipPath, Path clipPath,
Clip clipBehavior = Clip.antiAlias, Clip clipBehavior = Clip.antiAlias,
}) : _clipPath = clipPath, }) : _clipPath = clipPath,
_clipBehavior = clipBehavior, _clipBehavior = clipBehavior,
...@@ -1179,7 +1280,9 @@ class ClipPathLayer extends ContainerLayer { ...@@ -1179,7 +1280,9 @@ class ClipPathLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(clipPath != null);
assert(clipBehavior != null);
bool enabled = true; bool enabled = true;
assert(() { assert(() {
enabled = !debugDisableClipLayers; enabled = !debugDisableClipLayers;
...@@ -1187,12 +1290,13 @@ class ClipPathLayer extends ContainerLayer { ...@@ -1187,12 +1290,13 @@ class ClipPathLayer extends ContainerLayer {
}()); }());
if (enabled) { if (enabled) {
final Path shiftedPath = layerOffset == Offset.zero ? clipPath : clipPath.shift(layerOffset); final Path shiftedPath = layerOffset == Offset.zero ? clipPath : clipPath.shift(layerOffset);
builder.pushClipPath(shiftedPath, clipBehavior: clipBehavior); engineLayer = builder.pushClipPath(shiftedPath, clipBehavior: clipBehavior, oldLayer: _engineLayer);
} else {
engineLayer = null;
} }
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
if (enabled) if (enabled)
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
} }
...@@ -1200,12 +1304,11 @@ class ClipPathLayer extends ContainerLayer { ...@@ -1200,12 +1304,11 @@ class ClipPathLayer extends ContainerLayer {
class ColorFilterLayer extends ContainerLayer { class ColorFilterLayer extends ContainerLayer {
/// Creates a layer that applies a [ColorFilter] to its children. /// Creates a layer that applies a [ColorFilter] to its children.
/// ///
/// The [ColorFilter] property must be non-null before the compositing phase /// The [colorFilter] property must be non-null before the compositing phase
/// of the pipeline. /// of the pipeline.
ColorFilterLayer({ ColorFilterLayer({
@required ColorFilter colorFilter, ColorFilter colorFilter,
}) : _colorFilter = colorFilter, }) : _colorFilter = colorFilter;
assert(colorFilter != null);
/// The color filter to apply to children. /// The color filter to apply to children.
/// ///
...@@ -1214,6 +1317,7 @@ class ColorFilterLayer extends ContainerLayer { ...@@ -1214,6 +1317,7 @@ class ColorFilterLayer extends ContainerLayer {
ColorFilter get colorFilter => _colorFilter; ColorFilter get colorFilter => _colorFilter;
ColorFilter _colorFilter; ColorFilter _colorFilter;
set colorFilter(ColorFilter value) { set colorFilter(ColorFilter value) {
assert(value != null);
if (value != _colorFilter) { if (value != _colorFilter) {
_colorFilter = value; _colorFilter = value;
markNeedsAddToScene(); markNeedsAddToScene();
...@@ -1221,11 +1325,11 @@ class ColorFilterLayer extends ContainerLayer { ...@@ -1221,11 +1325,11 @@ class ColorFilterLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
builder.pushColorFilter(colorFilter); assert(colorFilter != null);
engineLayer = builder.pushColorFilter(colorFilter, oldLayer: _engineLayer);
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -1246,8 +1350,7 @@ class TransformLayer extends OffsetLayer { ...@@ -1246,8 +1350,7 @@ class TransformLayer extends OffsetLayer {
/// The [transform] and [offset] properties must be non-null before the /// The [transform] and [offset] properties must be non-null before the
/// compositing phase of the pipeline. /// compositing phase of the pipeline.
TransformLayer({ Matrix4 transform, Offset offset = Offset.zero }) TransformLayer({ Matrix4 transform, Offset offset = Offset.zero })
: assert(transform.storage.every((double value) => value.isFinite)), : _transform = transform,
_transform = transform,
super(offset: offset); super(offset: offset);
/// The matrix to apply. /// The matrix to apply.
...@@ -1262,10 +1365,13 @@ class TransformLayer extends OffsetLayer { ...@@ -1262,10 +1365,13 @@ class TransformLayer extends OffsetLayer {
Matrix4 get transform => _transform; Matrix4 get transform => _transform;
Matrix4 _transform; Matrix4 _transform;
set transform(Matrix4 value) { set transform(Matrix4 value) {
assert(value != null);
assert(value.storage.every((double component) => component.isFinite));
if (value == _transform) if (value == _transform)
return; return;
_transform = value; _transform = value;
_inverseDirty = true; _inverseDirty = true;
markNeedsAddToScene();
} }
Matrix4 _lastEffectiveTransform; Matrix4 _lastEffectiveTransform;
...@@ -1273,17 +1379,17 @@ class TransformLayer extends OffsetLayer { ...@@ -1273,17 +1379,17 @@ class TransformLayer extends OffsetLayer {
bool _inverseDirty = true; bool _inverseDirty = true;
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(transform != null);
_lastEffectiveTransform = transform; _lastEffectiveTransform = transform;
final Offset totalOffset = offset + layerOffset; final Offset totalOffset = offset + layerOffset;
if (totalOffset != Offset.zero) { if (totalOffset != Offset.zero) {
_lastEffectiveTransform = Matrix4.translationValues(totalOffset.dx, totalOffset.dy, 0.0) _lastEffectiveTransform = Matrix4.translationValues(totalOffset.dx, totalOffset.dy, 0.0)
..multiply(_lastEffectiveTransform); ..multiply(_lastEffectiveTransform);
} }
builder.pushTransform(_lastEffectiveTransform.storage); engineLayer = builder.pushTransform(_lastEffectiveTransform.storage, oldLayer: _engineLayer);
addChildrenToScene(builder); addChildrenToScene(builder);
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
Offset _transformOffset(Offset regionOffset) { Offset _transformOffset(Offset regionOffset) {
...@@ -1348,7 +1454,7 @@ class OpacityLayer extends ContainerLayer { ...@@ -1348,7 +1454,7 @@ class OpacityLayer extends ContainerLayer {
/// The [alpha] property must be non-null before the compositing phase of /// The [alpha] property must be non-null before the compositing phase of
/// the pipeline. /// the pipeline.
OpacityLayer({ OpacityLayer({
@required int alpha, int alpha,
Offset offset = Offset.zero, Offset offset = Offset.zero,
}) : _alpha = alpha, }) : _alpha = alpha,
_offset = offset; _offset = offset;
...@@ -1363,6 +1469,7 @@ class OpacityLayer extends ContainerLayer { ...@@ -1363,6 +1469,7 @@ class OpacityLayer extends ContainerLayer {
int get alpha => _alpha; int get alpha => _alpha;
int _alpha; int _alpha;
set alpha(int value) { set alpha(int value) {
assert(value != null);
if (value != _alpha) { if (value != _alpha) {
_alpha = value; _alpha = value;
markNeedsAddToScene(); markNeedsAddToScene();
...@@ -1387,18 +1494,21 @@ class OpacityLayer extends ContainerLayer { ...@@ -1387,18 +1494,21 @@ class OpacityLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(alpha != null);
bool enabled = firstChild != null; // don't add this layer if there's no child bool enabled = firstChild != null; // don't add this layer if there's no child
assert(() { assert(() {
enabled = enabled && !debugDisableOpacityLayers; enabled = enabled && !debugDisableOpacityLayers;
return true; return true;
}()); }());
if (enabled) if (enabled)
builder.pushOpacity(alpha, offset: offset + layerOffset); engineLayer = builder.pushOpacity(alpha, offset: offset + layerOffset, oldLayer: _engineLayer);
else
engineLayer = null;
addChildrenToScene(builder); addChildrenToScene(builder);
if (enabled) if (enabled)
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -1416,9 +1526,9 @@ class ShaderMaskLayer extends ContainerLayer { ...@@ -1416,9 +1526,9 @@ class ShaderMaskLayer extends ContainerLayer {
/// The [shader], [maskRect], and [blendMode] properties must be non-null /// The [shader], [maskRect], and [blendMode] properties must be non-null
/// before the compositing phase of the pipeline. /// before the compositing phase of the pipeline.
ShaderMaskLayer({ ShaderMaskLayer({
@required Shader shader, Shader shader,
@required Rect maskRect, Rect maskRect,
@required BlendMode blendMode, BlendMode blendMode,
}) : _shader = shader, }) : _shader = shader,
_maskRect = maskRect, _maskRect = maskRect,
_blendMode = blendMode; _blendMode = blendMode;
...@@ -1463,12 +1573,14 @@ class ShaderMaskLayer extends ContainerLayer { ...@@ -1463,12 +1573,14 @@ class ShaderMaskLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(shader != null);
assert(maskRect != null);
assert(blendMode != null);
final Rect shiftedMaskRect = layerOffset == Offset.zero ? maskRect : maskRect.shift(layerOffset); final Rect shiftedMaskRect = layerOffset == Offset.zero ? maskRect : maskRect.shift(layerOffset);
builder.pushShaderMask(shader, shiftedMaskRect, blendMode); engineLayer = builder.pushShaderMask(shader, shiftedMaskRect, blendMode, oldLayer: _engineLayer);
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
@override @override
...@@ -1486,7 +1598,7 @@ class BackdropFilterLayer extends ContainerLayer { ...@@ -1486,7 +1598,7 @@ class BackdropFilterLayer extends ContainerLayer {
/// ///
/// The [filter] property must be non-null before the compositing phase of the /// The [filter] property must be non-null before the compositing phase of the
/// pipeline. /// pipeline.
BackdropFilterLayer({ @required ui.ImageFilter filter }) : _filter = filter; BackdropFilterLayer({ ui.ImageFilter filter }) : _filter = filter;
/// The filter to apply to the existing contents of the scene. /// The filter to apply to the existing contents of the scene.
/// ///
...@@ -1502,11 +1614,11 @@ class BackdropFilterLayer extends ContainerLayer { ...@@ -1502,11 +1614,11 @@ class BackdropFilterLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
builder.pushBackdropFilter(filter); assert(filter != null);
engineLayer = builder.pushBackdropFilter(filter, oldLayer: _engineLayer);
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
builder.pop(); builder.pop();
return null; // this does not return an engine layer yet.
} }
} }
...@@ -1523,19 +1635,15 @@ class PhysicalModelLayer extends ContainerLayer { ...@@ -1523,19 +1635,15 @@ class PhysicalModelLayer extends ContainerLayer {
/// Creates a composited layer that uses a physical model to producing /// Creates a composited layer that uses a physical model to producing
/// lighting effects. /// lighting effects.
/// ///
/// The [clipPath], [elevation], and [color] arguments must not be null. /// The [clipPath], [clipBehavior], [elevation], [color], and [shadowColor]
/// arguments must be non-null before the compositing phase of the pipeline.
PhysicalModelLayer({ PhysicalModelLayer({
@required Path clipPath, Path clipPath,
Clip clipBehavior = Clip.none, Clip clipBehavior = Clip.none,
@required double elevation, double elevation,
@required Color color, Color color,
@required Color shadowColor, Color shadowColor,
}) : assert(clipPath != null), }) : _clipPath = clipPath,
assert(clipBehavior != null),
assert(elevation != null),
assert(color != null),
assert(shadowColor != null),
_clipPath = clipPath,
_clipBehavior = clipBehavior, _clipBehavior = clipBehavior,
_elevation = elevation, _elevation = elevation,
_color = color, _color = color,
...@@ -1632,8 +1740,13 @@ class PhysicalModelLayer extends ContainerLayer { ...@@ -1632,8 +1740,13 @@ class PhysicalModelLayer extends ContainerLayer {
} }
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
ui.EngineLayer engineLayer; assert(clipPath != null);
assert(clipBehavior != null);
assert(elevation != null);
assert(color != null);
assert(shadowColor != null);
bool enabled = true; bool enabled = true;
assert(() { assert(() {
enabled = !debugDisablePhysicalShapeLayers; enabled = !debugDisablePhysicalShapeLayers;
...@@ -1646,12 +1759,14 @@ class PhysicalModelLayer extends ContainerLayer { ...@@ -1646,12 +1759,14 @@ class PhysicalModelLayer extends ContainerLayer {
color: color, color: color,
shadowColor: shadowColor, shadowColor: shadowColor,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
oldLayer: _engineLayer,
); );
} else {
engineLayer = null;
} }
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
if (enabled) if (enabled)
builder.pop(); builder.pop();
return engineLayer;
} }
@override @override
...@@ -1697,13 +1812,18 @@ class LeaderLayer extends ContainerLayer { ...@@ -1697,13 +1812,18 @@ class LeaderLayer extends ContainerLayer {
/// ///
/// The [offset] property must be non-null before the compositing phase of the /// The [offset] property must be non-null before the compositing phase of the
/// pipeline. /// pipeline.
LeaderLayer({ @required this.link, this.offset = Offset.zero }) : assert(link != null); LeaderLayer({ @required LayerLink link, this.offset = Offset.zero }) : assert(link != null), _link = link;
/// The object with which this layer should register. /// The object with which this layer should register.
/// ///
/// The link will be established when this layer is [attach]ed, and will be /// The link will be established when this layer is [attach]ed, and will be
/// cleared when this layer is [detach]ed. /// cleared when this layer is [detach]ed.
final LayerLink link; LayerLink get link => _link;
set link(LayerLink value) {
assert(value != null);
_link = value;
}
LayerLink _link;
/// Offset from parent in the parent's coordinate system. /// Offset from parent in the parent's coordinate system.
/// ///
...@@ -1748,15 +1868,14 @@ class LeaderLayer extends ContainerLayer { ...@@ -1748,15 +1868,14 @@ class LeaderLayer extends ContainerLayer {
Iterable<S> findAll<S>(Offset regionOffset) => super.findAll<S>(regionOffset - offset); Iterable<S> findAll<S>(Offset regionOffset) => super.findAll<S>(regionOffset - offset);
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(offset != null); assert(offset != null);
_lastOffset = offset + layerOffset; _lastOffset = offset + layerOffset;
if (_lastOffset != Offset.zero) if (_lastOffset != Offset.zero)
builder.pushTransform(Matrix4.translationValues(_lastOffset.dx, _lastOffset.dy, 0.0).storage); engineLayer = builder.pushTransform(Matrix4.translationValues(_lastOffset.dx, _lastOffset.dy, 0.0).storage, oldLayer: _engineLayer);
addChildrenToScene(builder); addChildrenToScene(builder);
if (_lastOffset != Offset.zero) if (_lastOffset != Offset.zero)
builder.pop(); builder.pop();
return null; // this does not have an engine layer.
} }
/// Applies the transform that would be applied when compositing the given /// Applies the transform that would be applied when compositing the given
...@@ -1799,18 +1918,23 @@ class FollowerLayer extends ContainerLayer { ...@@ -1799,18 +1918,23 @@ class FollowerLayer extends ContainerLayer {
/// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties /// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties
/// must be non-null before the compositing phase of the pipeline. /// must be non-null before the compositing phase of the pipeline.
FollowerLayer({ FollowerLayer({
@required this.link, @required LayerLink link,
this.showWhenUnlinked = true, this.showWhenUnlinked = true,
this.unlinkedOffset = Offset.zero, this.unlinkedOffset = Offset.zero,
this.linkedOffset = Offset.zero, this.linkedOffset = Offset.zero,
}) : assert(link != null); }) : assert(link != null), _link = link;
/// The link to the [LeaderLayer]. /// The link to the [LeaderLayer].
/// ///
/// The same object should be provided to a [LeaderLayer] that is earlier in /// The same object should be provided to a [LeaderLayer] that is earlier in
/// the layer tree. When this layer is composited, it will apply a transform /// the layer tree. When this layer is composited, it will apply a transform
/// that moves its children to match the position of the [LeaderLayer]. /// that moves its children to match the position of the [LeaderLayer].
final LayerLink link; LayerLink get link => _link;
set link(LayerLink value) {
assert(value != null);
_link = value;
}
LayerLink _link;
/// Whether to show the layer's contents when the [link] does not point to a /// Whether to show the layer's contents when the [link] does not point to a
/// [LeaderLayer]. /// [LeaderLayer].
...@@ -1970,43 +2094,43 @@ class FollowerLayer extends ContainerLayer { ...@@ -1970,43 +2094,43 @@ class FollowerLayer extends ContainerLayer {
} }
/// {@template flutter.leaderFollower.alwaysNeedsAddToScene} /// {@template flutter.leaderFollower.alwaysNeedsAddToScene}
/// This disables retained rendering for Leader/FollowerLayer. /// This disables retained rendering.
/// ///
/// A FollowerLayer copies changes from a LeaderLayer that could be anywhere /// A [FollowerLayer] copies changes from a [LeaderLayer] that could be anywhere
/// in the Layer tree, and that LeaderLayer could change without notifying the /// in the Layer tree, and that leader layer could change without notifying the
/// FollowerLayer. Therefore we have to always call a FollowerLayer's /// follower layer. Therefore we have to always call a follower layer's
/// [addToScene]. In order to call FollowerLayer's [addToScene], LeaderLayer's /// [addToScene]. In order to call follower layer's [addToScene], leader layer's
/// [addToScene] must be called first so LeaderLayer must also be considered /// [addToScene] must be called first so leader layer must also be considered
/// as [alwaysNeedsAddToScene]. /// as [alwaysNeedsAddToScene].
/// {@endtemplate} /// {@endtemplate}
@override @override
bool get alwaysNeedsAddToScene => true; bool get alwaysNeedsAddToScene => true;
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(link != null); assert(link != null);
assert(showWhenUnlinked != null); assert(showWhenUnlinked != null);
if (link.leader == null && !showWhenUnlinked) { if (link.leader == null && !showWhenUnlinked) {
_lastTransform = null; _lastTransform = null;
_lastOffset = null; _lastOffset = null;
_inverseDirty = true; _inverseDirty = true;
return null; // this does not have an engine layer. engineLayer = null;
return;
} }
_establishTransform(); _establishTransform();
if (_lastTransform != null) { if (_lastTransform != null) {
builder.pushTransform(_lastTransform.storage); engineLayer = builder.pushTransform(_lastTransform.storage, oldLayer: _engineLayer);
addChildrenToScene(builder); addChildrenToScene(builder);
builder.pop(); builder.pop();
_lastOffset = unlinkedOffset + layerOffset; _lastOffset = unlinkedOffset + layerOffset;
} else { } else {
_lastOffset = null; _lastOffset = null;
final Matrix4 matrix = Matrix4.translationValues(unlinkedOffset.dx, unlinkedOffset.dy, .0); final Matrix4 matrix = Matrix4.translationValues(unlinkedOffset.dx, unlinkedOffset.dy, .0);
builder.pushTransform(matrix.storage); engineLayer = builder.pushTransform(matrix.storage, oldLayer: _engineLayer);
addChildrenToScene(builder); addChildrenToScene(builder);
builder.pop(); builder.pop();
} }
_inverseDirty = true; _inverseDirty = true;
return null; // this does not have an engine layer.
} }
@override @override
......
...@@ -112,19 +112,31 @@ class PaintingContext extends ClipContext { ...@@ -112,19 +112,31 @@ class PaintingContext extends ClipContext {
); );
return true; return true;
}()); }());
if (child._layer == null) { OffsetLayer childLayer = child._layer;
if (childLayer == null) {
assert(debugAlsoPaintedParent); assert(debugAlsoPaintedParent);
child._layer = OffsetLayer(); // Not using the `layer` setter because the setter asserts that we not
// replace the layer for repaint boundaries. That assertion does not
// apply here because this is exactly the place designed to create a
// layer for repaint boundaries.
child._layer = childLayer = OffsetLayer();
} else { } else {
assert(debugAlsoPaintedParent || child._layer.attached); assert(childLayer is OffsetLayer);
child._layer.removeAllChildren(); assert(debugAlsoPaintedParent || childLayer.attached);
childLayer.removeAllChildren();
} }
assert(identical(childLayer, child._layer));
assert(child._layer is OffsetLayer);
assert(() { assert(() {
child._layer.debugCreator = child.debugCreator ?? child.runtimeType; child._layer.debugCreator = child.debugCreator ?? child.runtimeType;
return true; return true;
}()); }());
childContext ??= PaintingContext(child._layer, child.paintBounds); childContext ??= PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero); child._paintWithContext(childContext, Offset.zero);
// Double-check that the paint method did not replace the layer (the first
// check is done in the [layer] setter itself).
assert(identical(childLayer, child._layer));
childContext.stopRecordingIfNeeded(); childContext.stopRecordingIfNeeded();
} }
...@@ -188,7 +200,6 @@ class PaintingContext extends ClipContext { ...@@ -188,7 +200,6 @@ class PaintingContext extends ClipContext {
if (child._needsPaint) { if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true); repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else { } else {
assert(child._layer != null);
assert(() { assert(() {
// register the call for RepaintBoundary metrics // register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint( child.debugRegisterRepaintBoundaryPaint(
...@@ -199,8 +210,9 @@ class PaintingContext extends ClipContext { ...@@ -199,8 +210,9 @@ class PaintingContext extends ClipContext {
return true; return true;
}()); }());
} }
assert(child._layer != null); assert(child._layer is OffsetLayer);
child._layer.offset = offset; final OffsetLayer childOffsetLayer = child._layer;
childOffsetLayer.offset = offset;
appendLayer(child._layer); appendLayer(child._layer);
} }
...@@ -360,9 +372,12 @@ class PaintingContext extends ClipContext { ...@@ -360,9 +372,12 @@ class PaintingContext extends ClipContext {
/// ///
/// * [addLayer], for pushing a leaf layer whose canvas is not used. /// * [addLayer], for pushing a leaf layer whose canvas is not used.
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) { void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
assert(!childLayer.attached);
assert(childLayer.parent == null);
assert(painter != null); assert(painter != null);
// If a layer is being reused, it may already contain children. We remove
// them so that `painter` can add children that are relevant for this frame.
if (childLayer.hasChildren) {
childLayer.removeAllChildren();
}
stopRecordingIfNeeded(); stopRecordingIfNeeded();
appendLayer(childLayer); appendLayer(childLayer);
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds); final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
...@@ -378,28 +393,47 @@ class PaintingContext extends ClipContext { ...@@ -378,28 +393,47 @@ class PaintingContext extends ClipContext {
/// Clip further painting using a rectangle. /// Clip further painting using a rectangle.
/// ///
/// {@template flutter.rendering.object.needsCompositing}
/// * `needsCompositing` is whether the child needs compositing. Typically /// * `needsCompositing` is whether the child needs compositing. Typically
/// matches the value of [RenderObject.needsCompositing] for the caller. /// matches the value of [RenderObject.needsCompositing] for the caller. If
/// * `offset` is the offset from the origin of the canvas's coordinate system /// false, this method returns null, indicating that a layer is no longer
/// necessary. If a render object calling this method stores the `oldLayer`
/// in its [RenderObject.layer] field, it should set that field to null.
/// {@end template}
/// * `offset` is the offset from the origin of the canvas' coordinate system
/// to the origin of the caller's coordinate system. /// to the origin of the caller's coordinate system.
/// * `clipRect` is rectangle (in the caller's coordinate system) to use to /// * `clipRect` is rectangle (in the caller's coordinate system) to use to
/// clip the painting done by [painter]. /// clip the painting done by [painter].
/// * `painter` is a callback that will paint with the [clipRect] applied. This /// * `painter` is a callback that will paint with the [clipRect] applied. This
/// function calls the [painter] synchronously. /// function calls the [painter] synchronously.
/// * `clipBehavior` controls how the rectangle is clipped. /// * `clipBehavior` controls how the rectangle is clipped.
void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge }) { /// {@template flutter.rendering.object.oldLayer}
/// * `oldLayer` is the layer created in the previous frame. Specifying the
/// old layer gives the engine more information for performance
/// optimizations. Typically this is the value of [RenderObject.layer] that
/// a render object creates once, then reuses for all subsequent frames
/// until a layer is no longer needed (e.g. the render object no longer
/// needs compositing) or until the render object changes the type of the
/// layer (e.g. from opacity layer to a clip rect layer).
/// {@end template}
ClipRectLayer pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge, ClipRectLayer oldLayer }) {
final Rect offsetClipRect = clipRect.shift(offset); final Rect offsetClipRect = clipRect.shift(offset);
if (needsCompositing) { if (needsCompositing) {
pushLayer(ClipRectLayer(clipRect: offsetClipRect, clipBehavior: clipBehavior), painter, offset, childPaintBounds: offsetClipRect); final ClipRectLayer layer = oldLayer ?? ClipRectLayer();
layer
..clipRect = offsetClipRect
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetClipRect);
return layer;
} else { } else {
clipRectAndPaint(offsetClipRect, clipBehavior, offsetClipRect, () => painter(this, offset)); clipRectAndPaint(offsetClipRect, clipBehavior, offsetClipRect, () => painter(this, offset));
return null;
} }
} }
/// Clip further painting using a rounded rectangle. /// Clip further painting using a rounded rectangle.
/// ///
/// * `needsCompositing` is whether the child needs compositing. Typically /// {@macro flutter.rendering.object.needsCompositing}
/// matches the value of [RenderObject.needsCompositing] for the caller.
/// * `offset` is the offset from the origin of the canvas' coordinate system /// * `offset` is the offset from the origin of the canvas' coordinate system
/// to the origin of the caller's coordinate system. /// to the origin of the caller's coordinate system.
/// * `bounds` is the region of the canvas (in the caller's coordinate system) /// * `bounds` is the region of the canvas (in the caller's coordinate system)
...@@ -409,21 +443,27 @@ class PaintingContext extends ClipContext { ...@@ -409,21 +443,27 @@ class PaintingContext extends ClipContext {
/// * `painter` is a callback that will paint with the `clipRRect` applied. This /// * `painter` is a callback that will paint with the `clipRRect` applied. This
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
/// * `clipBehavior` controls how the path is clipped. /// * `clipBehavior` controls how the path is clipped.
void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) { /// {@macro flutter.rendering.object.oldLayer}
ClipRRectLayer pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipRRectLayer oldLayer }) {
assert(clipBehavior != null); assert(clipBehavior != null);
final Rect offsetBounds = bounds.shift(offset); final Rect offsetBounds = bounds.shift(offset);
final RRect offsetClipRRect = clipRRect.shift(offset); final RRect offsetClipRRect = clipRRect.shift(offset);
if (needsCompositing) { if (needsCompositing) {
pushLayer(ClipRRectLayer(clipRRect: offsetClipRRect, clipBehavior: clipBehavior), painter, offset, childPaintBounds: offsetBounds); final ClipRRectLayer layer = oldLayer ?? ClipRRectLayer();
layer
..clipRRect = offsetClipRRect
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
return layer;
} else { } else {
clipRRectAndPaint(offsetClipRRect, clipBehavior, offsetBounds, () => painter(this, offset)); clipRRectAndPaint(offsetClipRRect, clipBehavior, offsetBounds, () => painter(this, offset));
return null;
} }
} }
/// Clip further painting using a path. /// Clip further painting using a path.
/// ///
/// * `needsCompositing` is whether the child needs compositing. Typically /// {@macro flutter.rendering.object.needsCompositing}
/// matches the value of [RenderObject.needsCompositing] for the caller.
/// * `offset` is the offset from the origin of the canvas' coordinate system /// * `offset` is the offset from the origin of the canvas' coordinate system
/// to the origin of the caller's coordinate system. /// to the origin of the caller's coordinate system.
/// * `bounds` is the region of the canvas (in the caller's coordinate system) /// * `bounds` is the region of the canvas (in the caller's coordinate system)
...@@ -433,14 +473,21 @@ class PaintingContext extends ClipContext { ...@@ -433,14 +473,21 @@ class PaintingContext extends ClipContext {
/// * `painter` is a callback that will paint with the `clipPath` applied. This /// * `painter` is a callback that will paint with the `clipPath` applied. This
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
/// * `clipBehavior` controls how the rounded rectangle is clipped. /// * `clipBehavior` controls how the rounded rectangle is clipped.
void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) { /// {@macro flutter.rendering.object.oldLayer}
ClipPathLayer pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipPathLayer oldLayer }) {
assert(clipBehavior != null); assert(clipBehavior != null);
final Rect offsetBounds = bounds.shift(offset); final Rect offsetBounds = bounds.shift(offset);
final Path offsetClipPath = clipPath.shift(offset); final Path offsetClipPath = clipPath.shift(offset);
if (needsCompositing) { if (needsCompositing) {
pushLayer(ClipPathLayer(clipPath: offsetClipPath, clipBehavior: clipBehavior), painter, offset, childPaintBounds: offsetBounds); final ClipPathLayer layer = oldLayer ?? ClipPathLayer();
layer
..clipPath = offsetClipPath
..clipBehavior = clipBehavior;
pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
return layer;
} else { } else {
clipPathAndPaint(offsetClipPath, clipBehavior, offsetBounds, () => painter(this, offset)); clipPathAndPaint(offsetClipPath, clipBehavior, offsetBounds, () => painter(this, offset));
return null;
} }
} }
...@@ -452,35 +499,42 @@ class PaintingContext extends ClipContext { ...@@ -452,35 +499,42 @@ class PaintingContext extends ClipContext {
/// painting done by `painter`. /// painting done by `painter`.
/// * `painter` is a callback that will paint with the `colorFilter` applied. /// * `painter` is a callback that will paint with the `colorFilter` applied.
/// This function calls the `painter` synchronously. /// This function calls the `painter` synchronously.
/// {@macro flutter.rendering.object.oldLayer}
/// ///
/// A [RenderObject] that uses this function is very likely to require its /// A [RenderObject] that uses this function is very likely to require its
/// [RenderObject.alwaysNeedsCompositing] property to return true. That informs /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs
/// ancestor render objects that this render object will include a composited /// ancestor render objects that this render object will include a composited
/// layer, which, for example, causes them to use composited clips. /// layer, which, for example, causes them to use composited clips.
void pushColorFilter(Offset offset, ColorFilter colorFilter, PaintingContextCallback painter) { ColorFilterLayer pushColorFilter(Offset offset, ColorFilter colorFilter, PaintingContextCallback painter, { ColorFilterLayer oldLayer }) {
assert(colorFilter != null); assert(colorFilter != null);
pushLayer(ColorFilterLayer(colorFilter: colorFilter), painter, offset); final ColorFilterLayer layer = oldLayer ?? ColorFilterLayer();
layer.colorFilter = colorFilter;
pushLayer(layer, painter, offset);
return layer;
} }
/// Transform further painting using a matrix. /// Transform further painting using a matrix.
/// ///
/// * `needsCompositing` is whether the child needs compositing. Typically /// {@macro flutter.rendering.object.needsCompositing}
/// matches the value of [RenderObject.needsCompositing] for the caller.
/// * `offset` is the offset from the origin of the canvas' coordinate system /// * `offset` is the offset from the origin of the canvas' coordinate system
/// to the origin of the caller's coordinate system. /// to the origin of the caller's coordinate system.
/// * `transform` is the matrix to apply to the painting done by `painter`. /// * `transform` is the matrix to apply to the painting done by `painter`.
/// * `painter` is a callback that will paint with the `transform` applied. This /// * `painter` is a callback that will paint with the `transform` applied. This
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) { /// {@macro flutter.rendering.object.oldLayer}
TransformLayer pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter, { TransformLayer oldLayer }) {
final Matrix4 effectiveTransform = Matrix4.translationValues(offset.dx, offset.dy, 0.0) final Matrix4 effectiveTransform = Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(transform)..translate(-offset.dx, -offset.dy); ..multiply(transform)..translate(-offset.dx, -offset.dy);
if (needsCompositing) { if (needsCompositing) {
final TransformLayer layer = oldLayer ?? TransformLayer();
layer.transform = effectiveTransform;
pushLayer( pushLayer(
TransformLayer(transform: effectiveTransform), layer,
painter, painter,
offset, offset,
childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, estimatedBounds), childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, estimatedBounds),
); );
return layer;
} else { } else {
canvas canvas
..save() ..save()
...@@ -488,6 +542,7 @@ class PaintingContext extends ClipContext { ...@@ -488,6 +542,7 @@ class PaintingContext extends ClipContext {
painter(this, offset); painter(this, offset);
canvas canvas
..restore(); ..restore();
return null;
} }
} }
...@@ -500,13 +555,19 @@ class PaintingContext extends ClipContext { ...@@ -500,13 +555,19 @@ class PaintingContext extends ClipContext {
/// and an alpha value of 255 means the painting is fully opaque. /// and an alpha value of 255 means the painting is fully opaque.
/// * `painter` is a callback that will paint with the `alpha` applied. This /// * `painter` is a callback that will paint with the `alpha` applied. This
/// function calls the `painter` synchronously. /// function calls the `painter` synchronously.
/// {@macro flutter.rendering.object.oldLayer}
/// ///
/// A [RenderObject] that uses this function is very likely to require its /// A [RenderObject] that uses this function is very likely to require its
/// [RenderObject.alwaysNeedsCompositing] property to return true. That informs /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs
/// ancestor render objects that this render object will include a composited /// ancestor render objects that this render object will include a composited
/// layer, which, for example, causes them to use composited clips. /// layer, which, for example, causes them to use composited clips.
void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) { OpacityLayer pushOpacity(Offset offset, int alpha, PaintingContextCallback painter, { OpacityLayer oldLayer }) {
pushLayer(OpacityLayer(alpha: alpha, offset: offset), painter, Offset.zero); final OpacityLayer layer = oldLayer ?? OpacityLayer();
layer
..alpha = alpha
..offset = offset;
pushLayer(layer, painter, Offset.zero);
return layer;
} }
@override @override
...@@ -1781,7 +1842,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1781,7 +1842,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// to repaint. /// to repaint.
/// ///
/// If this getter returns true, the [paintBounds] are applied to this object /// If this getter returns true, the [paintBounds] are applied to this object
/// and all descendants. /// and all descendants. The framework automatically creates an [OffsetLayer]
/// and assigns it to the [layer] field. Render objects that declare
/// themselves as repaint boundaries must not replace the layer created by
/// the framework.
/// ///
/// Warning: This getter must not change value over the lifetime of this object. /// Warning: This getter must not change value over the lifetime of this object.
bool get isRepaintBoundary => false; bool get isRepaintBoundary => false;
...@@ -1804,19 +1868,41 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1804,19 +1868,41 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@protected @protected
bool get alwaysNeedsCompositing => false; bool get alwaysNeedsCompositing => false;
OffsetLayer _layer;
/// The compositing layer that this render object uses to repaint. /// The compositing layer that this render object uses to repaint.
/// ///
/// Call only when [isRepaintBoundary] is true and the render object has /// If this render object is not a repaint boundary, it is the responsibility
/// already painted. /// of the [paint] method to populate this field. If [needsCompositing] is
/// /// true, this field may be populated with the root-most layer used by the
/// To access the layer in debug code, even when it might be inappropriate to /// render object implementation. When repainting, instead of creating a new
/// access it (e.g. because it is dirty), consider [debugLayer]. /// layer the render object may update the layer stored in this field for better
OffsetLayer get layer { /// performance. It is also OK to leave this field as null and create a new
assert(isRepaintBoundary, 'You can only access RenderObject.layer for render objects that are repaint boundaries.'); /// layer on every repaint, but without the performance benefit. If
assert(!_needsPaint); /// [needsCompositing] is false, this field must be set to null either by
/// never populating this field, or by setting it to null when the value of
/// [needsCompositing] changes from true to false.
///
/// If this render object is a repaint boundary, the framework automatically
/// creates an [OffsetLayer] and populates this field prior to calling the
/// [paint] method. The [paint] method must not replace the value of this
/// field.
@protected
ContainerLayer get layer {
assert(!isRepaintBoundary || (_layer == null || _layer is OffsetLayer));
return _layer; return _layer;
} }
@protected
set layer(ContainerLayer newLayer) {
assert(
!isRepaintBoundary,
'Attempted to set a layer to a repaint boundary render object.\n'
'The framework creates and assigns an OffsetLayer to a repaint '
'boundary automatically.',
);
_layer = newLayer;
}
ContainerLayer _layer;
/// In debug mode, the compositing layer that this render object uses to repaint. /// In debug mode, the compositing layer that this render object uses to repaint.
/// ///
/// This getter is intended for debugging purposes only. In release builds, it /// This getter is intended for debugging purposes only. In release builds, it
...@@ -1824,8 +1910,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1824,8 +1910,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// is dirty. /// is dirty.
/// ///
/// For production code, consider [layer]. /// For production code, consider [layer].
OffsetLayer get debugLayer { ContainerLayer get debugLayer {
OffsetLayer result; ContainerLayer result;
assert(() { assert(() {
result = _layer; result = _layer;
return true; return true;
...@@ -1961,16 +2047,12 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -1961,16 +2047,12 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}()); }());
// If we always have our own layer, then we can just repaint // If we always have our own layer, then we can just repaint
// ourselves without involving any other nodes. // ourselves without involving any other nodes.
assert(_layer != null); assert(_layer is OffsetLayer);
if (owner != null) { if (owner != null) {
owner._nodesNeedingPaint.add(this); owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate(); owner.requestVisualUpdate();
} }
} else if (parent is RenderObject) { } else if (parent is RenderObject) {
// We don't have our own layer; one of our ancestors will take
// care of updating the layer we're in and when they do that
// we'll get our paint() method called.
assert(_layer == null);
final RenderObject parent = this.parent; final RenderObject parent = this.parent;
parent.markNeedsPaint(); parent.markNeedsPaint();
assert(parent == this.parent); assert(parent == this.parent);
...@@ -2682,7 +2764,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2682,7 +2764,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
properties.add(DiagnosticsProperty<ParentData>('parentData', parentData, tooltip: _debugCanParentUseSize == true ? 'can use size' : null, missingIfNull: true)); properties.add(DiagnosticsProperty<ParentData>('parentData', parentData, tooltip: _debugCanParentUseSize == true ? 'can use size' : null, missingIfNull: true));
properties.add(DiagnosticsProperty<Constraints>('constraints', constraints, missingIfNull: true)); properties.add(DiagnosticsProperty<Constraints>('constraints', constraints, missingIfNull: true));
// don't access it via the "layer" getter since that's only valid when we don't need paint // don't access it via the "layer" getter since that's only valid when we don't need paint
properties.add(DiagnosticsProperty<OffsetLayer>('layer', _layer, defaultValue: null)); properties.add(DiagnosticsProperty<ContainerLayer>('layer', _layer, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsNode>('semantics node', _semantics, defaultValue: null)); properties.add(DiagnosticsProperty<SemanticsNode>('semantics node', _semantics, defaultValue: null));
properties.add(FlagProperty( properties.add(FlagProperty(
'isBlockingSemanticsOfPreviouslyPaintedNodes', 'isBlockingSemanticsOfPreviouslyPaintedNodes',
......
...@@ -789,14 +789,18 @@ class RenderOpacity extends RenderProxyBox { ...@@ -789,14 +789,18 @@ class RenderOpacity extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
if (_alpha == 0) { if (_alpha == 0) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
return; return;
} }
if (_alpha == 255) { if (_alpha == 255) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
context.paintChild(child, offset); context.paintChild(child, offset);
return; return;
} }
assert(needsCompositing); assert(needsCompositing);
context.pushOpacity(offset, _alpha, super.paint); layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer);
} }
} }
...@@ -904,14 +908,19 @@ class RenderAnimatedOpacity extends RenderProxyBox { ...@@ -904,14 +908,19 @@ class RenderAnimatedOpacity extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
if (_alpha == 0) if (_alpha == 0) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
return; return;
}
if (_alpha == 255) { if (_alpha == 255) {
// No need to keep the layer. We'll create a new one if necessary.
layer = null;
context.paintChild(child, offset); context.paintChild(child, offset);
return; return;
} }
assert(needsCompositing); assert(needsCompositing);
context.pushOpacity(offset, _alpha, super.paint); layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer);
} }
} }
...@@ -952,6 +961,9 @@ class RenderShaderMask extends RenderProxyBox { ...@@ -952,6 +961,9 @@ class RenderShaderMask extends RenderProxyBox {
_blendMode = blendMode, _blendMode = blendMode,
super(child); super(child);
@override
ShaderMaskLayer get layer => super.layer;
/// Called to creates the [Shader] that generates the mask. /// Called to creates the [Shader] that generates the mask.
/// ///
/// The shader callback is called with the current size of the child so that /// The shader callback is called with the current size of the child so that
...@@ -989,15 +1001,14 @@ class RenderShaderMask extends RenderProxyBox { ...@@ -989,15 +1001,14 @@ class RenderShaderMask extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
assert(needsCompositing); assert(needsCompositing);
context.pushLayer( layer ??= ShaderMaskLayer();
ShaderMaskLayer( layer
shader: _shaderCallback(offset & size), ..shader = _shaderCallback(offset & size)
maskRect: offset & size, ..maskRect = offset & size
blendMode: _blendMode, ..blendMode = _blendMode;
), context.pushLayer(layer, super.paint, offset);
super.paint, } else {
offset, layer = null;
);
} }
} }
} }
...@@ -1015,6 +1026,9 @@ class RenderBackdropFilter extends RenderProxyBox { ...@@ -1015,6 +1026,9 @@ class RenderBackdropFilter extends RenderProxyBox {
_filter = filter, _filter = filter,
super(child); super(child);
@override
BackdropFilterLayer get layer => super.layer;
/// The image filter to apply to the existing painted content before painting /// The image filter to apply to the existing painted content before painting
/// the child. /// the child.
/// ///
...@@ -1037,7 +1051,11 @@ class RenderBackdropFilter extends RenderProxyBox { ...@@ -1037,7 +1051,11 @@ class RenderBackdropFilter extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
assert(needsCompositing); assert(needsCompositing);
context.pushLayer(BackdropFilterLayer(filter: _filter), super.paint, offset); layer ??= BackdropFilterLayer();
layer.filter = _filter;
context.pushLayer(layer, super.paint, offset);
} else {
layer = null;
} }
} }
} }
...@@ -1292,7 +1310,9 @@ class RenderClipRect extends _RenderCustomClip<Rect> { ...@@ -1292,7 +1310,9 @@ class RenderClipRect extends _RenderCustomClip<Rect> {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
_updateClip(); _updateClip();
context.pushClipRect(needsCompositing, offset, _clip, super.paint, clipBehavior: clipBehavior); layer = context.pushClipRect(needsCompositing, offset, _clip, super.paint, clipBehavior: clipBehavior, oldLayer: layer);
} else {
layer = null;
} }
} }
...@@ -1368,7 +1388,9 @@ class RenderClipRRect extends _RenderCustomClip<RRect> { ...@@ -1368,7 +1388,9 @@ class RenderClipRRect extends _RenderCustomClip<RRect> {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
_updateClip(); _updateClip();
context.pushClipRRect(needsCompositing, offset, _clip.outerRect, _clip, super.paint, clipBehavior: clipBehavior); layer = context.pushClipRRect(needsCompositing, offset, _clip.outerRect, _clip, super.paint, clipBehavior: clipBehavior, oldLayer: layer);
} else {
layer = null;
} }
} }
...@@ -1436,7 +1458,9 @@ class RenderClipOval extends _RenderCustomClip<Rect> { ...@@ -1436,7 +1458,9 @@ class RenderClipOval extends _RenderCustomClip<Rect> {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
_updateClip(); _updateClip();
context.pushClipPath(needsCompositing, offset, _clip, _getClipPath(_clip), super.paint, clipBehavior: clipBehavior); layer = context.pushClipPath(needsCompositing, offset, _clip, _getClipPath(_clip), super.paint, clipBehavior: clipBehavior, oldLayer: layer);
} else {
layer = null;
} }
} }
...@@ -1498,7 +1522,9 @@ class RenderClipPath extends _RenderCustomClip<Path> { ...@@ -1498,7 +1522,9 @@ class RenderClipPath extends _RenderCustomClip<Path> {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
_updateClip(); _updateClip();
context.pushClipPath(needsCompositing, offset, Offset.zero & size, _clip, super.paint, clipBehavior: clipBehavior); layer = context.pushClipPath(needsCompositing, offset, Offset.zero & size, _clip, super.paint, clipBehavior: clipBehavior, oldLayer: layer);
} else {
layer = null;
} }
} }
...@@ -1631,6 +1657,9 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> { ...@@ -1631,6 +1657,9 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
shadowColor: shadowColor shadowColor: shadowColor
); );
@override
PhysicalModelLayer get layer => super.layer;
/// The shape of the layer. /// The shape of the layer.
/// ///
/// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners /// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners
...@@ -1710,18 +1739,20 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> { ...@@ -1710,18 +1739,20 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
} }
return true; return true;
}()); }());
final PhysicalModelLayer physicalModel = PhysicalModelLayer( layer ??= PhysicalModelLayer();
clipPath: offsetRRectAsPath, layer
clipBehavior: clipBehavior, ..clipPath = offsetRRectAsPath
elevation: paintShadows ? elevation : 0.0, ..clipBehavior = clipBehavior
color: color, ..elevation = paintShadows ? elevation : 0.0
shadowColor: shadowColor, ..color = color
); ..shadowColor = shadowColor;
context.pushLayer(layer, super.paint, offset, childPaintBounds: offsetBounds);
assert(() { assert(() {
physicalModel.debugCreator = debugCreator; layer.debugCreator = debugCreator;
return true; return true;
}()); }());
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds); } else {
layer = null;
} }
} }
...@@ -1768,6 +1799,9 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> { ...@@ -1768,6 +1799,9 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
clipBehavior: clipBehavior clipBehavior: clipBehavior
); );
@override
PhysicalModelLayer get layer => super.layer;
@override @override
Path get _defaultClip => Path()..addRect(Offset.zero & size); Path get _defaultClip => Path()..addRect(Offset.zero & size);
...@@ -1804,18 +1838,20 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> { ...@@ -1804,18 +1838,20 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
} }
return true; return true;
}()); }());
final PhysicalModelLayer physicalModel = PhysicalModelLayer( layer ??= PhysicalModelLayer();
clipPath: offsetPath, layer
clipBehavior: clipBehavior, ..clipPath = offsetPath
elevation: paintShadows ? elevation : 0.0, ..clipBehavior = clipBehavior
color: color, ..elevation = paintShadows ? elevation : 0.0
shadowColor: shadowColor, ..color = color
); ..shadowColor = shadowColor;
context.pushLayer(layer, super.paint, offset, childPaintBounds: offsetBounds);
assert(() { assert(() {
physicalModel.debugCreator = debugCreator; layer.debugCreator = debugCreator;
return true; return true;
}()); }());
context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds); } else {
layer = null;
} }
} }
...@@ -2145,10 +2181,12 @@ class RenderTransform extends RenderProxyBox { ...@@ -2145,10 +2181,12 @@ class RenderTransform extends RenderProxyBox {
if (child != null) { if (child != null) {
final Matrix4 transform = _effectiveTransform; final Matrix4 transform = _effectiveTransform;
final Offset childOffset = MatrixUtils.getAsTranslation(transform); final Offset childOffset = MatrixUtils.getAsTranslation(transform);
if (childOffset == null) if (childOffset == null) {
context.pushTransform(needsCompositing, offset, transform, super.paint); layer = context.pushTransform(needsCompositing, offset, transform, super.paint, oldLayer: layer);
else } else {
super.paint(context, offset + childOffset); super.paint(context, offset + childOffset);
layer = null;
}
} }
} }
...@@ -2288,12 +2326,14 @@ class RenderFittedBox extends RenderProxyBox { ...@@ -2288,12 +2326,14 @@ class RenderFittedBox extends RenderProxyBox {
} }
} }
void _paintChildWithTransform(PaintingContext context, Offset offset) { TransformLayer _paintChildWithTransform(PaintingContext context, Offset offset) {
final Offset childOffset = MatrixUtils.getAsTranslation(_transform); final Offset childOffset = MatrixUtils.getAsTranslation(_transform);
if (childOffset == null) if (childOffset == null)
context.pushTransform(needsCompositing, offset, _transform, super.paint); return context.pushTransform(needsCompositing, offset, _transform, super.paint,
oldLayer: layer is TransformLayer ? layer : null);
else else
super.paint(context, offset + childOffset); super.paint(context, offset + childOffset);
return null;
} }
@override @override
...@@ -2303,9 +2343,10 @@ class RenderFittedBox extends RenderProxyBox { ...@@ -2303,9 +2343,10 @@ class RenderFittedBox extends RenderProxyBox {
_updatePaintData(); _updatePaintData();
if (child != null) { if (child != null) {
if (_hasVisualOverflow) if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintChildWithTransform); layer = context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintChildWithTransform,
oldLayer: layer is ClipRectLayer ? layer : null);
else else
_paintChildWithTransform(context, offset); layer = _paintChildWithTransform(context, offset);
} }
} }
...@@ -2706,6 +2747,7 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2706,6 +2747,7 @@ class RenderMouseRegion extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_annotationIsActive) { if (_annotationIsActive) {
// Annotated region layers are not retained because they do not create engine layers.
final AnnotatedRegionLayer<MouseTrackerAnnotation> layer = AnnotatedRegionLayer<MouseTrackerAnnotation>( final AnnotatedRegionLayer<MouseTrackerAnnotation> layer = AnnotatedRegionLayer<MouseTrackerAnnotation>(
_hoverAnnotation, _hoverAnnotation,
size: size, size: size,
...@@ -2832,7 +2874,8 @@ class RenderRepaintBoundary extends RenderProxyBox { ...@@ -2832,7 +2874,8 @@ class RenderRepaintBoundary extends RenderProxyBox {
/// * [dart:ui.Scene.toImage] for more information about the image returned. /// * [dart:ui.Scene.toImage] for more information about the image returned.
Future<ui.Image> toImage({ double pixelRatio = 1.0 }) { Future<ui.Image> toImage({ double pixelRatio = 1.0 }) {
assert(!debugNeedsPaint); assert(!debugNeedsPaint);
return layer.toImage(Offset.zero & size, pixelRatio: pixelRatio); final OffsetLayer offsetLayer = layer;
return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio);
} }
...@@ -4657,7 +4700,16 @@ class RenderLeaderLayer extends RenderProxyBox { ...@@ -4657,7 +4700,16 @@ class RenderLeaderLayer extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
context.pushLayer(LeaderLayer(link: link, offset: offset), super.paint, Offset.zero); if (layer == null) {
layer = LeaderLayer(link: link, offset: offset);
} else {
final LeaderLayer leaderLayer = layer;
leaderLayer
..link = link
..offset = offset;
}
context.pushLayer(layer, super.paint, Offset.zero);
assert(layer != null);
} }
@override @override
...@@ -4743,7 +4795,7 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4743,7 +4795,7 @@ class RenderFollowerLayer extends RenderProxyBox {
@override @override
void detach() { void detach() {
_layer = null; layer = null;
super.detach(); super.detach();
} }
...@@ -4751,7 +4803,8 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4751,7 +4803,8 @@ class RenderFollowerLayer extends RenderProxyBox {
bool get alwaysNeedsCompositing => true; bool get alwaysNeedsCompositing => true;
/// The layer we created when we were last painted. /// The layer we created when we were last painted.
FollowerLayer _layer; @override
FollowerLayer get layer => super.layer;
/// Return the transform that was used in the last composition phase, if any. /// Return the transform that was used in the last composition phase, if any.
/// ///
...@@ -4760,7 +4813,7 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4760,7 +4813,7 @@ class RenderFollowerLayer extends RenderProxyBox {
/// [FollowerLayer.getLastTransform]), this returns the identity matrix (see /// [FollowerLayer.getLastTransform]), this returns the identity matrix (see
/// [new Matrix4.identity]. /// [new Matrix4.identity].
Matrix4 getCurrentTransform() { Matrix4 getCurrentTransform() {
return _layer?.getLastTransform() ?? Matrix4.identity(); return layer?.getLastTransform() ?? Matrix4.identity();
} }
@override @override
...@@ -4786,14 +4839,22 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4786,14 +4839,22 @@ class RenderFollowerLayer extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
assert(showWhenUnlinked != null); assert(showWhenUnlinked != null);
_layer = FollowerLayer( if (layer == null) {
layer = FollowerLayer(
link: link, link: link,
showWhenUnlinked: showWhenUnlinked, showWhenUnlinked: showWhenUnlinked,
linkedOffset: this.offset, linkedOffset: this.offset,
unlinkedOffset: offset, unlinkedOffset: offset,
); );
} else {
layer
..link = link
..showWhenUnlinked = showWhenUnlinked
..linkedOffset = this.offset
..unlinkedOffset = offset;
}
context.pushLayer( context.pushLayer(
_layer, layer,
super.paint, super.paint,
Offset.zero, Offset.zero,
childPaintBounds: const Rect.fromLTRB( childPaintBounds: const Rect.fromLTRB(
...@@ -4871,6 +4932,7 @@ class RenderAnnotatedRegion<T> extends RenderProxyBox { ...@@ -4871,6 +4932,7 @@ class RenderAnnotatedRegion<T> extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
// Annotated region layers are not retained because they do not create engine layers.
final AnnotatedRegionLayer<T> layer = AnnotatedRegionLayer<T>( final AnnotatedRegionLayer<T> layer = AnnotatedRegionLayer<T>(
value, value,
size: sized ? size : null, size: sized ? size : null,
......
...@@ -7,6 +7,7 @@ import 'dart:io' show Platform; ...@@ -7,6 +7,7 @@ import 'dart:io' show Platform;
import 'dart:ui' as ui show Scene, SceneBuilder, Window; import 'dart:ui' as ui show Scene, SceneBuilder, Window;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show MouseTrackerAnnotation;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
...@@ -173,6 +174,19 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -173,6 +174,19 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
return true; return true;
} }
/// Determines the set of mouse tracker annotations at the given position.
///
/// See also:
///
/// * [Layer.findAll], which is used by this method to find all
/// [AnnotatedRegionLayer]s annotated for mouse tracking.
Iterable<MouseTrackerAnnotation> hitTestMouseTrackers(Offset position) {
// Layer hit testing is done using device pixels, so we have to convert
// the logical coordinates of the event location back to device pixels
// here.
return layer.findAll<MouseTrackerAnnotation>(position * configuration.devicePixelRatio);
}
@override @override
bool get isRepaintBoundary => true; bool get isRepaintBoundary => true;
......
...@@ -55,6 +55,6 @@ class _ColorFilterRenderObject extends RenderProxyBox { ...@@ -55,6 +55,6 @@ class _ColorFilterRenderObject extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
context.pushColorFilter(offset, colorFilter, super.paint); layer = context.pushColorFilter(offset, colorFilter, super.paint, oldLayer: layer);
} }
} }
...@@ -10,7 +10,6 @@ import 'dart:typed_data'; ...@@ -10,7 +10,6 @@ import 'dart:typed_data';
import 'dart:ui' as ui import 'dart:ui' as ui
show show
ClipOp, ClipOp,
EngineLayer,
Image, Image,
ImageByteFormat, ImageByteFormat,
Paragraph, Paragraph,
...@@ -53,8 +52,8 @@ class _ProxyLayer extends Layer { ...@@ -53,8 +52,8 @@ class _ProxyLayer extends Layer {
final Layer _layer; final Layer _layer;
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
return _layer.addToScene(builder, layerOffset); _layer.addToScene(builder, layerOffset);
} }
@override @override
...@@ -314,9 +313,8 @@ Rect _calculateSubtreeBounds(RenderObject object) { ...@@ -314,9 +313,8 @@ Rect _calculateSubtreeBounds(RenderObject object) {
/// screenshots render to the scene in the local coordinate system of the layer. /// screenshots render to the scene in the local coordinate system of the layer.
class _ScreenshotContainerLayer extends OffsetLayer { class _ScreenshotContainerLayer extends OffsetLayer {
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
addChildrenToScene(builder, layerOffset); addChildrenToScene(builder, layerOffset);
return null; // this does not have an engine layer.
} }
} }
...@@ -556,9 +554,10 @@ class _ScreenshotPaintingContext extends PaintingContext { ...@@ -556,9 +554,10 @@ class _ScreenshotPaintingContext extends PaintingContext {
// Painting the existing repaint boundary to the screenshot is sufficient. // Painting the existing repaint boundary to the screenshot is sufficient.
// We don't just take a direct screenshot of the repaint boundary as we // We don't just take a direct screenshot of the repaint boundary as we
// want to capture debugPaint information as well. // want to capture debugPaint information as well.
data.containerLayer.append(_ProxyLayer(repaintBoundary.layer)); data.containerLayer.append(_ProxyLayer(repaintBoundary.debugLayer));
data.foundTarget = true; data.foundTarget = true;
data.screenshotOffset = repaintBoundary.layer.offset; final OffsetLayer offsetLayer = repaintBoundary.debugLayer;
data.screenshotOffset = offsetLayer.offset;
} else { } else {
// Repaint everything under the repaint boundary. // Repaint everything under the repaint boundary.
// We call debugInstrumentRepaintCompositedChild instead of paintChild as // We call debugInstrumentRepaintCompositedChild instead of paintChild as
...@@ -591,7 +590,7 @@ class _ScreenshotPaintingContext extends PaintingContext { ...@@ -591,7 +590,7 @@ class _ScreenshotPaintingContext extends PaintingContext {
// We must build the regular scene before we can build the screenshot // We must build the regular scene before we can build the screenshot
// scene as building the screenshot scene assumes addToScene has already // scene as building the screenshot scene assumes addToScene has already
// been called successfully for all layers in the regular scene. // been called successfully for all layers in the regular scene.
repaintBoundary.layer.buildScene(ui.SceneBuilder()); repaintBoundary.debugLayer.buildScene(ui.SceneBuilder());
return data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio); return data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio);
} }
...@@ -2504,9 +2503,9 @@ class _InspectorOverlayLayer extends Layer { ...@@ -2504,9 +2503,9 @@ class _InspectorOverlayLayer extends Layer {
double _textPainterMaxWidth; double _textPainterMaxWidth;
@override @override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
if (!selection.active) if (!selection.active)
return null; return;
final RenderObject selected = selection.current; final RenderObject selected = selection.current;
final List<_TransformedRect> candidates = <_TransformedRect>[]; final List<_TransformedRect> candidates = <_TransformedRect>[];
...@@ -2529,7 +2528,6 @@ class _InspectorOverlayLayer extends Layer { ...@@ -2529,7 +2528,6 @@ class _InspectorOverlayLayer extends Layer {
_picture = _buildPicture(state); _picture = _buildPicture(state);
} }
builder.addPicture(layerOffset, _picture); builder.addPicture(layerOffset, _picture);
return null; // this does not have an engine layer.
} }
ui.Picture _buildPicture(_InspectorOverlayRenderState state) { ui.Picture _buildPicture(_InspectorOverlayRenderState state) {
......
...@@ -99,11 +99,6 @@ void main() { ...@@ -99,11 +99,6 @@ void main() {
}); });
test('RenderAspectRatio: Unbounded', () { test('RenderAspectRatio: Unbounded', () {
bool hadError = false;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
hadError = true;
};
final RenderBox box = RenderConstrainedOverflowBox( final RenderBox box = RenderConstrainedOverflowBox(
maxWidth: double.infinity, maxWidth: double.infinity,
maxHeight: double.infinity, maxHeight: double.infinity,
...@@ -112,10 +107,16 @@ void main() { ...@@ -112,10 +107,16 @@ void main() {
child: RenderSizedBox(const Size(90.0, 70.0)), child: RenderSizedBox(const Size(90.0, 70.0)),
), ),
); );
expect(hadError, false);
layout(box); final List<String> errorMessages = <String>[];
expect(hadError, true); layout(box, onErrors: () {
FlutterError.onError = oldHandler; errorMessages.addAll(
renderer.takeAllFlutterErrorDetails().map((FlutterErrorDetails details) => '${details.exceptionAsString()}'),
);
});
expect(errorMessages, hasLength(2));
expect(errorMessages[0], contains('RenderAspectRatio has unbounded constraints.'));
// The second error message is a generic message generated by the Dart VM. Not worth testing.
}); });
test('RenderAspectRatio: Sizing', () { test('RenderAspectRatio: Sizing', () {
......
...@@ -365,10 +365,6 @@ void main() { ...@@ -365,10 +365,6 @@ void main() {
}); });
test('MainAxisSize.min inside unconstrained', () { test('MainAxisSize.min inside unconstrained', () {
final List<dynamic> exceptions = <dynamic>[];
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0); const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0);
final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square);
final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square);
...@@ -387,17 +383,15 @@ void main() { ...@@ -387,17 +383,15 @@ void main() {
flex.addAll(<RenderBox>[box1, box2, box3]); flex.addAll(<RenderBox>[box1, box2, box3]);
final FlexParentData box2ParentData = box2.parentData; final FlexParentData box2ParentData = box2.parentData;
box2ParentData.flex = 1; box2ParentData.flex = 1;
expect(exceptions, isEmpty); final List<dynamic> exceptions = <dynamic>[];
layout(parent); layout(parent, onErrors: () {
exceptions.addAll(renderer.takeAllFlutterExceptions());
});
expect(exceptions, isNotEmpty); expect(exceptions, isNotEmpty);
expect(exceptions.first, isInstanceOf<FlutterError>()); expect(exceptions.first, isInstanceOf<FlutterError>());
}); });
test('MainAxisSize.min inside unconstrained', () { test('MainAxisSize.min inside unconstrained', () {
final List<dynamic> exceptions = <dynamic>[];
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0); const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0);
final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square);
final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square);
...@@ -417,8 +411,10 @@ void main() { ...@@ -417,8 +411,10 @@ void main() {
final FlexParentData box2ParentData = box2.parentData; final FlexParentData box2ParentData = box2.parentData;
box2ParentData.flex = 1; box2ParentData.flex = 1;
box2ParentData.fit = FlexFit.loose; box2ParentData.fit = FlexFit.loose;
expect(exceptions, isEmpty); final List<dynamic> exceptions = <dynamic>[];
layout(parent); layout(parent, onErrors: () {
exceptions.addAll(renderer.takeAllFlutterExceptions());
});
expect(exceptions, isNotEmpty); expect(exceptions, isNotEmpty);
expect(exceptions.first, isInstanceOf<FlutterError>()); expect(exceptions.first, isInstanceOf<FlutterError>());
}); });
......
...@@ -22,29 +22,75 @@ void main() { ...@@ -22,29 +22,75 @@ void main() {
); );
layout(root, phase: EnginePhase.paint); layout(root, phase: EnginePhase.paint);
expect(inner.isRepaintBoundary, isFalse); expect(inner.isRepaintBoundary, isFalse);
expect(() => inner.layer, throwsAssertionError); expect(inner.debugLayer, null);
expect(boundary.isRepaintBoundary, isTrue); expect(boundary.isRepaintBoundary, isTrue);
expect(boundary.layer, isNotNull); expect(boundary.debugLayer, isNotNull);
expect(boundary.layer.attached, isTrue); // this time it painted... expect(boundary.debugLayer.attached, isTrue); // this time it painted...
root.opacity = 0.0; root.opacity = 0.0;
pumpFrame(phase: EnginePhase.paint); pumpFrame(phase: EnginePhase.paint);
expect(inner.isRepaintBoundary, isFalse); expect(inner.isRepaintBoundary, isFalse);
expect(() => inner.layer, throwsAssertionError); expect(inner.debugLayer, null);
expect(boundary.isRepaintBoundary, isTrue); expect(boundary.isRepaintBoundary, isTrue);
expect(boundary.layer, isNotNull); expect(boundary.debugLayer, isNotNull);
expect(boundary.layer.attached, isFalse); // this time it did not. expect(boundary.debugLayer.attached, isFalse); // this time it did not.
root.opacity = 0.5; root.opacity = 0.5;
pumpFrame(phase: EnginePhase.paint); pumpFrame(phase: EnginePhase.paint);
expect(inner.isRepaintBoundary, isFalse); expect(inner.isRepaintBoundary, isFalse);
expect(() => inner.layer, throwsAssertionError); expect(inner.debugLayer, null);
expect(boundary.isRepaintBoundary, isTrue); expect(boundary.isRepaintBoundary, isTrue);
expect(boundary.layer, isNotNull); expect(boundary.debugLayer, isNotNull);
expect(boundary.layer.attached, isTrue); // this time it did again! expect(boundary.debugLayer.attached, isTrue); // this time it did again!
}); });
test('layer subtree dirtiness is correctly computed', () { test('updateSubtreeNeedsAddToScene propagates Layer.alwaysNeedsAddToScene up the tree', () {
final ContainerLayer a = ContainerLayer();
final ContainerLayer b = ContainerLayer();
final ContainerLayer c = ContainerLayer();
final _TestAlwaysNeedsAddToSceneLayer d = _TestAlwaysNeedsAddToSceneLayer();
final ContainerLayer e = ContainerLayer();
final ContainerLayer f = ContainerLayer();
// Tree structure:
// a
// / \
// b c
// / \
// (x)d e
// /
// f
a.append(b);
a.append(c);
b.append(d);
b.append(e);
d.append(f);
a.debugMarkClean();
b.debugMarkClean();
c.debugMarkClean();
d.debugMarkClean();
e.debugMarkClean();
f.debugMarkClean();
expect(a.debugSubtreeNeedsAddToScene, false);
expect(b.debugSubtreeNeedsAddToScene, false);
expect(c.debugSubtreeNeedsAddToScene, false);
expect(d.debugSubtreeNeedsAddToScene, false);
expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false);
a.updateSubtreeNeedsAddToScene();
expect(a.debugSubtreeNeedsAddToScene, true);
expect(b.debugSubtreeNeedsAddToScene, true);
expect(c.debugSubtreeNeedsAddToScene, false);
expect(d.debugSubtreeNeedsAddToScene, true);
expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false);
});
test('updateSubtreeNeedsAddToScene propagates Layer._needsAddToScene up the tree', () {
final ContainerLayer a = ContainerLayer(); final ContainerLayer a = ContainerLayer();
final ContainerLayer b = ContainerLayer(); final ContainerLayer b = ContainerLayer();
final ContainerLayer c = ContainerLayer(); final ContainerLayer c = ContainerLayer();
...@@ -52,53 +98,59 @@ void main() { ...@@ -52,53 +98,59 @@ void main() {
final ContainerLayer e = ContainerLayer(); final ContainerLayer e = ContainerLayer();
final ContainerLayer f = ContainerLayer(); final ContainerLayer f = ContainerLayer();
final ContainerLayer g = ContainerLayer(); final ContainerLayer g = ContainerLayer();
final List<ContainerLayer> allLayers = <ContainerLayer>[a, b, c, d, e, f, g];
final PictureLayer h = PictureLayer(Rect.zero);
final PictureLayer i = PictureLayer(Rect.zero);
final PictureLayer j = PictureLayer(Rect.zero);
// The tree is like the following where b and j are dirty: // The tree is like the following where b and j are dirty:
// a____ // a____
// / \ // / \
// (x)b___ c // (x)b___ c
// / \ \ | // / \ \ |
// d e f g // d e f g(x)
// / \ |
// h i j(x)
a.append(b); a.append(b);
a.append(c); a.append(c);
b.append(d); b.append(d);
b.append(e); b.append(e);
b.append(f); b.append(f);
d.append(h);
d.append(i);
c.append(g); c.append(g);
g.append(j);
a.debugMarkClean(); for (ContainerLayer layer in allLayers) {
expect(layer.debugSubtreeNeedsAddToScene, true);
}
for (ContainerLayer layer in allLayers) {
layer.debugMarkClean();
}
for (ContainerLayer layer in allLayers) {
expect(layer.debugSubtreeNeedsAddToScene, false);
}
b.markNeedsAddToScene(); b.markNeedsAddToScene();
c.debugMarkClean(); a.updateSubtreeNeedsAddToScene();
d.debugMarkClean();
e.debugMarkClean();
f.debugMarkClean();
g.debugMarkClean();
h.debugMarkClean();
i.debugMarkClean();
j.markNeedsAddToScene();
expect(a.debugSubtreeNeedsAddToScene, true);
expect(b.debugSubtreeNeedsAddToScene, true);
expect(c.debugSubtreeNeedsAddToScene, false);
expect(d.debugSubtreeNeedsAddToScene, false);
expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false);
expect(g.debugSubtreeNeedsAddToScene, false);
g.markNeedsAddToScene();
a.updateSubtreeNeedsAddToScene(); a.updateSubtreeNeedsAddToScene();
expect(a.debugSubtreeNeedsAddToScene, true); expect(a.debugSubtreeNeedsAddToScene, true);
expect(b.debugSubtreeNeedsAddToScene, true); expect(b.debugSubtreeNeedsAddToScene, true);
expect(c.debugSubtreeNeedsAddToScene, true); expect(c.debugSubtreeNeedsAddToScene, true);
expect(g.debugSubtreeNeedsAddToScene, true);
expect(j.debugSubtreeNeedsAddToScene, true);
expect(d.debugSubtreeNeedsAddToScene, false); expect(d.debugSubtreeNeedsAddToScene, false);
expect(e.debugSubtreeNeedsAddToScene, false); expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false); expect(f.debugSubtreeNeedsAddToScene, false);
expect(h.debugSubtreeNeedsAddToScene, false); expect(g.debugSubtreeNeedsAddToScene, true);
expect(i.debugSubtreeNeedsAddToScene, false);
a.buildScene(SceneBuilder());
for (ContainerLayer layer in allLayers) {
expect(layer.debugSubtreeNeedsAddToScene, false);
}
}); });
test('leader and follower layers are always dirty', () { test('leader and follower layers are always dirty', () {
...@@ -465,4 +517,27 @@ void main() { ...@@ -465,4 +517,27 @@ void main() {
_testConflicts(layerA, layerB, expectedErrorCount: 1); _testConflicts(layerA, layerB, expectedErrorCount: 1);
}); });
}, skip: isBrowser); }, skip: isBrowser);
test('ContainerLayer.toImage can render interior layer', () {
final OffsetLayer parent = OffsetLayer();
final OffsetLayer child = OffsetLayer();
final OffsetLayer grandChild = OffsetLayer();
child.append(grandChild);
parent.append(child);
// This renders the layers and generates engine layers.
parent.buildScene(SceneBuilder());
// Causes grandChild to pass its engine layer as `oldLayer`
grandChild.toImage(const Rect.fromLTRB(0, 0, 10, 10));
// Ensure we can render the same scene again after rendering an interior
// layer.
parent.buildScene(SceneBuilder());
});
}
class _TestAlwaysNeedsAddToSceneLayer extends ContainerLayer {
@override
bool get alwaysNeedsAddToScene => true;
} }
...@@ -5,13 +5,14 @@ ...@@ -5,13 +5,14 @@
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';
import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
void main() { void main() {
test('ensure frame is scheduled for markNeedsSemanticsUpdate', () { test('ensure frame is scheduled for markNeedsSemanticsUpdate', () {
// Initialize all bindings because owner.flushSemantics() requires a window // Initialize all bindings because owner.flushSemantics() requires a window
TestWidgetsFlutterBinding.ensureInitialized(); renderer;
final TestRenderObject renderObject = TestRenderObject(); final TestRenderObject renderObject = TestRenderObject();
int onNeedVisualUpdateCallCount = 0; int onNeedVisualUpdateCallCount = 0;
...@@ -98,6 +99,81 @@ void main() { ...@@ -98,6 +99,81 @@ void main() {
..nextSibling = RenderOpacity(); ..nextSibling = RenderOpacity();
expect(() => data3.detach(), throwsAssertionError); expect(() => data3.detach(), throwsAssertionError);
}); });
test('PaintingContext.pushClipRect reuses the layer', () {
_testPaintingContextLayerReuse<ClipRectLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushClipRect(true, offset, Rect.zero, painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushClipRRect reuses the layer', () {
_testPaintingContextLayerReuse<ClipRRectLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushClipRRect(true, offset, Rect.zero, RRect.fromRectAndRadius(Rect.zero, const Radius.circular(1.0)), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushClipPath reuses the layer', () {
_testPaintingContextLayerReuse<ClipPathLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushClipPath(true, offset, Rect.zero, Path(), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushColorFilter reuses the layer', () {
_testPaintingContextLayerReuse<ColorFilterLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushColorFilter(offset, const ColorFilter.mode(Color.fromRGBO(0, 0, 0, 1.0), BlendMode.clear), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushTransform reuses the layer', () {
_testPaintingContextLayerReuse<TransformLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushTransform(true, offset, Matrix4.identity(), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushOpacity reuses the layer', () {
_testPaintingContextLayerReuse<OpacityLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushOpacity(offset, 100, painter, oldLayer: oldLayer);
});
});
}
// 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
// frame reuses the layer painted on the first frame.
void _testPaintingContextLayerReuse<L extends Layer>(_LayerTestPaintCallback painter) {
final _TestCustomLayerBox box = _TestCustomLayerBox(painter);
layout(box, phase: EnginePhase.paint);
// Force a repaint. Otherwise, pumpFrame is a noop.
box.markNeedsPaint();
pumpFrame(phase: EnginePhase.paint);
expect(box.paintedLayers, hasLength(2));
expect(box.paintedLayers[0], isInstanceOf<L>());
expect(box.paintedLayers[0], same(box.paintedLayers[1]));
}
typedef _LayerTestPaintCallback = Layer Function(PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer);
class _TestCustomLayerBox extends RenderBox {
_TestCustomLayerBox(this.painter);
final _LayerTestPaintCallback painter;
final List<Layer> paintedLayers = <Layer>[];
@override
bool get isRepaintBoundary => false;
@override
void performLayout() {
size = constraints.smallest;
}
@override
void paint(PaintingContext context, Offset offset) {
final Layer paintedLayer = painter(super.paint, context, offset, layer);
paintedLayers.add(paintedLayer);
layer = paintedLayer;
}
} }
class TestParentData extends ParentData with ContainerParentDataMixin<RenderBox> { } class TestParentData extends ParentData with ContainerParentDataMixin<RenderBox> { }
......
...@@ -16,17 +16,11 @@ void main() { ...@@ -16,17 +16,11 @@ void main() {
// compatible with existing tests in object_test.dart. // compatible with existing tests in object_test.dart.
test('reentrant paint error', () { test('reentrant paint error', () {
FlutterErrorDetails errorDetails; FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
final RenderBox root = TestReentrantPaintingErrorRenderBox(); final RenderBox root = TestReentrantPaintingErrorRenderBox();
try { layout(root, onErrors: () {
layout(root); errorDetails = renderer.takeFlutterErrorDetails();
});
pumpFrame(phase: EnginePhase.paint); pumpFrame(phase: EnginePhase.paint);
} finally {
FlutterError.onError = oldHandler;
}
expect(errorDetails, isNotNull); expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull); expect(errorDetails.stack, isNotNull);
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Gradient, Image, ImageFilter;
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -218,7 +218,7 @@ void main() { ...@@ -218,7 +218,7 @@ void main() {
expect(getPixel(0, 0), equals(0x00000080)); expect(getPixel(0, 0), equals(0x00000080));
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff)); expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
final OffsetLayer layer = boundary.layer; final OffsetLayer layer = boundary.debugLayer;
image = await layer.toImage(Offset.zero & const Size(20.0, 20.0)); image = await layer.toImage(Offset.zero & const Size(20.0, 20.0));
expect(image.width, equals(20)); expect(image.width, equals(20));
...@@ -268,6 +268,13 @@ void main() { ...@@ -268,6 +268,13 @@ void main() {
expect(renderOpacity.needsCompositing, false); expect(renderOpacity.needsCompositing, false);
}); });
test('RenderOpacity reuses its layer', () {
_testLayerReuse<OpacityLayer>(RenderOpacity(
opacity: 0.5, // must not be 0 or 1.0. Otherwise, it won't create a layer
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderAnimatedOpacity does not composite if it is transparent', () async { test('RenderAnimatedOpacity does not composite if it is transparent', () async {
final Animation<double> opacityAnimation = AnimationController( final Animation<double> opacityAnimation = AnimationController(
vsync: _FakeTickerProvider(), vsync: _FakeTickerProvider(),
...@@ -297,6 +304,183 @@ void main() { ...@@ -297,6 +304,183 @@ void main() {
layout(renderAnimatedOpacity, phase: EnginePhase.composite); layout(renderAnimatedOpacity, phase: EnginePhase.composite);
expect(renderAnimatedOpacity.needsCompositing, false); expect(renderAnimatedOpacity.needsCompositing, false);
}); });
test('RenderAnimatedOpacity reuses its layer', () {
final Animation<double> opacityAnimation = AnimationController(
vsync: _FakeTickerProvider(),
)..value = 0.5; // must not be 0 or 1.0. Otherwise, it won't create a layer
_testLayerReuse<OpacityLayer>(RenderAnimatedOpacity(
opacity: opacityAnimation,
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderShaderMask reuses its layer', () {
_testLayerReuse<ShaderMaskLayer>(RenderShaderMask(
shaderCallback: (Rect rect) {
return ui.Gradient.radial(
rect.center,
rect.shortestSide / 2.0,
const <Color>[Color.fromRGBO(0, 0, 0, 1.0), Color.fromRGBO(255, 255, 255, 1.0)],
);
},
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderBackdropFilter reuses its layer', () {
_testLayerReuse<BackdropFilterLayer>(RenderBackdropFilter(
filter: ui.ImageFilter.blur(),
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderClipRect reuses its layer', () {
_testLayerReuse<ClipRectLayer>(RenderClipRect(
clipper: _TestRectClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderClipRRect reuses its layer', () {
_testLayerReuse<ClipRRectLayer>(RenderClipRRect(
clipper: _TestRRectClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderClipOval reuses its layer', () {
_testLayerReuse<ClipPathLayer>(RenderClipOval(
clipper: _TestRectClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderClipPath reuses its layer', () {
_testLayerReuse<ClipPathLayer>(RenderClipPath(
clipper: _TestPathClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderPhysicalModel reuses its layer', () {
_testLayerReuse<PhysicalModelLayer>(RenderPhysicalModel(
color: const Color.fromRGBO(0, 0, 0, 1.0),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderPhysicalShape reuses its layer', () {
_testLayerReuse<PhysicalModelLayer>(RenderPhysicalShape(
clipper: _TestPathClipper(),
color: const Color.fromRGBO(0, 0, 0, 1.0),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderTransform reuses its layer', () {
_testLayerReuse<TransformLayer>(RenderTransform(
// Use a 3D transform to force compositing.
transform: Matrix4.rotationX(0.1),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
void _testFittedBoxWithClipRectLayer() {
_testLayerReuse<ClipRectLayer>(RenderFittedBox(
alignment: Alignment.center,
fit: BoxFit.cover,
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(100.0, 200.0)),
), // size doesn't matter
));
}
void _testFittedBoxWithTransformLayer() {
_testLayerReuse<TransformLayer>(RenderFittedBox(
alignment: Alignment.center,
fit: BoxFit.fill,
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1, 1)),
), // size doesn't matter
));
}
test('RenderFittedBox reuses ClipRectLayer', () {
_testFittedBoxWithClipRectLayer();
});
test('RenderFittedBox reuses TransformLayer', () {
_testFittedBoxWithTransformLayer();
});
test('RenderFittedBox switches between ClipRectLayer and TransformLayer, and reuses them', () {
_testFittedBoxWithClipRectLayer();
// clip -> transform
_testFittedBoxWithTransformLayer();
// transform -> clip
_testFittedBoxWithClipRectLayer();
});
}
class _TestRectClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) {
return Rect.zero;
}
@override
Rect getApproximateClipRect(Size size) => getClip(size);
@override
bool shouldReclip(_TestRectClipper oldClipper) => true;
}
class _TestRRectClipper extends CustomClipper<RRect> {
@override
RRect getClip(Size size) {
return RRect.zero;
}
@override
Rect getApproximateClipRect(Size size) => getClip(size).outerRect;
@override
bool shouldReclip(_TestRRectClipper oldClipper) => true;
} }
class _FakeTickerProvider implements TickerProvider { class _FakeTickerProvider implements TickerProvider {
...@@ -348,3 +532,32 @@ class _FakeTicker implements Ticker { ...@@ -348,3 +532,32 @@ class _FakeTicker implements Ticker {
@override @override
String toString({ bool debugIncludeStack = false }) => super.toString(); String toString({ bool debugIncludeStack = false }) => super.toString();
} }
// Forces two frames and checks that:
// - a layer is created on the first frame
// - the layer is reused on the second frame
void _testLayerReuse<L extends Layer>(RenderObject renderObject) {
expect(L, isNot(Layer));
expect(renderObject.debugLayer, null);
layout(renderObject, phase: EnginePhase.paint, constraints: BoxConstraints.tight(const Size(10, 10)));
final Layer layer = renderObject.debugLayer;
expect(layer, isInstanceOf<L>());
expect(layer, isNotNull);
// Mark for repaint otherwise pumpFrame is a noop.
renderObject.markNeedsPaint();
expect(renderObject.debugNeedsPaint, true);
pumpFrame(phase: EnginePhase.paint);
expect(renderObject.debugNeedsPaint, false);
expect(renderObject.debugLayer, same(layer));
}
class _TestPathClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
return Path()
..addRect(const Rect.fromLTWH(50.0, 50.0, 100.0, 100.0));
}
@override
bool shouldReclip(_TestPathClipper oldClipper) => false;
}
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/src/rendering/layer.dart';
/// An [Invocation] and the [stack] trace that led to it. /// An [Invocation] and the [stack] trace that led to it.
/// ///
...@@ -103,38 +104,49 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex ...@@ -103,38 +104,49 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex
} }
@override @override
void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge }) { ClipRectLayer pushClipRect(bool needsCompositing, Offset offset, Rect clipRect,
PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge, ClipRectLayer oldLayer }) {
clipRectAndPaint(clipRect.shift(offset), clipBehavior, clipRect.shift(offset), () => painter(this, offset)); clipRectAndPaint(clipRect.shift(offset), clipBehavior, clipRect.shift(offset), () => painter(this, offset));
return null;
} }
@override @override
void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) { ClipRRectLayer pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect,
PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipRRectLayer oldLayer }) {
assert(clipBehavior != null); assert(clipBehavior != null);
clipRRectAndPaint(clipRRect.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset)); clipRRectAndPaint(clipRRect.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset));
return null;
} }
@override @override
void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) { ClipPathLayer pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath,
PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipPathLayer oldLayer }) {
clipPathAndPaint(clipPath.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset)); clipPathAndPaint(clipPath.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset));
return null;
} }
@override @override
void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) { TransformLayer pushTransform(bool needsCompositing, Offset offset, Matrix4 transform,
PaintingContextCallback painter, { TransformLayer oldLayer }) {
canvas.save(); canvas.save();
canvas.transform(transform.storage); canvas.transform(transform.storage);
painter(this, offset); painter(this, offset);
canvas.restore(); canvas.restore();
return null;
} }
@override @override
void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) { OpacityLayer pushOpacity(Offset offset, int alpha, PaintingContextCallback painter,
{ OpacityLayer oldLayer }) {
canvas.saveLayer(null, null); // TODO(ianh): Expose the alpha somewhere. canvas.saveLayer(null, null); // TODO(ianh): Expose the alpha somewhere.
painter(this, offset); painter(this, offset);
canvas.restore(); canvas.restore();
return null;
} }
@override @override
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) { void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset,
{ Rect childPaintBounds }) {
painter(this, offset); painter(this, offset);
} }
......
...@@ -7,16 +7,79 @@ import 'package:flutter/gestures.dart'; ...@@ -7,16 +7,79 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart' show EnginePhase, fail;
import 'package:flutter_test/flutter_test.dart' show EnginePhase; export 'package:flutter/foundation.dart' show FlutterError, FlutterErrorDetails;
export 'package:flutter_test/flutter_test.dart' show EnginePhase; export 'package:flutter_test/flutter_test.dart' show EnginePhase;
class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding { class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding {
/// Creates a binding for testing rendering library functionality.
///
/// If [onErrors] is not null, it is called if [FlutterError] caught any errors
/// while drawing the frame. If [onErrors] is null and [FlutterError] caught at least
/// one error, this function fails the test. A test may override [onErrors] and
/// inspect errors using [takeFlutterErrorDetails].
TestRenderingFlutterBinding({ this.onErrors });
final List<FlutterErrorDetails> _errors = <FlutterErrorDetails>[];
/// A function called after drawing a frame if [FlutterError] caught any errors.
///
/// This function is expected to inspect these errors and decide whether they
/// are expected or not. Use [takeFlutterErrorDetails] to take one error at a
/// time, or [takeAllFlutterErrorDetails] to iterate over all errors.
VoidCallback onErrors;
/// Returns the error least recently caught by [FlutterError] and removes it
/// from the list of captured errors.
///
/// Returns null if no errors were captures, or if the list was exhausted by
/// calling this method repeatedly.
FlutterErrorDetails takeFlutterErrorDetails() {
if (_errors.isEmpty) {
return null;
}
return _errors.removeAt(0);
}
/// Returns all error details caught by [FlutterError] from least recently caught to
/// most recently caught, and removes them from the list of captured errors.
///
/// The returned iterable takes errors lazily. If, for example, you iterate over 2
/// errors, but there are 5 errors total, this binding will still fail the test.
/// Tests are expected to take and inspect all errors.
Iterable<FlutterErrorDetails> takeAllFlutterErrorDetails() sync* {
// sync* and yield are used for lazy evaluation. Otherwise, the list would be
// drained eagerly and allow a test pass with unexpected errors.
while (_errors.isNotEmpty) {
yield _errors.removeAt(0);
}
}
/// Returns all exceptions caught by [FlutterError] from least recently caught to
/// most recently caught, and removes them from the list of captured errors.
///
/// The returned iterable takes errors lazily. If, for example, you iterate over 2
/// errors, but there are 5 errors total, this binding will still fail the test.
/// Tests are expected to take and inspect all errors.
Iterable<dynamic> takeAllFlutterExceptions() sync* {
// sync* and yield are used for lazy evaluation. Otherwise, the list would be
// drained eagerly and allow a test pass with unexpected errors.
while (_errors.isNotEmpty) {
yield _errors.removeAt(0).exception;
}
}
EnginePhase phase = EnginePhase.composite; EnginePhase phase = EnginePhase.composite;
@override @override
void drawFrame() { void drawFrame() {
assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead'); assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
final FlutterExceptionHandler oldErrorHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
_errors.add(details);
};
try {
pipelineOwner.flushLayout(); pipelineOwner.flushLayout();
if (phase == EnginePhase.layout) if (phase == EnginePhase.layout)
return; return;
...@@ -34,6 +97,21 @@ class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, Gest ...@@ -34,6 +97,21 @@ class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, Gest
return; return;
assert(phase == EnginePhase.flushSemantics || assert(phase == EnginePhase.flushSemantics ||
phase == EnginePhase.sendSemanticsUpdate); phase == EnginePhase.sendSemanticsUpdate);
} finally {
FlutterError.onError = oldErrorHandler;
if (_errors.isNotEmpty) {
if (onErrors != null) {
onErrors();
if (_errors.isNotEmpty) {
_errors.forEach(FlutterError.dumpErrorToConsole);
fail('There are more errors than the test inspected using TestRenderingFlutterBinding.takeFlutterErrorDetails.');
}
} else {
_errors.forEach(FlutterError.dumpErrorToConsole);
fail('Caught error while rendering frame. See preceding logs for details.');
}
}
}
} }
} }
...@@ -55,11 +133,14 @@ TestRenderingFlutterBinding get renderer { ...@@ -55,11 +133,14 @@ TestRenderingFlutterBinding get renderer {
/// ///
/// The EnginePhase must not be [EnginePhase.build], since the rendering layer /// The EnginePhase must not be [EnginePhase.build], since the rendering layer
/// has no build phase. /// has no build phase.
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
void layout( void layout(
RenderBox box, { RenderBox box, {
BoxConstraints constraints, BoxConstraints constraints,
Alignment alignment = Alignment.center, Alignment alignment = Alignment.center,
EnginePhase phase = EnginePhase.layout, EnginePhase phase = EnginePhase.layout,
VoidCallback onErrors,
}) { }) {
assert(box != null); // If you want to just repump the last box, call pumpFrame(). assert(box != null); // If you want to just repump the last box, call pumpFrame().
assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry. assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry.
...@@ -76,13 +157,21 @@ void layout( ...@@ -76,13 +157,21 @@ void layout(
} }
renderer.renderView.child = box; renderer.renderView.child = box;
pumpFrame(phase: phase); pumpFrame(phase: phase, onErrors: onErrors);
} }
void pumpFrame({ EnginePhase phase = EnginePhase.layout }) { /// Pumps a single frame.
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
void pumpFrame({ EnginePhase phase = EnginePhase.layout, VoidCallback onErrors }) {
assert(renderer != null); assert(renderer != null);
assert(renderer.renderView != null); assert(renderer.renderView != null);
assert(renderer.renderView.child != null); // call layout() first! assert(renderer.renderView.child != null); // call layout() first!
if (onErrors != null) {
renderer.onErrors = onErrors;
}
renderer.phase = phase; renderer.phase = phase;
renderer.drawFrame(); renderer.drawFrame();
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
...@@ -51,4 +52,102 @@ void main() { ...@@ -51,4 +52,102 @@ void main() {
padding.child = repaintBoundary; padding.child = repaintBoundary;
pumpFrame(phase: EnginePhase.flushSemantics); pumpFrame(phase: EnginePhase.flushSemantics);
}); });
test('Framework creates an OffsetLayer for a repaint boundary', () {
final _TestRepaintBoundary repaintBoundary = _TestRepaintBoundary();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: repaintBoundary,
);
layout(opacity, phase: EnginePhase.flushSemantics);
expect(repaintBoundary.debugLayer, isInstanceOf<OffsetLayer>());
});
test('Framework does not create an OffsetLayer for a non-repaint boundary', () {
final _TestNonCompositedBox nonCompositedBox = _TestNonCompositedBox();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: nonCompositedBox,
);
layout(opacity, phase: EnginePhase.flushSemantics);
expect(nonCompositedBox.debugLayer, null);
});
test('Framework allows a non-repaint boundary to create own layer', () {
final _TestCompositedBox compositedBox = _TestCompositedBox();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: compositedBox,
);
layout(opacity, phase: EnginePhase.flushSemantics);
expect(compositedBox.debugLayer, isInstanceOf<OpacityLayer>());
});
test('Framework ensures repaint boundary layer is not overwritten', () {
final _TestRepaintBoundaryThatOverwritesItsLayer faultyRenderObject = _TestRepaintBoundaryThatOverwritesItsLayer();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: faultyRenderObject,
);
FlutterErrorDetails error;
layout(opacity, phase: EnginePhase.flushSemantics, onErrors: () {
error = renderer.takeFlutterErrorDetails();
});
expect('${error.exception}', contains('Attempted to set a layer to a repaint boundary render object.'));
});
}
// A plain render object that's a repaint boundary.
class _TestRepaintBoundary extends RenderBox {
@override
bool get isRepaintBoundary => true;
@override
void performLayout() {
size = constraints.smallest;
}
}
// A render object that's a repaint boundary and (incorrectly) creates its own layer.
class _TestRepaintBoundaryThatOverwritesItsLayer extends RenderBox {
@override
bool get isRepaintBoundary => true;
@override
void performLayout() {
size = constraints.smallest;
}
@override
void paint(PaintingContext context, Offset offset) {
layer = OpacityLayer(alpha: 50);
}
}
// A render object that's neither a repaint boundary nor creates its own layer.
class _TestNonCompositedBox extends RenderBox {
@override
bool get isRepaintBoundary => false;
@override
void performLayout() {
size = constraints.smallest;
}
}
// A render object that's not a repaint boundary but creates its own layer.
class _TestCompositedBox extends RenderBox {
@override
bool get isRepaintBoundary => false;
@override
void performLayout() {
size = constraints.smallest;
}
@override
void paint(PaintingContext context, Offset offset) {
layer = OpacityLayer(alpha: 50);
}
} }
...@@ -31,12 +31,12 @@ void main() { ...@@ -31,12 +31,12 @@ void main() {
), ),
), ),
); );
int result = RendererBinding.instance.renderView.layer.find<int>(Offset( int result = RendererBinding.instance.renderView.debugLayer.find<int>(Offset(
10.0 * window.devicePixelRatio, 10.0 * window.devicePixelRatio,
10.0 * window.devicePixelRatio, 10.0 * window.devicePixelRatio,
)); ));
expect(result, null); expect(result, null);
result = RendererBinding.instance.renderView.layer.find<int>(Offset( result = RendererBinding.instance.renderView.debugLayer.find<int>(Offset(
50.0 * window.devicePixelRatio, 50.0 * window.devicePixelRatio,
50.0 * window.devicePixelRatio, 50.0 * window.devicePixelRatio,
)); ));
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() { void main() {
testWidgets('Color filter - red', (WidgetTester tester) async { testWidgets('Color filter - red', (WidgetTester tester) async {
...@@ -64,4 +65,26 @@ void main() { ...@@ -64,4 +65,26 @@ void main() {
), ),
); );
}); });
testWidgets('Color filter - reuses its layer', (WidgetTester tester) async {
Future<void> pumpWithColor(Color color) async {
await tester.pumpWidget(
RepaintBoundary(
child: ColorFiltered(
colorFilter: ColorFilter.mode(color, BlendMode.color),
child: const Placeholder(),
),
),
);
}
await pumpWithColor(Colors.red);
final RenderObject renderObject = tester.firstRenderObject(find.byType(ColorFiltered));
final ColorFilterLayer originalLayer = renderObject.debugLayer;
expect(originalLayer, isNotNull);
// Change color to force a repaint.
await pumpWithColor(Colors.green);
expect(renderObject.debugLayer, same(originalLayer));
});
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
...@@ -191,6 +192,7 @@ void main() { ...@@ -191,6 +192,7 @@ void main() {
final Element element = find.byType(RepaintBoundary).first.evaluate().single; final Element element = find.byType(RepaintBoundary).first.evaluate().single;
// The following line will send the layer to engine and cause crash if an // The following line will send the layer to engine and cause crash if an
// empty opacity layer is sent. // empty opacity layer is sent.
await element.renderObject.layer.toImage(const Rect.fromLTRB(0.0, 0.0, 1.0, 1.0)); final OffsetLayer offsetLayer = element.renderObject.debugLayer;
await offsetLayer.toImage(const Rect.fromLTRB(0.0, 0.0, 1.0, 1.0));
}, skip: isBrowser); }, skip: isBrowser);
} }
...@@ -196,7 +196,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -196,7 +196,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
Future<Evaluation> evaluate(WidgetTester tester) async { Future<Evaluation> evaluate(WidgetTester tester) async {
final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final RenderView renderView = tester.binding.renderView; final RenderView renderView = tester.binding.renderView;
final OffsetLayer layer = renderView.layer; final OffsetLayer layer = renderView.debugLayer;
ui.Image image; ui.Image image;
final ByteData byteData = await tester.binding.runAsync<ByteData>(() async { final ByteData byteData = await tester.binding.runAsync<ByteData>(() async {
// Needs to be the same pixel ratio otherwise our dimensions won't match the // Needs to be the same pixel ratio otherwise our dimensions won't match the
......
...@@ -231,7 +231,7 @@ abstract class WidgetController { ...@@ -231,7 +231,7 @@ abstract class WidgetController {
} }
/// Returns a list of all the [Layer] objects in the rendering. /// Returns a list of all the [Layer] objects in the rendering.
List<Layer> get layers => _walkLayers(binding.renderView.layer).toList(); List<Layer> get layers => _walkLayers(binding.renderView.debugLayer).toList();
Iterable<Layer> _walkLayers(Layer layer) sync* { Iterable<Layer> _walkLayers(Layer layer) sync* {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
yield layer; yield layer;
......
...@@ -1620,7 +1620,7 @@ Future<ui.Image> _captureImage(Element element) { ...@@ -1620,7 +1620,7 @@ Future<ui.Image> _captureImage(Element element) {
assert(renderObject != null); assert(renderObject != null);
} }
assert(!renderObject.debugNeedsPaint); assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.layer; final OffsetLayer layer = renderObject.debugLayer;
return layer.toImage(renderObject.paintBounds); return layer.toImage(renderObject.paintBounds);
} }
......
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