Unverified Commit a2acc6a3 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Revert "Revert "Enable taking screenshots of arbitrary RenderObjects from a...

Revert "Revert "Enable taking screenshots of arbitrary RenderObjects from a running a… (#20637)" (#21395)" (#21448)

This reverts commit 5b5a5b82.
parent de9f775f
...@@ -50,6 +50,8 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin { ...@@ -50,6 +50,8 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
Layer _previousSibling; Layer _previousSibling;
/// Removes this layer from its parent layer's child list. /// Removes this layer from its parent layer's child list.
///
/// This has no effect if the layer's parent is already null.
@mustCallSuper @mustCallSuper
void remove() { void remove() {
parent?._removeChild(this); parent?._removeChild(this);
...@@ -588,7 +590,11 @@ class OffsetLayer extends ContainerLayer { ...@@ -588,7 +590,11 @@ class OffsetLayer extends ContainerLayer {
assert(bounds != null); assert(bounds != null);
assert(pixelRatio != null); assert(pixelRatio != null);
final ui.SceneBuilder builder = new ui.SceneBuilder(); final ui.SceneBuilder builder = new ui.SceneBuilder();
final Matrix4 transform = new Matrix4.translationValues(bounds.left - offset.dx, bounds.top - offset.dy, 0.0); final Matrix4 transform = new Matrix4.translationValues(
(-bounds.left - offset.dx) * pixelRatio,
(-bounds.top - offset.dy) * pixelRatio,
0.0,
);
transform.scale(pixelRatio, pixelRatio); transform.scale(pixelRatio, pixelRatio);
builder.pushTransform(transform.storage); builder.pushTransform(transform.storage);
addToScene(builder, Offset.zero); addToScene(builder, Offset.zero);
......
...@@ -60,7 +60,13 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset); ...@@ -60,7 +60,13 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset);
/// New [PaintingContext] objects are created automatically when using /// New [PaintingContext] objects are created automatically when using
/// [PaintingContext.repaintCompositedChild] and [pushLayer]. /// [PaintingContext.repaintCompositedChild] and [pushLayer].
class PaintingContext extends ClipContext { class PaintingContext extends ClipContext {
PaintingContext._(this._containerLayer, this.estimatedBounds)
/// Creates a painting context.
///
/// Typically only called by [PaintingContext.repaintCompositedChild]
/// and [pushLayer].
@protected
PaintingContext(this._containerLayer, this.estimatedBounds)
: assert(_containerLayer != null), : assert(_containerLayer != null),
assert(estimatedBounds != null); assert(estimatedBounds != null);
...@@ -86,8 +92,19 @@ class PaintingContext extends ClipContext { ...@@ -86,8 +92,19 @@ class PaintingContext extends ClipContext {
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject] /// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
/// has a composited layer. /// has a composited layer.
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) { static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
assert(child.isRepaintBoundary);
assert(child._needsPaint); assert(child._needsPaint);
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
assert(child.isRepaintBoundary);
assert(() { assert(() {
// register the call for RepaintBoundary metrics // register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint( child.debugRegisterRepaintBoundaryPaint(
...@@ -107,9 +124,32 @@ class PaintingContext extends ClipContext { ...@@ -107,9 +124,32 @@ class PaintingContext extends ClipContext {
child._layer.debugCreator = child.debugCreator ?? child.runtimeType; child._layer.debugCreator = child.debugCreator ?? child.runtimeType;
return true; return true;
}()); }());
final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds); childContext ??= new PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero); child._paintWithContext(childContext, Offset.zero);
childContext._stopRecordingIfNeeded(); childContext.stopRecordingIfNeeded();
}
/// In debug mode, repaint the given render object using a custom painting
/// context that can record the results of the painting operation in addition
/// to performing the regular paint of the child.
///
/// See also:
///
/// * [repaintCompositedChild], for repainting a composited child without
/// instrumentation.
static void debugInstrumentRepaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
@required PaintingContext customContext,
}) {
assert(() {
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
childContext: customContext,
);
return true;
}());
} }
/// Paint a child [RenderObject]. /// Paint a child [RenderObject].
...@@ -125,7 +165,7 @@ class PaintingContext extends ClipContext { ...@@ -125,7 +165,7 @@ class PaintingContext extends ClipContext {
}()); }());
if (child.isRepaintBoundary) { if (child.isRepaintBoundary) {
_stopRecordingIfNeeded(); stopRecordingIfNeeded();
_compositeChild(child, offset); _compositeChild(child, offset);
} else { } else {
child._paintWithContext(this, offset); child._paintWithContext(this, offset);
...@@ -159,10 +199,20 @@ class PaintingContext extends ClipContext { ...@@ -159,10 +199,20 @@ class PaintingContext extends ClipContext {
}()); }());
} }
child._layer.offset = offset; child._layer.offset = offset;
_appendLayer(child._layer); appendLayer(child._layer);
} }
void _appendLayer(Layer layer) { /// Adds a layer to the recording requiring that the recording is already
/// stopped.
///
/// Do not call this function directly: call [addLayer] or [pushLayer]
/// instead. This function is called internally when all layers not
/// generated from the [canvas] are added.
///
/// Subclasses that need to customize how layers are added should override
/// this method.
@protected
void appendLayer(Layer layer) {
assert(!_isRecording); assert(!_isRecording);
layer.remove(); layer.remove();
_containerLayer.append(layer); _containerLayer.append(layer);
...@@ -210,7 +260,19 @@ class PaintingContext extends ClipContext { ...@@ -210,7 +260,19 @@ class PaintingContext extends ClipContext {
_containerLayer.append(_currentLayer); _containerLayer.append(_currentLayer);
} }
void _stopRecordingIfNeeded() { /// Stop recording to a canvas if recording has started.
///
/// Do not call this function directly: functions in this class will call
/// this method as needed. This function is called internally to ensure that
/// recording is stopped before adding layers or finalizing the results of a
/// paint.
///
/// Subclasses that need to customize how recording to a canvas is performed
/// should override this method to save the results of the custom canvas
/// recordings.
@protected
@mustCallSuper
void stopRecordingIfNeeded() {
if (!_isRecording) if (!_isRecording)
return; return;
assert(() { assert(() {
...@@ -271,8 +333,8 @@ class PaintingContext extends ClipContext { ...@@ -271,8 +333,8 @@ class PaintingContext extends ClipContext {
/// * [pushLayer], for adding a layer and using its canvas to paint with that /// * [pushLayer], for adding a layer and using its canvas to paint with that
/// layer. /// layer.
void addLayer(Layer layer) { void addLayer(Layer layer) {
_stopRecordingIfNeeded(); stopRecordingIfNeeded();
_appendLayer(layer); appendLayer(layer);
} }
/// Appends the given layer to the recording, and calls the `painter` callback /// Appends the given layer to the recording, and calls the `painter` callback
...@@ -295,15 +357,21 @@ class PaintingContext extends ClipContext { ...@@ -295,15 +357,21 @@ class PaintingContext extends ClipContext {
/// See also: /// See also:
/// ///
/// * [addLayer], for pushing a leaf layer whose canvas is not used. /// * [addLayer], for pushing a leaf layer whose canvas is not used.
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) { void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
assert(!childLayer.attached); assert(!childLayer.attached);
assert(childLayer.parent == null); assert(childLayer.parent == null);
assert(painter != null); assert(painter != null);
_stopRecordingIfNeeded(); stopRecordingIfNeeded();
_appendLayer(childLayer); appendLayer(childLayer);
final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? estimatedBounds); final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset); painter(childContext, offset);
childContext._stopRecordingIfNeeded(); childContext.stopRecordingIfNeeded();
}
/// Creates a compatible painting context to paint onto [childLayer].
@protected
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
return new PaintingContext(childLayer, bounds);
} }
/// Clip further painting using a rectangle. /// Clip further painting using a rectangle.
...@@ -2036,7 +2104,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2036,7 +2104,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
Rect get paintBounds; Rect get paintBounds;
/// Override this method to paint debugging information. /// Override this method to paint debugging information.
@protected
void debugPaint(PaintingContext context, Offset offset) { } void debugPaint(PaintingContext context, Offset offset) { }
/// Paint this render object into the given context at the given offset. /// Paint this render object into the given context at the given offset.
......
...@@ -7,13 +7,26 @@ import 'dart:collection'; ...@@ -7,13 +7,26 @@ import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui show window, Picture, SceneBuilder, PictureRecorder; import 'dart:typed_data';
import 'dart:ui' show Offset; import 'dart:ui' as ui
show
window,
ClipOp,
Image,
ImageByteFormat,
Paragraph,
Picture,
PictureRecorder,
PointMode,
SceneBuilder,
Vertices;
import 'dart:ui' show Canvas, Offset;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:vector_math/vector_math_64.dart';
import 'app.dart'; import 'app.dart';
import 'basic.dart'; import 'basic.dart';
...@@ -31,6 +44,556 @@ typedef void _RegisterServiceExtensionCallback({ ...@@ -31,6 +44,556 @@ typedef void _RegisterServiceExtensionCallback({
@required ServiceExtensionCallback callback @required ServiceExtensionCallback callback
}); });
/// A layer that mimics the behavior of another layer.
///
/// A proxy layer is used for cases where a layer needs to be placed into
/// multiple trees of layers.
class _ProxyLayer extends Layer {
final Layer _layer;
_ProxyLayer(this._layer);
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
_layer.addToScene(builder, layerOffset);
}
@override
S find<S>(Offset regionOffset) => _layer.find(regionOffset);
}
/// A [Canvas] that multicasts all method calls to a main canvas and a
/// secondary screenshot canvas so that a screenshot can be recorded at the same
/// time as performing a normal paint.
class _MulticastCanvas implements Canvas {
final Canvas _main;
final Canvas _screenshot;
_MulticastCanvas({
@required Canvas main,
@required Canvas screenshot,
}) : assert(main != null),
assert(screenshot != null),
_main = main,
_screenshot = screenshot;
@override
void clipPath(Path path, {bool doAntiAlias = true}) {
_main.clipPath(path, doAntiAlias: doAntiAlias);
_screenshot.clipPath(path, doAntiAlias: doAntiAlias);
}
@override
void clipRRect(RRect rrect, {bool doAntiAlias = true}) {
_main.clipRRect(rrect, doAntiAlias: doAntiAlias);
_screenshot.clipRRect(rrect, doAntiAlias: doAntiAlias);
}
@override
void clipRect(Rect rect, {ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true}) {
_main.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
_screenshot.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
}
@override
void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint) {
_main.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
_screenshot.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
}
@override
void drawAtlas(ui.Image atlas, List<RSTransform> transforms, List<Rect> rects, List<Color> colors, BlendMode blendMode, Rect cullRect, Paint paint) {
_main.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
_screenshot.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
}
@override
void drawCircle(Offset c, double radius, Paint paint) {
_main.drawCircle(c, radius, paint);
_screenshot.drawCircle(c, radius, paint);
}
@override
void drawColor(Color color, BlendMode blendMode) {
_main.drawColor(color, blendMode);
_screenshot.drawColor(color, blendMode);
}
@override
void drawDRRect(RRect outer, RRect inner, Paint paint) {
_main.drawDRRect(outer, inner, paint);
_screenshot.drawDRRect(outer, inner, paint);
}
@override
void drawImage(ui.Image image, Offset p, Paint paint) {
_main.drawImage(image, p, paint);
_screenshot.drawImage(image, p, paint);
}
@override
void drawImageNine(ui.Image image, Rect center, Rect dst, Paint paint) {
_main.drawImageNine(image, center, dst, paint);
_screenshot.drawImageNine(image, center, dst, paint);
}
@override
void drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint) {
_main.drawImageRect(image, src, dst, paint);
_screenshot.drawImageRect(image, src, dst, paint);
}
@override
void drawLine(Offset p1, Offset p2, Paint paint) {
_main.drawLine(p1, p2, paint);
_screenshot.drawLine(p1, p2, paint);
}
@override
void drawOval(Rect rect, Paint paint) {
_main.drawOval(rect, paint);
_screenshot.drawOval(rect, paint);
}
@override
void drawPaint(Paint paint) {
_main.drawPaint(paint);
_screenshot.drawPaint(paint);
}
@override
void drawParagraph(ui.Paragraph paragraph, Offset offset) {
_main.drawParagraph(paragraph, offset);
_screenshot.drawParagraph(paragraph, offset);
}
@override
void drawPath(Path path, Paint paint) {
_main.drawPath(path, paint);
_screenshot.drawPath(path, paint);
}
@override
void drawPicture(ui.Picture picture) {
_main.drawPicture(picture);
_screenshot.drawPicture(picture);
}
@override
void drawPoints(ui.PointMode pointMode, List<Offset> points, Paint paint) {
_main.drawPoints(pointMode, points, paint);
_screenshot.drawPoints(pointMode, points, paint);
}
@override
void drawRRect(RRect rrect, Paint paint) {
_main.drawRRect(rrect, paint);
_screenshot.drawRRect(rrect, paint);
}
@override
void drawRawAtlas(ui.Image atlas, Float32List rstTransforms, Float32List rects, Int32List colors, BlendMode blendMode, Rect cullRect, Paint paint) {
_main.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
_screenshot.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
}
@override
void drawRawPoints(ui.PointMode pointMode, Float32List points, Paint paint) {
_main.drawRawPoints(pointMode, points, paint);
_screenshot.drawRawPoints(pointMode, points, paint);
}
@override
void drawRect(Rect rect, Paint paint) {
_main.drawRect(rect, paint);
_screenshot.drawRect(rect, paint);
}
@override
void drawShadow(Path path, Color color, double elevation, bool transparentOccluder) {
_main.drawShadow(path, color, elevation, transparentOccluder);
_screenshot.drawShadow(path, color, elevation, transparentOccluder);
}
@override
void drawVertices(ui.Vertices vertices, BlendMode blendMode, Paint paint) {
_main.drawVertices(vertices, blendMode, paint);
_screenshot.drawVertices(vertices, blendMode, paint);
}
@override
int getSaveCount() {
// The main canvas is used instead of the screenshot canvas as the main
// canvas is guaranteed to be consistent with the canvas expected by the
// normal paint pipeline so any logic depending on getSaveCount() will
// behave the same as for the regular paint pipeline.
return _main.getSaveCount();
}
@override
void restore() {
_main.restore();
_screenshot.restore();
}
@override
void rotate(double radians) {
_main.rotate(radians);
_screenshot.rotate(radians);
}
@override
void save() {
_main.save();
_screenshot.save();
}
@override
void saveLayer(Rect bounds, Paint paint) {
_main.saveLayer(bounds, paint);
_screenshot.saveLayer(bounds, paint);
}
@override
void scale(double sx, [double sy]) {
_main.scale(sx, sy);
_screenshot.scale(sx, sy);
}
@override
void skew(double sx, double sy) {
_main.skew(sx, sy);
_screenshot.skew(sx, sy);
}
@override
void transform(Float64List matrix4) {
_main.transform(matrix4);
_screenshot.transform(matrix4);
}
@override
void translate(double dx, double dy) {
_main.translate(dx, dy);
_screenshot.translate(dx, dy);
}
}
Rect _calculateSubtreeBoundsHelper(RenderObject object, Matrix4 transform) {
Rect bounds = MatrixUtils.transformRect(transform, object.semanticBounds);
object.visitChildren((RenderObject child) {
final Matrix4 childTransform = transform.clone();
object.applyPaintTransform(child, childTransform);
Rect childBounds = _calculateSubtreeBoundsHelper(child, childTransform);
final Rect paintClip = object.describeApproximatePaintClip(child);
if (paintClip != null) {
final Rect transformedPaintClip = MatrixUtils.transformRect(
transform,
paintClip,
);
childBounds = childBounds.intersect(transformedPaintClip);
}
if (childBounds.isFinite && !childBounds.isEmpty) {
bounds = bounds.isEmpty ? childBounds : bounds.expandToInclude(childBounds);
}
});
return bounds;
}
/// Calculate bounds for a render object and all of its descendants.
Rect _calculateSubtreeBounds(RenderObject object) {
return _calculateSubtreeBoundsHelper(object, new Matrix4.identity());
}
/// A layer that omits its own offset when adding children to the scene so that
/// screenshots render to the scene in the local coordinate system of the layer.
class _ScreenshotContainerLayer extends OffsetLayer {
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
addChildrenToScene(builder, layerOffset);
}
}
/// Data shared between nested [_ScreenshotPaintingContext] objects recording
/// a screenshot.
class _ScreenshotData {
_ScreenshotData({
@required this.target,
}) : assert(target != null),
containerLayer = new _ScreenshotContainerLayer();
/// Target to take a screenshot of.
final RenderObject target;
/// Root of the layer tree containing the screenshot.
final OffsetLayer containerLayer;
/// Whether the screenshot target has already been found in the render tree.
bool foundTarget = false;
/// Whether paint operations should record to the screenshot.
///
/// At least one of [includeInScreenshot] and [includeInRegularContext] must
/// be true.
bool includeInScreenshot = false;
/// Whether paint operations should record to the regular context.
///
/// This should only be set to false before paint operations that should only
/// apply to the screenshot such rendering debug information about the
/// [target].
///
/// At least one of [includeInScreenshot] and [includeInRegularContext] must
/// be true.
bool includeInRegularContext = true;
/// Offset of the screenshot corresponding to the offset [target] was given as
/// part of the regular paint.
Offset get screenshotOffset {
assert(foundTarget);
return containerLayer.offset;
}
set screenshotOffset(Offset offset) {
containerLayer.offset = offset;
}
}
/// A place to paint to build screenshots of [RenderObject]s.
///
/// Requires that the render objects have already painted successfully as part
/// of the regular rendering pipeline.
/// This painting context behaves the same as standard [PaintingContext] with
/// instrumentation added to compute a screenshot of a specified [RenderObject]
/// added. To correctly mimic the behavor of the regular rendering pipeline, the
/// full subtree of the first [RepaintBoundary] ancestor of the specified
/// [RenderObject] will also be rendered rather than just the subtree of the
/// render object.
class _ScreenshotPaintingContext extends PaintingContext {
_ScreenshotPaintingContext({
@required ContainerLayer containerLayer,
@required Rect estimatedBounds,
@required _ScreenshotData screenshotData,
}) : _data = screenshotData,
super(containerLayer, estimatedBounds);
final _ScreenshotData _data;
// Recording state
PictureLayer _screenshotCurrentLayer;
ui.PictureRecorder _screenshotRecorder;
Canvas _screenshotCanvas;
_MulticastCanvas _multicastCanvas;
@override
Canvas get canvas {
if (_data.includeInScreenshot) {
if (_screenshotCanvas == null) {
_startRecordingScreenshot();
}
assert(_screenshotCanvas != null);
return _data.includeInRegularContext ? _multicastCanvas : _screenshotCanvas;
} else {
assert(_data.includeInRegularContext);
return super.canvas;
}
}
bool get _isScreenshotRecording {
final bool hasScreenshotCanvas = _screenshotCanvas != null;
assert(() {
if (hasScreenshotCanvas) {
assert(_screenshotCurrentLayer != null);
assert(_screenshotRecorder != null);
assert(_screenshotCanvas != null);
} else {
assert(_screenshotCurrentLayer == null);
assert(_screenshotRecorder == null);
assert(_screenshotCanvas == null);
}
return true;
}());
return hasScreenshotCanvas;
}
void _startRecordingScreenshot() {
assert(_data.includeInScreenshot);
assert(!_isScreenshotRecording);
_screenshotCurrentLayer = new PictureLayer(estimatedBounds);
_screenshotRecorder = new ui.PictureRecorder();
_screenshotCanvas = new Canvas(_screenshotRecorder);
_data.containerLayer.append(_screenshotCurrentLayer);
if (_data.includeInRegularContext) {
_multicastCanvas = new _MulticastCanvas(
main: super.canvas,
screenshot: _screenshotCanvas,
);
} else {
_multicastCanvas = null;
}
}
@override
void stopRecordingIfNeeded() {
super.stopRecordingIfNeeded();
_stopRecordingScreenshotIfNeeded();
}
void _stopRecordingScreenshotIfNeeded() {
if (!_isScreenshotRecording)
return;
// There is no need to ever draw repaint rainbows as part of the screenshot.
_screenshotCurrentLayer.picture = _screenshotRecorder.endRecording();
_screenshotCurrentLayer = null;
_screenshotRecorder = null;
_multicastCanvas = null;
_screenshotCanvas = null;
}
@override
void appendLayer(Layer layer) {
if (_data.includeInRegularContext) {
super.appendLayer(layer);
if (_data.includeInScreenshot) {
assert(!_isScreenshotRecording);
// We must use a proxy layer here as the layer is already attached to
// the regular layer tree.
_data.containerLayer.append(new _ProxyLayer(layer));
}
} else {
// Only record to the screenshot.
assert(!_isScreenshotRecording);
assert(_data.includeInScreenshot);
layer.remove();
_data.containerLayer.append(layer);
return;
}
}
@override
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
if (_data.foundTarget) {
// We have already found the screenshotTarget in the layer tree
// so we can optimize and use a standard PaintingContext.
return super.createChildContext(childLayer, bounds);
} else {
return new _ScreenshotPaintingContext(
containerLayer: childLayer,
estimatedBounds: bounds,
screenshotData: _data,
);
}
}
@override
void paintChild(RenderObject child, Offset offset) {
final bool isScreenshotTarget = identical(child, _data.target);
if (isScreenshotTarget) {
assert(!_data.includeInScreenshot);
assert(!_data.foundTarget);
_data.foundTarget = true;
_data.screenshotOffset = offset;
_data.includeInScreenshot = true;
}
super.paintChild(child, offset);
if (isScreenshotTarget) {
_stopRecordingScreenshotIfNeeded();
_data.includeInScreenshot = false;
}
}
/// Captures an image of the current state of [renderObject] and its children.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset
/// by the top-left corner of [renderBounds], and have dimensions equal to the
/// size of [renderBounds] multiplied by [pixelRatio].
///
/// To use [toImage], the render object must have gone through the paint phase
/// (i.e. [debugNeedsPaint] must be false).
///
/// The [pixelRatio] describes the scale between the logical pixels and the
/// size of the output image. It is independent of the
/// [window.devicePixelRatio] for the device, so specifying 1.0 (the default)
/// will give you a 1:1 mapping between logical pixels and the output pixels
// / in the image.
///
/// The [debugPaint] argument specifies whether the image should include the
/// output of [RenderObject.debugPaint] for [renderObject] with
/// [debugPaintSizeEnabled] set to `true`. Debug paint information is not
/// included for the children of [renderObject] so that it is clear precisely
/// which object the debug paint information references.
///
/// See also:
///
/// * [RenderRepaintBoundary.toImage] for a similar API for [RenderObject]s
/// that are repaint boundaries that can be used outside of the inspector.
/// * [OffsetLayer.toImage] for a similar API at the layer level.
/// * [dart:ui.Scene.toImage] for more information about the image returned.
static Future<ui.Image> toImage(
RenderObject renderObject,
Rect renderBounds, {
double pixelRatio = 1.0,
bool debugPaint = false,
}) {
RenderObject repaintBoundary = renderObject;
while (repaintBoundary != null && !repaintBoundary.isRepaintBoundary) {
repaintBoundary = repaintBoundary.parent;
}
assert(repaintBoundary != null);
final _ScreenshotData data = new _ScreenshotData(target: renderObject);
final _ScreenshotPaintingContext context = new _ScreenshotPaintingContext(
containerLayer: repaintBoundary.debugLayer,
estimatedBounds: repaintBoundary.paintBounds,
screenshotData: data,
);
if (identical(renderObject, repaintBoundary)) {
// 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(new _ProxyLayer(repaintBoundary.layer));
data.foundTarget = true;
data.screenshotOffset = repaintBoundary.layer.offset;
} else {
// Repaint everything under the repaint boundary.
// We call debugInstrumentRepaintCompositedChild instead of paintChild as
// we need to force everything under the repaint boundary to repaint.
PaintingContext.debugInstrumentRepaintCompositedChild(
repaintBoundary,
customContext: context,
);
}
// The check that debugPaintSizeEnabled is false exists to ensure we only
// call debugPaint when it wasn't already called.
if (debugPaint && !debugPaintSizeEnabled) {
data.includeInRegularContext = false;
// Existing recording may be to a canvas that draws to both the normal and
// screenshot canvases.
context.stopRecordingIfNeeded();
assert(data.foundTarget);
data.includeInScreenshot = true;
debugPaintSizeEnabled = true;
try {
renderObject.debugPaint(context, data.screenshotOffset);
} finally {
debugPaintSizeEnabled = false;
context.stopRecordingIfNeeded();
}
}
// 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.addToScene(new ui.SceneBuilder(), Offset.zero);
return data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio);
}
}
/// A class describing a step along a path through a tree of [DiagnosticsNode] /// A class describing a step along a path through a tree of [DiagnosticsNode]
/// objects. /// objects.
/// ///
...@@ -464,6 +1027,33 @@ class WidgetInspectorService { ...@@ -464,6 +1027,33 @@ class WidgetInspectorService {
name: 'isWidgetCreationTracked', name: 'isWidgetCreationTracked',
callback: isWidgetCreationTracked, callback: isWidgetCreationTracked,
); );
registerServiceExtension(
name: 'screenshot',
callback: (Map<String, String> parameters) async {
assert(parameters.containsKey('id'));
assert(parameters.containsKey('width'));
assert(parameters.containsKey('height'));
final ui.Image image = await screenshot(
toObject(parameters['id']),
width: double.parse(parameters['width']),
height: double.parse(parameters['height']),
margin: parameters.containsKey('margin') ?
double.parse(parameters['margin']) : 0.0,
maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
double.parse(parameters['maxPixelRatio']) : 1.0,
debugPaint: parameters['debugPaint'] == 'true',
);
if (image == null) {
return <String, Object>{'result': null};
}
final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
return <String, Object>{
'result': base64.encoder.convert(new Uint8List.view(byteData.buffer)),
};
},
);
} }
/// Clear all InspectorService object references. /// Clear all InspectorService object references.
...@@ -1036,6 +1626,77 @@ class WidgetInspectorService { ...@@ -1036,6 +1626,77 @@ class WidgetInspectorService {
return _safeJsonEncode(_getSelectedWidget(previousSelectionId, groupName)); return _safeJsonEncode(_getSelectedWidget(previousSelectionId, groupName));
} }
/// Captures an image of the current state of an [object] that is a
/// [RenderObject] or [Element].
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes and will be scaled
/// to be at most [width] pixels wide and [height] pixels tall. The returned
/// image will never have a scale between logical pixels and the
/// size of the output image larger than maxPixelRatio.
/// [margin] indicates the number of pixels relative to the unscaled size of
/// the [object] to include as a margin to include around the bounds of the
/// [object] in the screenshot. Including a margin can be useful to capture
/// areas that are slightly outside of the normal bounds of an object such as
/// some debug paint information.
@protected
Future<ui.Image> screenshot(
Object object, {
@required double width,
@required double height,
double margin = 0.0,
double maxPixelRatio = 1.0,
bool debugPaint = false,
}) async {
if (object is! Element && object is! RenderObject) {
return null;
}
final RenderObject renderObject = object is Element ? object.renderObject : object;
if (renderObject == null || !renderObject.attached) {
return null;
}
if (renderObject.debugNeedsLayout) {
final PipelineOwner owner = renderObject.owner;
assert(owner != null);
assert(!owner.debugDoingLayout);
owner
..flushLayout()
..flushCompositingBits()
..flushPaint();
// If we still need layout, then that means that renderObject was skipped
// in the layout phase and therefore can't be painted. It is clearer to
// return null indicating that a screenshot is unavailable than to return
// an empty image.
if (renderObject.debugNeedsLayout) {
return null;
}
}
Rect renderBounds = _calculateSubtreeBounds(renderObject);
if (margin != 0.0) {
renderBounds = renderBounds.inflate(margin);
}
if (renderBounds.isEmpty) {
return null;
}
final double pixelRatio = math.min(
maxPixelRatio,
math.min(
width / renderBounds.width,
height / renderBounds.height,
),
);
return _ScreenshotPaintingContext.toImage(
renderObject,
renderBounds,
pixelRatio: pixelRatio,
debugPaint: debugPaint,
);
}
Map<String, Object> _getSelectedWidget(String previousSelectionId, String groupName) { Map<String, Object> _getSelectedWidget(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId); final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final Element current = selection?.currentElement; final Element current = selection?.currentElement;
......
...@@ -512,7 +512,7 @@ void main() { ...@@ -512,7 +512,7 @@ void main() {
// If you add a service extension... TEST IT! :-) // If you add a service extension... TEST IT! :-)
// ...then increment this number. // ...then increment this number.
expect(binding.extensions.length, 37); expect(binding.extensions.length, 38);
expect(console, isEmpty); expect(console, isEmpty);
debugPrint = debugPrintThrottled; debugPrint = debugPrintThrottled;
......
...@@ -185,12 +185,43 @@ void main() { ...@@ -185,12 +185,43 @@ void main() {
image = await boundary.toImage(); image = await boundary.toImage();
expect(image.width, equals(20)); expect(image.width, equals(20));
expect(image.height, equals(20)); expect(image.height, equals(20));
final ByteData data = await image.toByteData(); ByteData data = await image.toByteData();
int getPixel(int x, int y) => data.getUint32((x + y * image.width) * 4);
expect(data.lengthInBytes, equals(20 * 20 * 4)); expect(data.lengthInBytes, equals(20 * 20 * 4));
expect(data.elementSizeInBytes, equals(1)); expect(data.elementSizeInBytes, equals(1));
const int stride = 20 * 4; expect(getPixel(0, 0), equals(0x00000080));
expect(data.getUint32(0), equals(0x00000080)); expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
expect(data.getUint32(stride - 4), equals(0xffffffff));
final OffsetLayer layer = boundary.layer;
image = await layer.toImage(Offset.zero & const Size(20.0, 20.0));
expect(image.width, equals(20));
expect(image.height, equals(20));
data = await image.toByteData();
expect(getPixel(0, 0), equals(0x00000080));
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
// non-zero offsets.
image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0));
expect(image.width, equals(30));
expect(image.height, equals(30));
data = await image.toByteData();
expect(getPixel(0, 0), equals(0x00000000));
expect(getPixel(10, 10), equals(0x00000080));
expect(getPixel(image.width - 1, 0), equals(0x00000000));
expect(getPixel(image.width - 1, 10), equals(0xffffffff));
// offset combined with a custom pixel ratio.
image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0), pixelRatio: 2.0);
expect(image.width, equals(60));
expect(image.height, equals(60));
data = await image.toByteData();
expect(getPixel(0, 0), equals(0x00000000));
expect(getPixel(20, 20), equals(0x00000080));
expect(getPixel(image.width - 1, 0), equals(0x00000000));
expect(getPixel(image.width - 1, 20), equals(0xffffffff));
}); });
test('RenderOpacity does not composite if it is transparent', () { test('RenderOpacity does not composite if it is transparent', () {
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:ui' as ui show PictureRecorder;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -12,6 +14,80 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -12,6 +14,80 @@ import 'package:flutter_test/flutter_test.dart';
typedef FutureOr<Map<String, Object>> InspectorServiceExtensionCallback(Map<String, String> parameters); typedef FutureOr<Map<String, Object>> InspectorServiceExtensionCallback(Map<String, String> parameters);
class RenderRepaintBoundaryWithDebugPaint extends RenderRepaintBoundary {
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
assert(() {
// Draw some debug paint UI interleaving creating layers and drawing
// directly to the context's canvas.
final Paint paint = new Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = Colors.red;
{
final PictureLayer pictureLayer = new PictureLayer(Offset.zero & size);
final ui.PictureRecorder recorder = new ui.PictureRecorder();
final Canvas pictureCanvas = new Canvas(recorder);
pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
pictureLayer.picture = recorder.endRecording();
context.addLayer(
new OffsetLayer()
..offset = offset
..append(pictureLayer),
);
}
context.canvas.drawLine(
offset,
offset.translate(size.width, size.height),
paint,
);
{
final PictureLayer pictureLayer = new PictureLayer(Offset.zero & size);
final ui.PictureRecorder recorder = new ui.PictureRecorder();
final Canvas pictureCanvas = new Canvas(recorder);
pictureCanvas.drawCircle(const Offset(20.0, 20.0), 20.0, paint);
pictureLayer.picture = recorder.endRecording();
context.addLayer(
new OffsetLayer()
..offset = offset
..append(pictureLayer),
);
}
paint.color = Colors.blue;
context.canvas.drawLine(
offset,
offset.translate(size.width * 0.5, size.height * 0.5),
paint,
);
return true;
}());
}
}
class RepaintBoundaryWithDebugPaint extends RepaintBoundary {
/// Creates a widget that isolates repaints.
const RepaintBoundaryWithDebugPaint({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderRepaintBoundary createRenderObject(BuildContext context) {
return new RenderRepaintBoundaryWithDebugPaint();
}
}
int getChildLayerCount(OffsetLayer layer) {
Layer child = layer.firstChild;
int count = 0;
while (child != null) {
count++;
child = child.nextSibling;
}
return count;
}
void main() { void main() {
TestWidgetInspectorService.runTests(); TestWidgetInspectorService.runTests();
} }
...@@ -1239,5 +1315,483 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService { ...@@ -1239,5 +1315,483 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
expect(service.rebuildCount, equals(2)); expect(service.rebuildCount, equals(2));
expect(WidgetsApp.debugShowWidgetInspectorOverride, isFalse); expect(WidgetsApp.debugShowWidgetInspectorOverride, isFalse);
}); });
testWidgets('ext.flutter.inspector.screenshot',
(WidgetTester tester) async {
final GlobalKey outerContainerKey = new GlobalKey();
final GlobalKey paddingKey = new GlobalKey();
final GlobalKey redContainerKey = new GlobalKey();
final GlobalKey whiteContainerKey = new GlobalKey();
final GlobalKey sizedBoxKey = new GlobalKey();
// Complex widget tree intended to exercise features such as children
// with rotational transforms and clipping without introducing platform
// specific behavior as text rendering would.
await tester.pumpWidget(
new Center(
child: new RepaintBoundaryWithDebugPaint(
child: new Container(
key: outerContainerKey,
color: Colors.white,
child: new Padding(
key: paddingKey,
padding: const EdgeInsets.all(100.0),
child: new SizedBox(
key: sizedBoxKey,
height: 100.0,
width: 100.0,
child: new Transform.rotate(
angle: 1.0, // radians
child: new ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.elliptical(10.0, 20.0),
topRight: Radius.elliptical(5.0, 30.0),
bottomLeft: Radius.elliptical(2.5, 12.0),
bottomRight: Radius.elliptical(15.0, 6.0),
),
child: new Container(
key: redContainerKey,
color: Colors.red,
child: new Container(
key: whiteContainerKey,
color: Colors.white,
child: new RepaintBoundary(
child: new Center(
child: new Container(
color: Colors.black,
height: 10.0,
width: 10.0,
),
),
),
),
),
),
),
),
),
),
),
),
);
final Element repaintBoundary =
find.byType(RepaintBoundaryWithDebugPaint).evaluate().single;
final RenderRepaintBoundary renderObject = repaintBoundary.renderObject;
final OffsetLayer layer = renderObject.debugLayer;
final int expectedChildLayerCount = getChildLayerCount(layer);
expect(expectedChildLayerCount, equals(2));
await expectLater(
layer.toImage(renderObject.semanticBounds.inflate(50.0)),
matchesGoldenFile('inspector.repaint_boundary_margin.png'),
);
// Regression test for how rendering with a pixel scale other than 1.0
// was handled.
await expectLater(
layer.toImage(
renderObject.semanticBounds.inflate(50.0),
pixelRatio: 0.5,
),
matchesGoldenFile('inspector.repaint_boundary_margin_small.png'),
);
await expectLater(
layer.toImage(
renderObject.semanticBounds.inflate(50.0),
pixelRatio: 2.0,
),
matchesGoldenFile('inspector.repaint_boundary_margin_large.png'),
);
final Layer layerParent = layer.parent;
final Layer firstChild = layer.firstChild;
expect(layerParent, isNotNull);
expect(firstChild, isNotNull);
await expectLater(
service.screenshot(
repaintBoundary,
width: 300.0,
height: 300.0,
),
matchesGoldenFile('inspector.repaint_boundary.png'),
);
// Verify that taking a screenshot didn't change the layers associated with
// the renderObject.
expect(renderObject.debugLayer, equals(layer));
// Verify that taking a screenshot did not change the number of children
// of the layer.
expect(getChildLayerCount(layer), equals(expectedChildLayerCount));
await expectLater(
service.screenshot(
repaintBoundary,
width: 500.0,
height: 500.0,
margin: 50.0,
),
matchesGoldenFile('inspector.repaint_boundary_margin.png'),
);
// Verify that taking a screenshot didn't change the layers associated with
// the renderObject.
expect(renderObject.debugLayer, equals(layer));
// Verify that taking a screenshot did not change the number of children
// of the layer.
expect(getChildLayerCount(layer), equals(expectedChildLayerCount));
// Make sure taking a screenshot didn't change the parent of the layer.
expect(layer.parent, equals(layerParent));
await expectLater(
service.screenshot(
repaintBoundary,
width: 300.0,
height: 300.0,
debugPaint: true,
),
matchesGoldenFile('inspector.repaint_boundary_debugPaint.png'),
);
// Verify that taking a screenshot with debug paint on did not change
// the number of children the layer has.
expect(getChildLayerCount(layer), equals(expectedChildLayerCount));
// Ensure that creating screenshots including ones with debug paint
// hasn't changed the regular render of the widget.
await expectLater(
find.byType(RepaintBoundaryWithDebugPaint),
matchesGoldenFile('inspector.repaint_boundary.png'),
);
expect(renderObject.debugLayer, equals(layer));
expect(layer.attached, isTrue);
// Full size image
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 100.0,
height: 100.0,
),
matchesGoldenFile('inspector.container.png'),
);
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 100.0,
height: 100.0,
debugPaint: true,
),
matchesGoldenFile('inspector.container_debugPaint.png'),
);
{
// Verify calling the screenshot method still works if the RenderObject
// needs to be laid out again.
final RenderObject container =
find.byKey(outerContainerKey).evaluate().single.renderObject;
container
..markNeedsLayout()
..markNeedsPaint();
expect(container.debugNeedsLayout, isTrue);
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 100.0,
height: 100.0,
debugPaint: true,
),
matchesGoldenFile('inspector.container_debugPaint.png'),
);
expect(container.debugNeedsLayout, isFalse);
}
// Small image
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 50.0,
height: 100.0,
),
matchesGoldenFile('inspector.container_small.png'),
);
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 400.0,
height: 400.0,
maxPixelRatio: 3.0,
),
matchesGoldenFile('inspector.container_large.png'),
);
// This screenshot will show the clip rect debug paint but no other
// debug paint.
await expectLater(
service.screenshot(
find.byType(ClipRRect).evaluate().single,
width: 100.0,
height: 100.0,
debugPaint: true,
),
matchesGoldenFile('inspector.clipRect_debugPaint.png'),
);
// Add a margin so that the clip icon shows up in the screenshot.
// This golden image is platform dependent due to the clip icon.
await expectLater(
service.screenshot(
find.byType(ClipRRect).evaluate().single,
width: 100.0,
height: 100.0,
margin: 20.0,
debugPaint: true,
),
matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'),
skip: !Platform.isLinux
);
// Test with a very visible debug paint
await expectLater(
service.screenshot(
find.byKey(paddingKey).evaluate().single,
width: 300.0,
height: 300.0,
debugPaint: true,
),
matchesGoldenFile('inspector.padding_debugPaint.png'),
);
// The bounds for this box crop its rendered content.
await expectLater(
service.screenshot(
find.byKey(sizedBoxKey).evaluate().single,
width: 300.0,
height: 300.0,
debugPaint: true,
),
matchesGoldenFile('inspector.sizedBox_debugPaint.png'),
);
// Verify that setting a margin includes the previously cropped content.
await expectLater(
service.screenshot(
find.byKey(sizedBoxKey).evaluate().single,
width: 300.0,
height: 300.0,
margin: 50.0,
debugPaint: true,
),
matchesGoldenFile('inspector.sizedBox_debugPaint_margin.png'),
);
});
testWidgets('Screenshot of composited transforms - only offsets', (WidgetTester tester) async {
// Composited transforms are challenging to take screenshots of as the
// LeaderLayer and FollowerLayer classes used by CompositedTransformTarget
// and CompositedTransformFollower depend on traversing ancestors of the
// layer tree and mutating a [LayerLink] object when attaching layers to
// the tree so that the FollowerLayer knows about the LeaderLayer.
// 1. Finding the correct position for the follower layers requires
// traversing the ancestors of the follow layer to find a common ancestor
// with the leader layer.
// 2. Creating a LeaderLayer and attaching it to a layer tree has side
// effects as the leader layer will attempt to modify the mutable
// LeaderLayer object shared by the LeaderLayer and FollowerLayer.
// These tests verify that screenshots can still be taken and look correct
// when the leader and follower layer are both in the screenshots and when
// only the leader or follower layer is in the screenshot.
final LayerLink link = new LayerLink();
final GlobalKey key = new GlobalKey();
final GlobalKey mainStackKey = new GlobalKey();
final GlobalKey transformTargetParent = new GlobalKey();
final GlobalKey stackWithTransformFollower = new GlobalKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new RepaintBoundary(
child: new Stack(
key: mainStackKey,
children: <Widget>[
new Stack(
key: transformTargetParent,
children: <Widget>[
new Positioned(
left: 123.0,
top: 456.0,
child: new CompositedTransformTarget(
link: link,
child: new Container(height: 20.0, width: 20.0, color: const Color.fromARGB(128, 255, 0, 0)),
),
),
],
),
new Positioned(
left: 787.0,
top: 343.0,
child: new Stack(
key: stackWithTransformFollower,
children: <Widget>[
// Container so we can see how the follower layer was
// transformed relative to its initial location.
new Container(height: 15.0, width: 15.0, color: const Color.fromARGB(128, 0, 0, 255)),
new CompositedTransformFollower(
link: link,
child: new Container(key: key, height: 10.0, width: 10.0, color: const Color.fromARGB(128, 0, 255, 0)),
),
],
),
),
],
),
),
),
);
final RenderBox box = key.currentContext.findRenderObject();
expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
await expectLater(
find.byKey(mainStackKey),
matchesGoldenFile('inspector.composited_transform.only_offsets.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(stackWithTransformFollower).evaluate().first,
width: 5000.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.only_offsets_follower.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(find.byType(Stack).evaluate().first, width: 300.0, height: 300.0),
matchesGoldenFile('inspector.composited_transform.only_offsets_small.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(transformTargetParent).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.only_offsets_target.png'),
);
});
testWidgets('Screenshot composited transforms - with rotations', (WidgetTester tester) async {
final LayerLink link = new LayerLink();
final GlobalKey key1 = new GlobalKey();
final GlobalKey key2 = new GlobalKey();
final GlobalKey rotate1 = new GlobalKey();
final GlobalKey rotate2 = new GlobalKey();
final GlobalKey mainStackKey = new GlobalKey();
final GlobalKey stackWithTransformTarget = new GlobalKey();
final GlobalKey stackWithTransformFollower = new GlobalKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
key: mainStackKey,
children: <Widget>[
new Stack(
key: stackWithTransformTarget,
children: <Widget>[
new Positioned(
top: 123.0,
left: 456.0,
child: new Transform.rotate(
key: rotate1,
angle: 1.0, // radians
child: new CompositedTransformTarget(
link: link,
child: new Container(key: key1, height: 20.0, width: 20.0, color: const Color.fromARGB(128, 255, 0, 0)),
),
),
),
],
),
new Positioned(
top: 487.0,
left: 243.0,
child: new Stack(
key: stackWithTransformFollower,
children: <Widget>[
new Container(height: 15.0, width: 15.0, color: const Color.fromARGB(128, 0, 0, 255)),
new Transform.rotate(
key: rotate2,
angle: -0.3, // radians
child: new CompositedTransformFollower(
link: link,
child: new Container(key: key2, height: 10.0, width: 10.0, color: const Color.fromARGB(128, 0, 255, 0)),
),
),
],
),
),
],
),
),
);
final RenderBox box1 = key1.currentContext.findRenderObject();
final RenderBox box2 = key2.currentContext.findRenderObject();
// Snapshot the positions of the two relevant boxes to ensure that taking
// screenshots doesn't impact their positions.
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx));
expect(position1.dy, moreOrLessEquals(position2.dy));
// Image of the full scene to use as reference to help validate that the
// screenshots of specific subtrees are reasonable.
await expectLater(
find.byKey(mainStackKey),
matchesGoldenFile('inspector.composited_transform.with_rotations.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(mainStackKey).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.with_rotations_small.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(stackWithTransformTarget).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.with_rotations_target.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(stackWithTransformFollower).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.with_rotations_follower.png'),
);
// Make sure taking screenshots hasn't modified the positions of the
// TransformTarget or TransformFollower layers.
expect(identical(key1.currentContext.findRenderObject(), box1), isTrue);
expect(identical(key2.currentContext.findRenderObject(), box2), isTrue);
expect(box1.localToGlobal(Offset.zero), equals(position1));
expect(box2.localToGlobal(Offset.zero), equals(position2));
});
} }
} }
...@@ -251,8 +251,12 @@ Matcher isMethodCall(String name, {@required dynamic arguments}) { ...@@ -251,8 +251,12 @@ Matcher isMethodCall(String name, {@required dynamic arguments}) {
Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int sampleSize = 20}) Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int sampleSize = 20})
=> new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize); => new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
/// Asserts that a [Finder] matches exactly one widget whose rendered image /// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
/// matches the golden image file identified by [key]. /// golden image file identified by [key].
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget.
/// ///
/// [key] may be either a [Uri] or a [String] representation of a URI. /// [key] may be either a [Uri] or a [String] representation of a URI.
/// ///
...@@ -264,6 +268,8 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s ...@@ -264,6 +268,8 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s
/// ///
/// ```dart /// ```dart
/// await expectLater(find.text('Save'), matchesGoldenFile('save.png')); /// await expectLater(find.text('Save'), matchesGoldenFile('save.png'));
/// await expectLater(image, matchesGoldenFile('save.png'));
/// await expectLater(imageFuture, matchesGoldenFile('save.png'));
/// ``` /// ```
/// ///
/// See also: /// See also:
...@@ -1496,6 +1502,17 @@ class _CoversSameAreaAs extends Matcher { ...@@ -1496,6 +1502,17 @@ class _CoversSameAreaAs extends Matcher {
description.add('covers expected area and only expected area'); description.add('covers expected area and only expected area');
} }
Future<ui.Image> _captureImage(Element element) {
RenderObject renderObject = element.renderObject;
while (!renderObject.isRepaintBoundary) {
renderObject = renderObject.parent;
assert(renderObject != null);
}
assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.layer;
return layer.toImage(renderObject.paintBounds);
}
class _MatchesGoldenFile extends AsyncMatcher { class _MatchesGoldenFile extends AsyncMatcher {
const _MatchesGoldenFile(this.key); const _MatchesGoldenFile(this.key);
...@@ -1504,23 +1521,22 @@ class _MatchesGoldenFile extends AsyncMatcher { ...@@ -1504,23 +1521,22 @@ class _MatchesGoldenFile extends AsyncMatcher {
final Uri key; final Uri key;
@override @override
Future<String> matchAsync(covariant Finder finder) async { Future<String> matchAsync(dynamic item) async {
final Iterable<Element> elements = finder.evaluate(); Future<ui.Image> imageFuture;
if (elements.isEmpty) { if (item is Future<ui.Image>) {
return 'could not be rendered because no widget was found'; imageFuture = item;
} else if (elements.length > 1) { } else if (item is ui.Image) {
return 'matched too many widgets'; imageFuture = new Future<ui.Image>.value(item);
} } else {
final Element element = elements.single; final Finder finder = item;
final Iterable<Element> elements = finder.evaluate();
RenderObject renderObject = element.renderObject; if (elements.isEmpty) {
while (!renderObject.isRepaintBoundary) { return 'could not be rendered because no widget was found';
renderObject = renderObject.parent; } else if (elements.length > 1) {
assert(renderObject != null); return 'matched too many widgets';
}
imageFuture = _captureImage(elements.single);
} }
assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.layer;
final Future<ui.Image> imageFuture = layer.toImage(renderObject.paintBounds);
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
return binding.runAsync<String>(() async { return binding.runAsync<String>(() async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment