diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 03448328b2..ce499b0a88 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -242,12 +242,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture // Creates a [MouseTracker] which manages state about currently connected // mice, for hover notification. MouseTracker _createMouseTracker() { - return MouseTracker(pointerRouter, (Offset offset) { - // 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(offset * window.devicePixelRatio); - }); + return MouseTracker(pointerRouter, renderView.hitTestMouseTrackers); } void _handleSemanticsEnabledChanged() { diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index 2a46549cac..32c324d0db 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -45,19 +45,49 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { // 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; /// Mark that this layer has changed and [addToScene] needs to be called. @protected @visibleForTesting 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; } /// 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 void debugMarkClean() { assert(() { @@ -70,9 +100,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { @protected bool get alwaysNeedsAddToScene => false; - bool _subtreeNeedsAddToScene; - - /// Whether any layer in the subtree needs [addToScene]. + /// Whether this or any descendant layer in the subtree needs [addToScene]. /// /// This is for debug and test purpose only. It only becomes valid after /// calling [updateSubtreeNeedsAddToScene]. @@ -80,22 +108,77 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { bool get debugSubtreeNeedsAddToScene { bool result; assert(() { - result = _subtreeNeedsAddToScene; + result = _needsAddToScene; return true; }()); 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; - /// 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 subtree needs [addToScene] if any of its layers need [addToScene]. - /// The [ContainerLayer] will override this to respect its children. + /// A layer needs [addToScene] if any of the following is true: + /// + /// - [alwaysNeedsAddToScene] is true. + /// - [markNeedsAddToScene] has been called. + /// - Any of its descendants need [addToScene]. + /// + /// [ContainerLayer] overrides this method to recursively call it on its children. @protected @visibleForTesting void updateSubtreeNeedsAddToScene() { - _subtreeNeedsAddToScene = _needsAddToScene || alwaysNeedsAddToScene; + _needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene; } /// This layer's next sibling in the parent layer's child list. @@ -108,13 +191,17 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { @override void dropChild(AbstractNode child) { - markNeedsAddToScene(); + if (!alwaysNeedsAddToScene) { + markNeedsAddToScene(); + } super.dropChild(child); } @override void adoptChild(AbstractNode child) { - markNeedsAddToScene(); + if (!alwaysNeedsAddToScene) { + markNeedsAddToScene(); + } super.adoptChild(child); } @@ -157,23 +244,26 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { /// Return the engine layer for retained rendering. When there's no /// corresponding engine layer, null is returned. @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) { // There can't be a loop by adding a retained layer subtree whose - // _subtreeNeedsAddToScene is false. + // _needsAddToScene is false. // // Proof by contradiction: // // 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 // changed so A's _needsAddToScene is true. This contradicts - // _subtreeNeedsAddToScene being false. - if (!_subtreeNeedsAddToScene && _engineLayer != null) { + // _needsAddToScene being false. + if (!_needsAddToScene && _engineLayer != null) { builder.addRetained(_engineLayer); 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; } @@ -218,7 +308,7 @@ class PictureLayer extends Layer { ui.Picture get picture => _picture; ui.Picture _picture; set picture(ui.Picture picture) { - _needsAddToScene = true; + markNeedsAddToScene(); _picture = picture; } @@ -258,9 +348,8 @@ class PictureLayer extends Layer { } @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); - return null; // this does not return an engine layer yet. } @override @@ -329,7 +418,7 @@ class TextureLayer extends Layer { final bool freeze; @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); builder.addTexture( textureId, @@ -338,7 +427,6 @@ class TextureLayer extends Layer { height: shiftedRect.height, freeze: freeze, ); - return null; // this does not return an engine layer yet. } @override @@ -369,7 +457,7 @@ class PlatformViewLayer extends Layer { final int viewId; @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); builder.addPlatformView( viewId, @@ -377,7 +465,6 @@ class PlatformViewLayer extends Layer { width: shiftedRect.width, height: shiftedRect.height, ); - return null; } @override @@ -447,14 +534,13 @@ class PerformanceOverlayLayer extends Layer { final bool checkerboardOffscreenLayers; @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { assert(optionsMask != null); final Rect shiftedOverlayRect = layerOffset == Offset.zero ? overlayRect : overlayRect.shift(layerOffset); builder.addPerformanceOverlay(optionsMask, shiftedOverlayRect); builder.setRasterizerTracingThreshold(rasterizerThreshold); builder.setCheckerboardRasterCacheImages(checkerboardRasterCacheImages); builder.setCheckerboardOffscreenLayers(checkerboardOffscreenLayers); - return null; // this does not return an engine layer yet. } @override @@ -478,6 +564,44 @@ class ContainerLayer extends Layer { Layer get lastChild => _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 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 }) { assert(child.attached == attached); while (child.previousSibling != null) { @@ -598,7 +722,7 @@ class ContainerLayer extends Layer { Layer child = firstChild; while (child != null) { child.updateSubtreeNeedsAddToScene(); - _subtreeNeedsAddToScene = _subtreeNeedsAddToScene || child._subtreeNeedsAddToScene; + _needsAddToScene = _needsAddToScene || child._needsAddToScene; child = child.nextSibling; } } @@ -721,9 +845,8 @@ class ContainerLayer extends Layer { } @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { addChildrenToScene(builder, layerOffset); - return null; // ContainerLayer does not have a corresponding engine layer } /// Uploads all of this layer's children to the engine. @@ -867,45 +990,16 @@ class OffsetLayer extends ContainerLayer { 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 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 - 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. // 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 // node. Otherwise, changing an offset layer on the very high level could // 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); builder.pop(); - return engineLayer; } @override @@ -942,6 +1036,7 @@ class OffsetLayer extends ContainerLayer { transform.scale(pixelRatio, pixelRatio); builder.pushTransform(transform.storage); final ui.Scene scene = buildScene(builder); + try { // Size is rounded up to the next pixel to make sure we don't clip off // anything. @@ -963,10 +1058,10 @@ class OffsetLayer extends ContainerLayer { class ClipRectLayer extends ContainerLayer { /// Creates a layer with a rectangular clip. /// - /// The [clipRect] property must be non-null before the compositing phase of - /// the pipeline. + /// The [clipRect] and [clipBehavior] properties must be non-null before the + /// compositing phase of the pipeline. ClipRectLayer({ - @required Rect clipRect, + Rect clipRect, Clip clipBehavior = Clip.hardEdge, }) : _clipRect = clipRect, _clipBehavior = clipBehavior, @@ -1017,7 +1112,9 @@ class ClipRectLayer extends ContainerLayer { } @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; assert(() { enabled = !debugDisableClipLayers; @@ -1025,12 +1122,13 @@ class ClipRectLayer extends ContainerLayer { }()); if (enabled) { 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); if (enabled) builder.pop(); - return null; // this does not return an engine layer yet. } @override @@ -1048,10 +1146,10 @@ class ClipRectLayer extends ContainerLayer { class ClipRRectLayer extends ContainerLayer { /// Creates a layer with a rounded-rectangular clip. /// - /// The [clipRRect] property must be non-null before the compositing phase of - /// the pipeline. + /// The [clipRRect] and [clipBehavior] properties must be non-null before the + /// compositing phase of the pipeline. ClipRRectLayer({ - @required RRect clipRRect, + RRect clipRRect, Clip clipBehavior = Clip.antiAlias, }) : _clipRRect = clipRRect, _clipBehavior = clipBehavior, @@ -1098,7 +1196,9 @@ class ClipRRectLayer extends ContainerLayer { } @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; assert(() { enabled = !debugDisableClipLayers; @@ -1106,12 +1206,13 @@ class ClipRRectLayer extends ContainerLayer { }()); if (enabled) { 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); if (enabled) builder.pop(); - return null; // this does not return an engine layer yet. } @override @@ -1129,10 +1230,10 @@ class ClipRRectLayer extends ContainerLayer { class ClipPathLayer extends ContainerLayer { /// Creates a layer with a path-based clip. /// - /// The [clipPath] property must be non-null before the compositing phase of - /// the pipeline. + /// The [clipPath] and [clipBehavior] properties must be non-null before the + /// compositing phase of the pipeline. ClipPathLayer({ - @required Path clipPath, + Path clipPath, Clip clipBehavior = Clip.antiAlias, }) : _clipPath = clipPath, _clipBehavior = clipBehavior, @@ -1179,7 +1280,9 @@ class ClipPathLayer extends ContainerLayer { } @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; assert(() { enabled = !debugDisableClipLayers; @@ -1187,12 +1290,13 @@ class ClipPathLayer extends ContainerLayer { }()); if (enabled) { 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); if (enabled) builder.pop(); - return null; // this does not return an engine layer yet. } } @@ -1200,12 +1304,11 @@ class ClipPathLayer extends ContainerLayer { class ColorFilterLayer extends ContainerLayer { /// 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. ColorFilterLayer({ - @required ColorFilter colorFilter, - }) : _colorFilter = colorFilter, - assert(colorFilter != null); + ColorFilter colorFilter, + }) : _colorFilter = colorFilter; /// The color filter to apply to children. /// @@ -1214,6 +1317,7 @@ class ColorFilterLayer extends ContainerLayer { ColorFilter get colorFilter => _colorFilter; ColorFilter _colorFilter; set colorFilter(ColorFilter value) { + assert(value != null); if (value != _colorFilter) { _colorFilter = value; markNeedsAddToScene(); @@ -1221,11 +1325,11 @@ class ColorFilterLayer extends ContainerLayer { } @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { - builder.pushColorFilter(colorFilter); + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + assert(colorFilter != null); + engineLayer = builder.pushColorFilter(colorFilter, oldLayer: _engineLayer); addChildrenToScene(builder, layerOffset); builder.pop(); - return null; // this does not return an engine layer yet. } @override @@ -1246,8 +1350,7 @@ class TransformLayer extends OffsetLayer { /// The [transform] and [offset] properties must be non-null before the /// compositing phase of the pipeline. TransformLayer({ Matrix4 transform, Offset offset = Offset.zero }) - : assert(transform.storage.every((double value) => value.isFinite)), - _transform = transform, + : _transform = transform, super(offset: offset); /// The matrix to apply. @@ -1262,10 +1365,13 @@ class TransformLayer extends OffsetLayer { Matrix4 get transform => _transform; Matrix4 _transform; set transform(Matrix4 value) { + assert(value != null); + assert(value.storage.every((double component) => component.isFinite)); if (value == _transform) return; _transform = value; _inverseDirty = true; + markNeedsAddToScene(); } Matrix4 _lastEffectiveTransform; @@ -1273,17 +1379,17 @@ class TransformLayer extends OffsetLayer { bool _inverseDirty = true; @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; final Offset totalOffset = offset + layerOffset; if (totalOffset != Offset.zero) { _lastEffectiveTransform = Matrix4.translationValues(totalOffset.dx, totalOffset.dy, 0.0) ..multiply(_lastEffectiveTransform); } - builder.pushTransform(_lastEffectiveTransform.storage); + engineLayer = builder.pushTransform(_lastEffectiveTransform.storage, oldLayer: _engineLayer); addChildrenToScene(builder); builder.pop(); - return null; // this does not return an engine layer yet. } Offset _transformOffset(Offset regionOffset) { @@ -1348,7 +1454,7 @@ class OpacityLayer extends ContainerLayer { /// The [alpha] property must be non-null before the compositing phase of /// the pipeline. OpacityLayer({ - @required int alpha, + int alpha, Offset offset = Offset.zero, }) : _alpha = alpha, _offset = offset; @@ -1363,6 +1469,7 @@ class OpacityLayer extends ContainerLayer { int get alpha => _alpha; int _alpha; set alpha(int value) { + assert(value != null); if (value != _alpha) { _alpha = value; markNeedsAddToScene(); @@ -1387,18 +1494,21 @@ class OpacityLayer extends ContainerLayer { } @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 assert(() { enabled = enabled && !debugDisableOpacityLayers; return true; }()); + if (enabled) - builder.pushOpacity(alpha, offset: offset + layerOffset); + engineLayer = builder.pushOpacity(alpha, offset: offset + layerOffset, oldLayer: _engineLayer); + else + engineLayer = null; addChildrenToScene(builder); if (enabled) builder.pop(); - return null; // this does not return an engine layer yet. } @override @@ -1416,9 +1526,9 @@ class ShaderMaskLayer extends ContainerLayer { /// The [shader], [maskRect], and [blendMode] properties must be non-null /// before the compositing phase of the pipeline. ShaderMaskLayer({ - @required Shader shader, - @required Rect maskRect, - @required BlendMode blendMode, + Shader shader, + Rect maskRect, + BlendMode blendMode, }) : _shader = shader, _maskRect = maskRect, _blendMode = blendMode; @@ -1463,12 +1573,14 @@ class ShaderMaskLayer extends ContainerLayer { } @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); - builder.pushShaderMask(shader, shiftedMaskRect, blendMode); + engineLayer = builder.pushShaderMask(shader, shiftedMaskRect, blendMode, oldLayer: _engineLayer); addChildrenToScene(builder, layerOffset); builder.pop(); - return null; // this does not return an engine layer yet. } @override @@ -1486,7 +1598,7 @@ class BackdropFilterLayer extends ContainerLayer { /// /// The [filter] property must be non-null before the compositing phase of the /// 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. /// @@ -1502,11 +1614,11 @@ class BackdropFilterLayer extends ContainerLayer { } @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { - builder.pushBackdropFilter(filter); + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + assert(filter != null); + engineLayer = builder.pushBackdropFilter(filter, oldLayer: _engineLayer); addChildrenToScene(builder, layerOffset); builder.pop(); - return null; // this does not return an engine layer yet. } } @@ -1523,19 +1635,15 @@ class PhysicalModelLayer extends ContainerLayer { /// Creates a composited layer that uses a physical model to producing /// 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({ - @required Path clipPath, + Path clipPath, Clip clipBehavior = Clip.none, - @required double elevation, - @required Color color, - @required Color shadowColor, - }) : assert(clipPath != null), - assert(clipBehavior != null), - assert(elevation != null), - assert(color != null), - assert(shadowColor != null), - _clipPath = clipPath, + double elevation, + Color color, + Color shadowColor, + }) : _clipPath = clipPath, _clipBehavior = clipBehavior, _elevation = elevation, _color = color, @@ -1632,8 +1740,13 @@ class PhysicalModelLayer extends ContainerLayer { } @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { - ui.EngineLayer engineLayer; + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + assert(clipPath != null); + assert(clipBehavior != null); + assert(elevation != null); + assert(color != null); + assert(shadowColor != null); + bool enabled = true; assert(() { enabled = !debugDisablePhysicalShapeLayers; @@ -1646,12 +1759,14 @@ class PhysicalModelLayer extends ContainerLayer { color: color, shadowColor: shadowColor, clipBehavior: clipBehavior, + oldLayer: _engineLayer, ); + } else { + engineLayer = null; } addChildrenToScene(builder, layerOffset); if (enabled) builder.pop(); - return engineLayer; } @override @@ -1697,13 +1812,18 @@ class LeaderLayer extends ContainerLayer { /// /// The [offset] property must be non-null before the compositing phase of the /// 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 link will be established when this layer is [attach]ed, and will be /// 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. /// @@ -1748,15 +1868,14 @@ class LeaderLayer extends ContainerLayer { Iterable findAll(Offset regionOffset) => super.findAll(regionOffset - offset); @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { assert(offset != null); _lastOffset = offset + layerOffset; 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); if (_lastOffset != Offset.zero) builder.pop(); - return null; // this does not have an engine layer. } /// Applies the transform that would be applied when compositing the given @@ -1799,18 +1918,23 @@ class FollowerLayer extends ContainerLayer { /// The [unlinkedOffset], [linkedOffset], and [showWhenUnlinked] properties /// must be non-null before the compositing phase of the pipeline. FollowerLayer({ - @required this.link, + @required LayerLink link, this.showWhenUnlinked = true, this.unlinkedOffset = Offset.zero, this.linkedOffset = Offset.zero, - }) : assert(link != null); + }) : assert(link != null), _link = link; /// The link to the [LeaderLayer]. /// /// 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 /// 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 /// [LeaderLayer]. @@ -1970,43 +2094,43 @@ class FollowerLayer extends ContainerLayer { } /// {@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 - /// in the Layer tree, and that LeaderLayer could change without notifying the - /// FollowerLayer. Therefore we have to always call a FollowerLayer's - /// [addToScene]. In order to call FollowerLayer's [addToScene], LeaderLayer's - /// [addToScene] must be called first so LeaderLayer must also be considered + /// A [FollowerLayer] copies changes from a [LeaderLayer] that could be anywhere + /// in the Layer tree, and that leader layer could change without notifying the + /// follower layer. Therefore we have to always call a follower layer's + /// [addToScene]. In order to call follower layer's [addToScene], leader layer's + /// [addToScene] must be called first so leader layer must also be considered /// as [alwaysNeedsAddToScene]. /// {@endtemplate} @override bool get alwaysNeedsAddToScene => true; @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { assert(link != null); assert(showWhenUnlinked != null); if (link.leader == null && !showWhenUnlinked) { _lastTransform = null; _lastOffset = null; _inverseDirty = true; - return null; // this does not have an engine layer. + engineLayer = null; + return; } _establishTransform(); if (_lastTransform != null) { - builder.pushTransform(_lastTransform.storage); + engineLayer = builder.pushTransform(_lastTransform.storage, oldLayer: _engineLayer); addChildrenToScene(builder); builder.pop(); _lastOffset = unlinkedOffset + layerOffset; } else { _lastOffset = null; final Matrix4 matrix = Matrix4.translationValues(unlinkedOffset.dx, unlinkedOffset.dy, .0); - builder.pushTransform(matrix.storage); + engineLayer = builder.pushTransform(matrix.storage, oldLayer: _engineLayer); addChildrenToScene(builder); builder.pop(); } _inverseDirty = true; - return null; // this does not have an engine layer. } @override diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index cf0d5f12dd..ad4dc631f1 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -112,19 +112,31 @@ class PaintingContext extends ClipContext { ); return true; }()); - if (child._layer == null) { + OffsetLayer childLayer = child._layer; + if (childLayer == null) { 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 { - assert(debugAlsoPaintedParent || child._layer.attached); - child._layer.removeAllChildren(); + assert(childLayer is OffsetLayer); + assert(debugAlsoPaintedParent || childLayer.attached); + childLayer.removeAllChildren(); } + assert(identical(childLayer, child._layer)); + assert(child._layer is OffsetLayer); assert(() { child._layer.debugCreator = child.debugCreator ?? child.runtimeType; return true; }()); childContext ??= PaintingContext(child._layer, child.paintBounds); 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(); } @@ -188,7 +200,6 @@ class PaintingContext extends ClipContext { if (child._needsPaint) { repaintCompositedChild(child, debugAlsoPaintedParent: true); } else { - assert(child._layer != null); assert(() { // register the call for RepaintBoundary metrics child.debugRegisterRepaintBoundaryPaint( @@ -199,8 +210,9 @@ class PaintingContext extends ClipContext { return true; }()); } - assert(child._layer != null); - child._layer.offset = offset; + assert(child._layer is OffsetLayer); + final OffsetLayer childOffsetLayer = child._layer; + childOffsetLayer.offset = offset; appendLayer(child._layer); } @@ -360,9 +372,12 @@ class PaintingContext extends ClipContext { /// /// * [addLayer], for pushing a leaf layer whose canvas is not used. void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) { - assert(!childLayer.attached); - assert(childLayer.parent == 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(); appendLayer(childLayer); final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds); @@ -378,28 +393,47 @@ class PaintingContext extends ClipContext { /// Clip further painting using a rectangle. /// + /// {@template flutter.rendering.object.needsCompositing} /// * `needsCompositing` is whether the child needs compositing. Typically - /// matches the value of [RenderObject.needsCompositing] for the caller. - /// * `offset` is the offset from the origin of the canvas's coordinate system + /// matches the value of [RenderObject.needsCompositing] for the caller. If + /// 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. /// * `clipRect` is rectangle (in the caller's coordinate system) to use to /// clip the painting done by [painter]. /// * `painter` is a callback that will paint with the [clipRect] applied. This /// function calls the [painter] synchronously. /// * `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); 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 { clipRectAndPaint(offsetClipRect, clipBehavior, offsetClipRect, () => painter(this, offset)); + return null; } } /// Clip further painting using a rounded rectangle. /// - /// * `needsCompositing` is whether the child needs compositing. Typically - /// matches the value of [RenderObject.needsCompositing] for the caller. + /// {@macro flutter.rendering.object.needsCompositing} /// * `offset` is the offset from the origin of the canvas' coordinate system /// to the origin of 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 { /// * `painter` is a callback that will paint with the `clipRRect` applied. This /// function calls the `painter` synchronously. /// * `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); final Rect offsetBounds = bounds.shift(offset); final RRect offsetClipRRect = clipRRect.shift(offset); 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 { clipRRectAndPaint(offsetClipRRect, clipBehavior, offsetBounds, () => painter(this, offset)); + return null; } } /// Clip further painting using a path. /// - /// * `needsCompositing` is whether the child needs compositing. Typically - /// matches the value of [RenderObject.needsCompositing] for the caller. + /// {@macro flutter.rendering.object.needsCompositing} /// * `offset` is the offset from the origin of the canvas' coordinate system /// to the origin of 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 { /// * `painter` is a callback that will paint with the `clipPath` applied. This /// function calls the `painter` synchronously. /// * `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); final Rect offsetBounds = bounds.shift(offset); final Path offsetClipPath = clipPath.shift(offset); 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 { clipPathAndPaint(offsetClipPath, clipBehavior, offsetBounds, () => painter(this, offset)); + return null; } } @@ -452,35 +499,42 @@ class PaintingContext extends ClipContext { /// painting done by `painter`. /// * `painter` is a callback that will paint with the `colorFilter` applied. /// This function calls the `painter` synchronously. + /// {@macro flutter.rendering.object.oldLayer} /// /// A [RenderObject] that uses this function is very likely to require its /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs /// ancestor render objects that this render object will include a composited /// 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); - 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. /// - /// * `needsCompositing` is whether the child needs compositing. Typically - /// matches the value of [RenderObject.needsCompositing] for the caller. + /// {@macro flutter.rendering.object.needsCompositing} /// * `offset` is the offset from the origin of the canvas' coordinate system /// to the origin of the caller's coordinate system. /// * `transform` is the matrix to apply to the painting done by `painter`. /// * `painter` is a callback that will paint with the `transform` applied. This /// 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) ..multiply(transform)..translate(-offset.dx, -offset.dy); if (needsCompositing) { + final TransformLayer layer = oldLayer ?? TransformLayer(); + layer.transform = effectiveTransform; pushLayer( - TransformLayer(transform: effectiveTransform), + layer, painter, offset, childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, estimatedBounds), ); + return layer; } else { canvas ..save() @@ -488,6 +542,7 @@ class PaintingContext extends ClipContext { painter(this, offset); canvas ..restore(); + return null; } } @@ -500,13 +555,19 @@ class PaintingContext extends ClipContext { /// and an alpha value of 255 means the painting is fully opaque. /// * `painter` is a callback that will paint with the `alpha` applied. This /// function calls the `painter` synchronously. + /// {@macro flutter.rendering.object.oldLayer} /// /// A [RenderObject] that uses this function is very likely to require its /// [RenderObject.alwaysNeedsCompositing] property to return true. That informs /// ancestor render objects that this render object will include a composited /// layer, which, for example, causes them to use composited clips. - void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) { - pushLayer(OpacityLayer(alpha: alpha, offset: offset), painter, Offset.zero); + OpacityLayer pushOpacity(Offset offset, int alpha, PaintingContextCallback painter, { OpacityLayer oldLayer }) { + final OpacityLayer layer = oldLayer ?? OpacityLayer(); + layer + ..alpha = alpha + ..offset = offset; + pushLayer(layer, painter, Offset.zero); + return layer; } @override @@ -1781,7 +1842,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im /// to repaint. /// /// 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. bool get isRepaintBoundary => false; @@ -1804,19 +1868,41 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im @protected bool get alwaysNeedsCompositing => false; - OffsetLayer _layer; /// The compositing layer that this render object uses to repaint. /// - /// Call only when [isRepaintBoundary] is true and the render object has - /// already painted. + /// If this render object is not a repaint boundary, it is the responsibility + /// 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 + /// render object implementation. When repainting, instead of creating a new + /// layer the render object may update the layer stored in this field for better + /// performance. It is also OK to leave this field as null and create a new + /// layer on every repaint, but without the performance benefit. If + /// [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. /// - /// To access the layer in debug code, even when it might be inappropriate to - /// access it (e.g. because it is dirty), consider [debugLayer]. - OffsetLayer get layer { - assert(isRepaintBoundary, 'You can only access RenderObject.layer for render objects that are repaint boundaries.'); - assert(!_needsPaint); + /// 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; } + + @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. /// /// This getter is intended for debugging purposes only. In release builds, it @@ -1824,8 +1910,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im /// is dirty. /// /// For production code, consider [layer]. - OffsetLayer get debugLayer { - OffsetLayer result; + ContainerLayer get debugLayer { + ContainerLayer result; assert(() { result = _layer; return true; @@ -1961,16 +2047,12 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im }()); // If we always have our own layer, then we can just repaint // ourselves without involving any other nodes. - assert(_layer != null); + assert(_layer is OffsetLayer); if (owner != null) { owner._nodesNeedingPaint.add(this); owner.requestVisualUpdate(); } } 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; parent.markNeedsPaint(); assert(parent == this.parent); @@ -2682,7 +2764,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im properties.add(DiagnosticsProperty('parentData', parentData, tooltip: _debugCanParentUseSize == true ? 'can use size' : null, missingIfNull: true)); properties.add(DiagnosticsProperty('constraints', constraints, missingIfNull: true)); // don't access it via the "layer" getter since that's only valid when we don't need paint - properties.add(DiagnosticsProperty('layer', _layer, defaultValue: null)); + properties.add(DiagnosticsProperty('layer', _layer, defaultValue: null)); properties.add(DiagnosticsProperty('semantics node', _semantics, defaultValue: null)); properties.add(FlagProperty( 'isBlockingSemanticsOfPreviouslyPaintedNodes', diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index c02f69033e..308ec1f3a4 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -789,14 +789,18 @@ class RenderOpacity extends RenderProxyBox { void paint(PaintingContext context, Offset offset) { if (child != null) { if (_alpha == 0) { + // No need to keep the layer. We'll create a new one if necessary. + layer = null; return; } if (_alpha == 255) { + // No need to keep the layer. We'll create a new one if necessary. + layer = null; context.paintChild(child, offset); return; } 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 { @override void paint(PaintingContext context, Offset offset) { 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; + } if (_alpha == 255) { + // No need to keep the layer. We'll create a new one if necessary. + layer = null; context.paintChild(child, offset); return; } 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 { _blendMode = blendMode, super(child); + @override + ShaderMaskLayer get layer => super.layer; + /// Called to creates the [Shader] that generates the mask. /// /// The shader callback is called with the current size of the child so that @@ -989,15 +1001,14 @@ class RenderShaderMask extends RenderProxyBox { void paint(PaintingContext context, Offset offset) { if (child != null) { assert(needsCompositing); - context.pushLayer( - ShaderMaskLayer( - shader: _shaderCallback(offset & size), - maskRect: offset & size, - blendMode: _blendMode, - ), - super.paint, - offset, - ); + layer ??= ShaderMaskLayer(); + layer + ..shader = _shaderCallback(offset & size) + ..maskRect = offset & size + ..blendMode = _blendMode; + context.pushLayer(layer, super.paint, offset); + } else { + layer = null; } } } @@ -1015,6 +1026,9 @@ class RenderBackdropFilter extends RenderProxyBox { _filter = filter, super(child); + @override + BackdropFilterLayer get layer => super.layer; + /// The image filter to apply to the existing painted content before painting /// the child. /// @@ -1037,7 +1051,11 @@ class RenderBackdropFilter extends RenderProxyBox { void paint(PaintingContext context, Offset offset) { if (child != null) { 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 { void paint(PaintingContext context, Offset offset) { if (child != null) { _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 { void paint(PaintingContext context, Offset offset) { if (child != null) { _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 { void paint(PaintingContext context, Offset offset) { if (child != null) { _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 { void paint(PaintingContext context, Offset offset) { if (child != null) { _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 { shadowColor: shadowColor ); + @override + PhysicalModelLayer get layer => super.layer; + /// The shape of the layer. /// /// Defaults to [BoxShape.rectangle]. The [borderRadius] affects the corners @@ -1710,18 +1739,20 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase { } return true; }()); - final PhysicalModelLayer physicalModel = PhysicalModelLayer( - clipPath: offsetRRectAsPath, - clipBehavior: clipBehavior, - elevation: paintShadows ? elevation : 0.0, - color: color, - shadowColor: shadowColor, - ); + layer ??= PhysicalModelLayer(); + layer + ..clipPath = offsetRRectAsPath + ..clipBehavior = clipBehavior + ..elevation = paintShadows ? elevation : 0.0 + ..color = color + ..shadowColor = shadowColor; + context.pushLayer(layer, super.paint, offset, childPaintBounds: offsetBounds); assert(() { - physicalModel.debugCreator = debugCreator; + layer.debugCreator = debugCreator; return true; }()); - context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds); + } else { + layer = null; } } @@ -1768,6 +1799,9 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase { clipBehavior: clipBehavior ); + @override + PhysicalModelLayer get layer => super.layer; + @override Path get _defaultClip => Path()..addRect(Offset.zero & size); @@ -1804,18 +1838,20 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase { } return true; }()); - final PhysicalModelLayer physicalModel = PhysicalModelLayer( - clipPath: offsetPath, - clipBehavior: clipBehavior, - elevation: paintShadows ? elevation : 0.0, - color: color, - shadowColor: shadowColor, - ); + layer ??= PhysicalModelLayer(); + layer + ..clipPath = offsetPath + ..clipBehavior = clipBehavior + ..elevation = paintShadows ? elevation : 0.0 + ..color = color + ..shadowColor = shadowColor; + context.pushLayer(layer, super.paint, offset, childPaintBounds: offsetBounds); assert(() { - physicalModel.debugCreator = debugCreator; + layer.debugCreator = debugCreator; return true; }()); - context.pushLayer(physicalModel, super.paint, offset, childPaintBounds: offsetBounds); + } else { + layer = null; } } @@ -2145,10 +2181,12 @@ class RenderTransform extends RenderProxyBox { if (child != null) { final Matrix4 transform = _effectiveTransform; final Offset childOffset = MatrixUtils.getAsTranslation(transform); - if (childOffset == null) - context.pushTransform(needsCompositing, offset, transform, super.paint); - else + if (childOffset == null) { + layer = context.pushTransform(needsCompositing, offset, transform, super.paint, oldLayer: layer); + } else { super.paint(context, offset + childOffset); + layer = null; + } } } @@ -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); 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 super.paint(context, offset + childOffset); + return null; } @override @@ -2303,9 +2343,10 @@ class RenderFittedBox extends RenderProxyBox { _updatePaintData(); if (child != null) { 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 - _paintChildWithTransform(context, offset); + layer = _paintChildWithTransform(context, offset); } } @@ -2706,6 +2747,7 @@ class RenderMouseRegion extends RenderProxyBox { @override void paint(PaintingContext context, Offset offset) { if (_annotationIsActive) { + // Annotated region layers are not retained because they do not create engine layers. final AnnotatedRegionLayer layer = AnnotatedRegionLayer( _hoverAnnotation, size: size, @@ -2832,7 +2874,8 @@ class RenderRepaintBoundary extends RenderProxyBox { /// * [dart:ui.Scene.toImage] for more information about the image returned. Future toImage({ double pixelRatio = 1.0 }) { 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 { @override 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 @@ -4743,7 +4795,7 @@ class RenderFollowerLayer extends RenderProxyBox { @override void detach() { - _layer = null; + layer = null; super.detach(); } @@ -4751,7 +4803,8 @@ class RenderFollowerLayer extends RenderProxyBox { bool get alwaysNeedsCompositing => true; /// 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. /// @@ -4760,7 +4813,7 @@ class RenderFollowerLayer extends RenderProxyBox { /// [FollowerLayer.getLastTransform]), this returns the identity matrix (see /// [new Matrix4.identity]. Matrix4 getCurrentTransform() { - return _layer?.getLastTransform() ?? Matrix4.identity(); + return layer?.getLastTransform() ?? Matrix4.identity(); } @override @@ -4786,14 +4839,22 @@ class RenderFollowerLayer extends RenderProxyBox { @override void paint(PaintingContext context, Offset offset) { assert(showWhenUnlinked != null); - _layer = FollowerLayer( - link: link, - showWhenUnlinked: showWhenUnlinked, - linkedOffset: this.offset, - unlinkedOffset: offset, - ); + if (layer == null) { + layer = FollowerLayer( + link: link, + showWhenUnlinked: showWhenUnlinked, + linkedOffset: this.offset, + unlinkedOffset: offset, + ); + } else { + layer + ..link = link + ..showWhenUnlinked = showWhenUnlinked + ..linkedOffset = this.offset + ..unlinkedOffset = offset; + } context.pushLayer( - _layer, + layer, super.paint, Offset.zero, childPaintBounds: const Rect.fromLTRB( @@ -4871,6 +4932,7 @@ class RenderAnnotatedRegion extends RenderProxyBox { @override void paint(PaintingContext context, Offset offset) { + // Annotated region layers are not retained because they do not create engine layers. final AnnotatedRegionLayer layer = AnnotatedRegionLayer( value, size: sized ? size : null, diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index a29186d0c8..81d0f90afa 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -7,6 +7,7 @@ import 'dart:io' show Platform; import 'dart:ui' as ui show Scene, SceneBuilder, Window; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart' show MouseTrackerAnnotation; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; @@ -173,6 +174,19 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin 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 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(position * configuration.devicePixelRatio); + } + @override bool get isRepaintBoundary => true; diff --git a/packages/flutter/lib/src/widgets/color_filter.dart b/packages/flutter/lib/src/widgets/color_filter.dart index 923784d23a..cad96a2ee1 100644 --- a/packages/flutter/lib/src/widgets/color_filter.dart +++ b/packages/flutter/lib/src/widgets/color_filter.dart @@ -55,6 +55,6 @@ class _ColorFilterRenderObject extends RenderProxyBox { @override void paint(PaintingContext context, Offset offset) { - context.pushColorFilter(offset, colorFilter, super.paint); + layer = context.pushColorFilter(offset, colorFilter, super.paint, oldLayer: layer); } } diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index 23820de1f8..62dc21c58e 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -10,7 +10,6 @@ import 'dart:typed_data'; import 'dart:ui' as ui show ClipOp, - EngineLayer, Image, ImageByteFormat, Paragraph, @@ -53,8 +52,8 @@ class _ProxyLayer extends Layer { final Layer _layer; @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { - return _layer.addToScene(builder, layerOffset); + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + _layer.addToScene(builder, layerOffset); } @override @@ -314,9 +313,8 @@ Rect _calculateSubtreeBounds(RenderObject object) { /// screenshots render to the scene in the local coordinate system of the layer. class _ScreenshotContainerLayer extends OffsetLayer { @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { addChildrenToScene(builder, layerOffset); - return null; // this does not have an engine layer. } } @@ -556,9 +554,10 @@ class _ScreenshotPaintingContext extends PaintingContext { // Painting the existing repaint boundary to the screenshot is sufficient. // We don't just take a direct screenshot of the repaint boundary as we // want to capture debugPaint information as well. - data.containerLayer.append(_ProxyLayer(repaintBoundary.layer)); + data.containerLayer.append(_ProxyLayer(repaintBoundary.debugLayer)); data.foundTarget = true; - data.screenshotOffset = repaintBoundary.layer.offset; + final OffsetLayer offsetLayer = repaintBoundary.debugLayer; + data.screenshotOffset = offsetLayer.offset; } else { // Repaint everything under the repaint boundary. // We call debugInstrumentRepaintCompositedChild instead of paintChild as @@ -591,7 +590,7 @@ class _ScreenshotPaintingContext extends PaintingContext { // We must build the regular scene before we can build the screenshot // scene as building the screenshot scene assumes addToScene has already // 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); } @@ -2504,9 +2503,9 @@ class _InspectorOverlayLayer extends Layer { double _textPainterMaxWidth; @override - ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { + void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { if (!selection.active) - return null; + return; final RenderObject selected = selection.current; final List<_TransformedRect> candidates = <_TransformedRect>[]; @@ -2529,7 +2528,6 @@ class _InspectorOverlayLayer extends Layer { _picture = _buildPicture(state); } builder.addPicture(layerOffset, _picture); - return null; // this does not have an engine layer. } ui.Picture _buildPicture(_InspectorOverlayRenderState state) { diff --git a/packages/flutter/test/rendering/aspect_ratio_test.dart b/packages/flutter/test/rendering/aspect_ratio_test.dart index 7fd36908fc..d0e1c0f99a 100644 --- a/packages/flutter/test/rendering/aspect_ratio_test.dart +++ b/packages/flutter/test/rendering/aspect_ratio_test.dart @@ -99,11 +99,6 @@ void main() { }); test('RenderAspectRatio: Unbounded', () { - bool hadError = false; - final FlutterExceptionHandler oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - hadError = true; - }; final RenderBox box = RenderConstrainedOverflowBox( maxWidth: double.infinity, maxHeight: double.infinity, @@ -112,10 +107,16 @@ void main() { child: RenderSizedBox(const Size(90.0, 70.0)), ), ); - expect(hadError, false); - layout(box); - expect(hadError, true); - FlutterError.onError = oldHandler; + + final List errorMessages = []; + layout(box, onErrors: () { + 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', () { diff --git a/packages/flutter/test/rendering/flex_test.dart b/packages/flutter/test/rendering/flex_test.dart index a4fc043275..a80493a647 100644 --- a/packages/flutter/test/rendering/flex_test.dart +++ b/packages/flutter/test/rendering/flex_test.dart @@ -365,10 +365,6 @@ void main() { }); test('MainAxisSize.min inside unconstrained', () { - final List exceptions = []; - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0); final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square); @@ -387,17 +383,15 @@ void main() { flex.addAll([box1, box2, box3]); final FlexParentData box2ParentData = box2.parentData; box2ParentData.flex = 1; - expect(exceptions, isEmpty); - layout(parent); + final List exceptions = []; + layout(parent, onErrors: () { + exceptions.addAll(renderer.takeAllFlutterExceptions()); + }); expect(exceptions, isNotEmpty); expect(exceptions.first, isInstanceOf()); }); test('MainAxisSize.min inside unconstrained', () { - final List exceptions = []; - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0); final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square); final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square); @@ -417,8 +411,10 @@ void main() { final FlexParentData box2ParentData = box2.parentData; box2ParentData.flex = 1; box2ParentData.fit = FlexFit.loose; - expect(exceptions, isEmpty); - layout(parent); + final List exceptions = []; + layout(parent, onErrors: () { + exceptions.addAll(renderer.takeAllFlutterExceptions()); + }); expect(exceptions, isNotEmpty); expect(exceptions.first, isInstanceOf()); }); diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart index 0137209973..63a3f23303 100644 --- a/packages/flutter/test/rendering/layers_test.dart +++ b/packages/flutter/test/rendering/layers_test.dart @@ -22,29 +22,75 @@ void main() { ); layout(root, phase: EnginePhase.paint); expect(inner.isRepaintBoundary, isFalse); - expect(() => inner.layer, throwsAssertionError); + expect(inner.debugLayer, null); expect(boundary.isRepaintBoundary, isTrue); - expect(boundary.layer, isNotNull); - expect(boundary.layer.attached, isTrue); // this time it painted... + expect(boundary.debugLayer, isNotNull); + expect(boundary.debugLayer.attached, isTrue); // this time it painted... root.opacity = 0.0; pumpFrame(phase: EnginePhase.paint); expect(inner.isRepaintBoundary, isFalse); - expect(() => inner.layer, throwsAssertionError); + expect(inner.debugLayer, null); expect(boundary.isRepaintBoundary, isTrue); - expect(boundary.layer, isNotNull); - expect(boundary.layer.attached, isFalse); // this time it did not. + expect(boundary.debugLayer, isNotNull); + expect(boundary.debugLayer.attached, isFalse); // this time it did not. root.opacity = 0.5; pumpFrame(phase: EnginePhase.paint); expect(inner.isRepaintBoundary, isFalse); - expect(() => inner.layer, throwsAssertionError); + expect(inner.debugLayer, null); expect(boundary.isRepaintBoundary, isTrue); - expect(boundary.layer, isNotNull); - expect(boundary.layer.attached, isTrue); // this time it did again! + expect(boundary.debugLayer, isNotNull); + 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 b = ContainerLayer(); final ContainerLayer c = ContainerLayer(); @@ -52,53 +98,59 @@ void main() { final ContainerLayer e = ContainerLayer(); final ContainerLayer f = ContainerLayer(); final ContainerLayer g = ContainerLayer(); - - final PictureLayer h = PictureLayer(Rect.zero); - final PictureLayer i = PictureLayer(Rect.zero); - final PictureLayer j = PictureLayer(Rect.zero); + final List allLayers = [a, b, c, d, e, f, g]; // The tree is like the following where b and j are dirty: // a____ // / \ // (x)b___ c // / \ \ | - // d e f g - // / \ | - // h i j(x) + // d e f g(x) a.append(b); a.append(c); b.append(d); b.append(e); b.append(f); - d.append(h); - d.append(i); 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(); - c.debugMarkClean(); - d.debugMarkClean(); - e.debugMarkClean(); - f.debugMarkClean(); - g.debugMarkClean(); - h.debugMarkClean(); - i.debugMarkClean(); - j.markNeedsAddToScene(); + a.updateSubtreeNeedsAddToScene(); + 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(); expect(a.debugSubtreeNeedsAddToScene, true); expect(b.debugSubtreeNeedsAddToScene, true); expect(c.debugSubtreeNeedsAddToScene, true); - expect(g.debugSubtreeNeedsAddToScene, true); - expect(j.debugSubtreeNeedsAddToScene, true); - expect(d.debugSubtreeNeedsAddToScene, false); expect(e.debugSubtreeNeedsAddToScene, false); expect(f.debugSubtreeNeedsAddToScene, false); - expect(h.debugSubtreeNeedsAddToScene, false); - expect(i.debugSubtreeNeedsAddToScene, false); + expect(g.debugSubtreeNeedsAddToScene, true); + + a.buildScene(SceneBuilder()); + for (ContainerLayer layer in allLayers) { + expect(layer.debugSubtreeNeedsAddToScene, false); + } }); test('leader and follower layers are always dirty', () { @@ -465,4 +517,27 @@ void main() { _testConflicts(layerA, layerB, expectedErrorCount: 1); }); }, 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; } diff --git a/packages/flutter/test/rendering/object_test.dart b/packages/flutter/test/rendering/object_test.dart index 96df3ae9dc..fcc868ce73 100644 --- a/packages/flutter/test/rendering/object_test.dart +++ b/packages/flutter/test/rendering/object_test.dart @@ -5,13 +5,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding; import 'package:flutter_test/flutter_test.dart'; +import 'rendering_tester.dart'; + void main() { test('ensure frame is scheduled for markNeedsSemanticsUpdate', () { // Initialize all bindings because owner.flushSemantics() requires a window - TestWidgetsFlutterBinding.ensureInitialized(); + renderer; final TestRenderObject renderObject = TestRenderObject(); int onNeedVisualUpdateCallCount = 0; @@ -98,6 +99,81 @@ void main() { ..nextSibling = RenderOpacity(); expect(() => data3.detach(), throwsAssertionError); }); + + test('PaintingContext.pushClipRect reuses the layer', () { + _testPaintingContextLayerReuse((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((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((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((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((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((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(_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()); + 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 paintedLayers = []; + + @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 { } diff --git a/packages/flutter/test/rendering/paint_error_test.dart b/packages/flutter/test/rendering/paint_error_test.dart index 47d9031fa5..c1b646cd88 100644 --- a/packages/flutter/test/rendering/paint_error_test.dart +++ b/packages/flutter/test/rendering/paint_error_test.dart @@ -16,17 +16,11 @@ void main() { // compatible with existing tests in object_test.dart. test('reentrant paint error', () { FlutterErrorDetails errorDetails; - final FlutterExceptionHandler oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - errorDetails = details; - }; final RenderBox root = TestReentrantPaintingErrorRenderBox(); - try { - layout(root); - pumpFrame(phase: EnginePhase.paint); - } finally { - FlutterError.onError = oldHandler; - } + layout(root, onErrors: () { + errorDetails = renderer.takeFlutterErrorDetails(); + }); + pumpFrame(phase: EnginePhase.paint); expect(errorDetails, isNotNull); expect(errorDetails.stack, isNotNull); diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index 873c9ca409..d6b8cc71c5 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. 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/foundation.dart'; @@ -218,7 +218,7 @@ void main() { expect(getPixel(0, 0), equals(0x00000080)); 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)); expect(image.width, equals(20)); @@ -268,6 +268,13 @@ void main() { expect(renderOpacity.needsCompositing, false); }); + test('RenderOpacity reuses its layer', () { + _testLayerReuse(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 { final Animation opacityAnimation = AnimationController( vsync: _FakeTickerProvider(), @@ -297,6 +304,183 @@ void main() { layout(renderAnimatedOpacity, phase: EnginePhase.composite); expect(renderAnimatedOpacity.needsCompositing, false); }); + + test('RenderAnimatedOpacity reuses its layer', () { + final Animation opacityAnimation = AnimationController( + vsync: _FakeTickerProvider(), + )..value = 0.5; // must not be 0 or 1.0. Otherwise, it won't create a layer + + _testLayerReuse(RenderAnimatedOpacity( + opacity: opacityAnimation, + child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter + )); + }); + + test('RenderShaderMask reuses its layer', () { + _testLayerReuse(RenderShaderMask( + shaderCallback: (Rect rect) { + return ui.Gradient.radial( + rect.center, + rect.shortestSide / 2.0, + const [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(RenderBackdropFilter( + filter: ui.ImageFilter.blur(), + child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter + )); + }); + + test('RenderClipRect reuses its layer', () { + _testLayerReuse(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(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(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(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(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(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(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(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(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 { + @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 { + @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 { @@ -348,3 +532,32 @@ class _FakeTicker implements Ticker { @override 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(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()); + 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 { + @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; +} diff --git a/packages/flutter/test/rendering/recording_canvas.dart b/packages/flutter/test/rendering/recording_canvas.dart index 5011d0c69a..654f6436a8 100644 --- a/packages/flutter/test/rendering/recording_canvas.dart +++ b/packages/flutter/test/rendering/recording_canvas.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/src/rendering/layer.dart'; /// An [Invocation] and the [stack] trace that led to it. /// @@ -103,38 +104,49 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex } @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)); + return null; } @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); clipRRectAndPaint(clipRRect.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset)); + return null; } @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)); + return null; } @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.transform(transform.storage); painter(this, offset); canvas.restore(); + return null; } @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. painter(this, offset); canvas.restore(); + return null; } @override - void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) { + void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, + { Rect childPaintBounds }) { painter(this, offset); } diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index bb9810bf31..a1b9657c7a 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -7,33 +7,111 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.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; 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 _errors = []; + + /// 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 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 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; @override void drawFrame() { assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead'); - pipelineOwner.flushLayout(); - if (phase == EnginePhase.layout) - return; - pipelineOwner.flushCompositingBits(); - if (phase == EnginePhase.compositingBits) - return; - pipelineOwner.flushPaint(); - if (phase == EnginePhase.paint) - return; - renderView.compositeFrame(); - if (phase == EnginePhase.composite) - return; - pipelineOwner.flushSemantics(); - if (phase == EnginePhase.flushSemantics) - return; - assert(phase == EnginePhase.flushSemantics || - phase == EnginePhase.sendSemanticsUpdate); + final FlutterExceptionHandler oldErrorHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + _errors.add(details); + }; + try { + pipelineOwner.flushLayout(); + if (phase == EnginePhase.layout) + return; + pipelineOwner.flushCompositingBits(); + if (phase == EnginePhase.compositingBits) + return; + pipelineOwner.flushPaint(); + if (phase == EnginePhase.paint) + return; + renderView.compositeFrame(); + if (phase == EnginePhase.composite) + return; + pipelineOwner.flushSemantics(); + if (phase == EnginePhase.flushSemantics) + return; + assert(phase == EnginePhase.flushSemantics || + 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 { /// /// The EnginePhase must not be [EnginePhase.build], since the rendering layer /// has no build phase. +/// +/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError]. void layout( RenderBox box, { BoxConstraints constraints, Alignment alignment = Alignment.center, EnginePhase phase = EnginePhase.layout, + VoidCallback onErrors, }) { 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. @@ -76,13 +157,21 @@ void layout( } 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.renderView != null); assert(renderer.renderView.child != null); // call layout() first! + + if (onErrors != null) { + renderer.onErrors = onErrors; + } + renderer.phase = phase; renderer.drawFrame(); } diff --git a/packages/flutter/test/rendering/repaint_boundary_test.dart b/packages/flutter/test/rendering/repaint_boundary_test.dart index 18e86719bd..1b067bf1f4 100644 --- a/packages/flutter/test/rendering/repaint_boundary_test.dart +++ b/packages/flutter/test/rendering/repaint_boundary_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import '../flutter_test_alternative.dart'; @@ -51,4 +52,102 @@ void main() { padding.child = repaintBoundary; 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()); + }); + + 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()); + }); + + 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); + } } diff --git a/packages/flutter/test/widgets/annotated_region_test.dart b/packages/flutter/test/widgets/annotated_region_test.dart index 45cd5c3fd1..7b37167b9c 100644 --- a/packages/flutter/test/widgets/annotated_region_test.dart +++ b/packages/flutter/test/widgets/annotated_region_test.dart @@ -31,12 +31,12 @@ void main() { ), ), ); - int result = RendererBinding.instance.renderView.layer.find(Offset( + int result = RendererBinding.instance.renderView.debugLayer.find(Offset( 10.0 * window.devicePixelRatio, 10.0 * window.devicePixelRatio, )); expect(result, null); - result = RendererBinding.instance.renderView.layer.find(Offset( + result = RendererBinding.instance.renderView.debugLayer.find(Offset( 50.0 * window.devicePixelRatio, 50.0 * window.devicePixelRatio, )); diff --git a/packages/flutter/test/widgets/color_filter_test.dart b/packages/flutter/test/widgets/color_filter_test.dart index 469c508c89..9b8b273499 100644 --- a/packages/flutter/test/widgets/color_filter_test.dart +++ b/packages/flutter/test/widgets/color_filter_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; void main() { testWidgets('Color filter - red', (WidgetTester tester) async { @@ -64,4 +65,26 @@ void main() { ), ); }); -} \ No newline at end of file + + testWidgets('Color filter - reuses its layer', (WidgetTester tester) async { + Future 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)); + }); +} diff --git a/packages/flutter/test/widgets/opacity_test.dart b/packages/flutter/test/widgets/opacity_test.dart index 1d173b3c42..53060b404b 100644 --- a/packages/flutter/test/widgets/opacity_test.dart +++ b/packages/flutter/test/widgets/opacity_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import '../rendering/mock_canvas.dart'; import 'semantics_tester.dart'; @@ -191,6 +192,7 @@ void main() { final Element element = find.byType(RepaintBoundary).first.evaluate().single; // The following line will send the layer to engine and cause crash if an // 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); } diff --git a/packages/flutter_test/lib/src/accessibility.dart b/packages/flutter_test/lib/src/accessibility.dart index 29512534c6..fe2373186b 100644 --- a/packages/flutter_test/lib/src/accessibility.dart +++ b/packages/flutter_test/lib/src/accessibility.dart @@ -196,7 +196,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { Future evaluate(WidgetTester tester) async { final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; final RenderView renderView = tester.binding.renderView; - final OffsetLayer layer = renderView.layer; + final OffsetLayer layer = renderView.debugLayer; ui.Image image; final ByteData byteData = await tester.binding.runAsync(() async { // Needs to be the same pixel ratio otherwise our dimensions won't match the diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index b5a36231d7..17ae5109ff 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -231,7 +231,7 @@ abstract class WidgetController { } /// Returns a list of all the [Layer] objects in the rendering. - List get layers => _walkLayers(binding.renderView.layer).toList(); + List get layers => _walkLayers(binding.renderView.debugLayer).toList(); Iterable _walkLayers(Layer layer) sync* { TestAsyncUtils.guardSync(); yield layer; diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 8e3dca3d7f..16d147c59a 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -1620,7 +1620,7 @@ Future _captureImage(Element element) { assert(renderObject != null); } assert(!renderObject.debugNeedsPaint); - final OffsetLayer layer = renderObject.layer; + final OffsetLayer layer = renderObject.debugLayer; return layer.toImage(renderObject.paintBounds); }