mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
This reverts commit 3306fc1042
.
This commit is contained in:
parent
3306fc1042
commit
5b5a5b8228
@ -1 +1 @@
|
||||
da615501c1032cb803b2a5623b07d7f4834d9640
|
||||
1a999092d10a22bc700214b257cd4890c5800078
|
||||
|
@ -50,8 +50,6 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
|
||||
Layer _previousSibling;
|
||||
|
||||
/// Removes this layer from its parent layer's child list.
|
||||
///
|
||||
/// This has no effect if the layer's parent is already null.
|
||||
@mustCallSuper
|
||||
void remove() {
|
||||
parent?._removeChild(this);
|
||||
@ -590,11 +588,7 @@ class OffsetLayer extends ContainerLayer {
|
||||
assert(bounds != null);
|
||||
assert(pixelRatio != null);
|
||||
final ui.SceneBuilder builder = new ui.SceneBuilder();
|
||||
final Matrix4 transform = new Matrix4.translationValues(
|
||||
(-bounds.left - offset.dx) * pixelRatio,
|
||||
(-bounds.top - offset.dy) * pixelRatio,
|
||||
0.0,
|
||||
);
|
||||
final Matrix4 transform = new Matrix4.translationValues(bounds.left - offset.dx, bounds.top - offset.dy, 0.0);
|
||||
transform.scale(pixelRatio, pixelRatio);
|
||||
builder.pushTransform(transform.storage);
|
||||
addToScene(builder, Offset.zero);
|
||||
|
@ -60,13 +60,7 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset);
|
||||
/// New [PaintingContext] objects are created automatically when using
|
||||
/// [PaintingContext.repaintCompositedChild] and [pushLayer].
|
||||
class PaintingContext extends ClipContext {
|
||||
|
||||
/// Creates a painting context.
|
||||
///
|
||||
/// Typically only called by [PaintingContext.repaintCompositedChild]
|
||||
/// and [pushLayer].
|
||||
@protected
|
||||
PaintingContext(this._containerLayer, this.estimatedBounds)
|
||||
PaintingContext._(this._containerLayer, this.estimatedBounds)
|
||||
: assert(_containerLayer != null),
|
||||
assert(estimatedBounds != null);
|
||||
|
||||
@ -92,19 +86,8 @@ class PaintingContext extends ClipContext {
|
||||
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
|
||||
/// has a composited layer.
|
||||
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
|
||||
assert(child._needsPaint);
|
||||
_repaintCompositedChild(
|
||||
child,
|
||||
debugAlsoPaintedParent: debugAlsoPaintedParent,
|
||||
);
|
||||
}
|
||||
|
||||
static void _repaintCompositedChild(
|
||||
RenderObject child, {
|
||||
bool debugAlsoPaintedParent = false,
|
||||
PaintingContext childContext,
|
||||
}) {
|
||||
assert(child.isRepaintBoundary);
|
||||
assert(child._needsPaint);
|
||||
assert(() {
|
||||
// register the call for RepaintBoundary metrics
|
||||
child.debugRegisterRepaintBoundaryPaint(
|
||||
@ -124,32 +107,9 @@ class PaintingContext extends ClipContext {
|
||||
child._layer.debugCreator = child.debugCreator ?? child.runtimeType;
|
||||
return true;
|
||||
}());
|
||||
childContext ??= new PaintingContext(child._layer, child.paintBounds);
|
||||
final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds);
|
||||
child._paintWithContext(childContext, Offset.zero);
|
||||
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;
|
||||
}());
|
||||
childContext._stopRecordingIfNeeded();
|
||||
}
|
||||
|
||||
/// Paint a child [RenderObject].
|
||||
@ -165,7 +125,7 @@ class PaintingContext extends ClipContext {
|
||||
}());
|
||||
|
||||
if (child.isRepaintBoundary) {
|
||||
stopRecordingIfNeeded();
|
||||
_stopRecordingIfNeeded();
|
||||
_compositeChild(child, offset);
|
||||
} else {
|
||||
child._paintWithContext(this, offset);
|
||||
@ -199,20 +159,10 @@ class PaintingContext extends ClipContext {
|
||||
}());
|
||||
}
|
||||
child._layer.offset = offset;
|
||||
appendLayer(child._layer);
|
||||
_appendLayer(child._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) {
|
||||
void _appendLayer(Layer layer) {
|
||||
assert(!_isRecording);
|
||||
layer.remove();
|
||||
_containerLayer.append(layer);
|
||||
@ -260,19 +210,7 @@ class PaintingContext extends ClipContext {
|
||||
_containerLayer.append(_currentLayer);
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
void _stopRecordingIfNeeded() {
|
||||
if (!_isRecording)
|
||||
return;
|
||||
assert(() {
|
||||
@ -333,8 +271,8 @@ class PaintingContext extends ClipContext {
|
||||
/// * [pushLayer], for adding a layer and using its canvas to paint with that
|
||||
/// layer.
|
||||
void addLayer(Layer layer) {
|
||||
stopRecordingIfNeeded();
|
||||
appendLayer(layer);
|
||||
_stopRecordingIfNeeded();
|
||||
_appendLayer(layer);
|
||||
}
|
||||
|
||||
/// Appends the given layer to the recording, and calls the `painter` callback
|
||||
@ -357,21 +295,15 @@ class PaintingContext extends ClipContext {
|
||||
/// See also:
|
||||
///
|
||||
/// * [addLayer], for pushing a leaf layer whose canvas is not used.
|
||||
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
|
||||
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
|
||||
assert(!childLayer.attached);
|
||||
assert(childLayer.parent == null);
|
||||
assert(painter != null);
|
||||
stopRecordingIfNeeded();
|
||||
appendLayer(childLayer);
|
||||
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
|
||||
_stopRecordingIfNeeded();
|
||||
_appendLayer(childLayer);
|
||||
final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? estimatedBounds);
|
||||
painter(childContext, offset);
|
||||
childContext.stopRecordingIfNeeded();
|
||||
}
|
||||
|
||||
/// Creates a compatible painting context to paint onto [childLayer].
|
||||
@protected
|
||||
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
|
||||
return new PaintingContext(childLayer, bounds);
|
||||
childContext._stopRecordingIfNeeded();
|
||||
}
|
||||
|
||||
/// Clip further painting using a rectangle.
|
||||
@ -2104,6 +2036,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
|
||||
Rect get paintBounds;
|
||||
|
||||
/// Override this method to paint debugging information.
|
||||
@protected
|
||||
void debugPaint(PaintingContext context, Offset offset) { }
|
||||
|
||||
/// Paint this render object into the given context at the given offset.
|
||||
|
@ -7,26 +7,13 @@ import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui
|
||||
show
|
||||
window,
|
||||
ClipOp,
|
||||
Image,
|
||||
ImageByteFormat,
|
||||
Paragraph,
|
||||
Picture,
|
||||
PictureRecorder,
|
||||
PointMode,
|
||||
SceneBuilder,
|
||||
Vertices;
|
||||
import 'dart:ui' show Canvas, Offset;
|
||||
import 'dart:ui' as ui show window, Picture, SceneBuilder, PictureRecorder;
|
||||
import 'dart:ui' show Offset;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'app.dart';
|
||||
import 'basic.dart';
|
||||
@ -44,556 +31,6 @@ typedef void _RegisterServiceExtensionCallback({
|
||||
@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]
|
||||
/// objects.
|
||||
///
|
||||
@ -1027,33 +464,6 @@ class WidgetInspectorService {
|
||||
name: '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.
|
||||
@ -1626,77 +1036,6 @@ class WidgetInspectorService {
|
||||
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) {
|
||||
final DiagnosticsNode previousSelection = toObject(previousSelectionId);
|
||||
final Element current = selection?.currentElement;
|
||||
|
@ -512,7 +512,7 @@ void main() {
|
||||
|
||||
// If you add a service extension... TEST IT! :-)
|
||||
// ...then increment this number.
|
||||
expect(binding.extensions.length, 38);
|
||||
expect(binding.extensions.length, 37);
|
||||
|
||||
expect(console, isEmpty);
|
||||
debugPrint = debugPrintThrottled;
|
||||
|
@ -185,43 +185,12 @@ void main() {
|
||||
image = await boundary.toImage();
|
||||
expect(image.width, equals(20));
|
||||
expect(image.height, equals(20));
|
||||
ByteData data = await image.toByteData();
|
||||
|
||||
int getPixel(int x, int y) => data.getUint32((x + y * image.width) * 4);
|
||||
|
||||
final ByteData data = await image.toByteData();
|
||||
expect(data.lengthInBytes, equals(20 * 20 * 4));
|
||||
expect(data.elementSizeInBytes, equals(1));
|
||||
expect(getPixel(0, 0), equals(0x00000080));
|
||||
expect(getPixel(image.width - 1, 0 ), 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));
|
||||
const int stride = 20 * 4;
|
||||
expect(data.getUint32(0), equals(0x00000080));
|
||||
expect(data.getUint32(stride - 4), equals(0xffffffff));
|
||||
});
|
||||
|
||||
test('RenderOpacity does not composite if it is transparent', () {
|
||||
|
@ -4,8 +4,6 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:ui' as ui show PictureRecorder;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@ -14,80 +12,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
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() {
|
||||
TestWidgetInspectorService.runTests();
|
||||
}
|
||||
@ -1315,483 +1239,5 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
|
||||
expect(service.rebuildCount, equals(2));
|
||||
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,12 +251,8 @@ Matcher isMethodCall(String name, {@required dynamic arguments}) {
|
||||
Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int sampleSize = 20})
|
||||
=> new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
|
||||
|
||||
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
|
||||
/// 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.
|
||||
/// Asserts that a [Finder] matches exactly one widget whose rendered image
|
||||
/// matches the golden image file identified by [key].
|
||||
///
|
||||
/// [key] may be either a [Uri] or a [String] representation of a URI.
|
||||
///
|
||||
@ -268,8 +264,6 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s
|
||||
///
|
||||
/// ```dart
|
||||
/// await expectLater(find.text('Save'), matchesGoldenFile('save.png'));
|
||||
/// await expectLater(image, matchesGoldenFile('save.png'));
|
||||
/// await expectLater(imageFuture, matchesGoldenFile('save.png'));
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
@ -1502,17 +1496,6 @@ class _CoversSameAreaAs extends Matcher {
|
||||
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 {
|
||||
const _MatchesGoldenFile(this.key);
|
||||
|
||||
@ -1521,22 +1504,23 @@ class _MatchesGoldenFile extends AsyncMatcher {
|
||||
final Uri key;
|
||||
|
||||
@override
|
||||
Future<String> matchAsync(dynamic item) async {
|
||||
Future<ui.Image> imageFuture;
|
||||
if (item is Future<ui.Image>) {
|
||||
imageFuture = item;
|
||||
} else if (item is ui.Image) {
|
||||
imageFuture = new Future<ui.Image>.value(item);
|
||||
} else {
|
||||
final Finder finder = item;
|
||||
final Iterable<Element> elements = finder.evaluate();
|
||||
if (elements.isEmpty) {
|
||||
return 'could not be rendered because no widget was found';
|
||||
} else if (elements.length > 1) {
|
||||
return 'matched too many widgets';
|
||||
}
|
||||
imageFuture = _captureImage(elements.single);
|
||||
Future<String> matchAsync(covariant Finder finder) async {
|
||||
final Iterable<Element> elements = finder.evaluate();
|
||||
if (elements.isEmpty) {
|
||||
return 'could not be rendered because no widget was found';
|
||||
} else if (elements.length > 1) {
|
||||
return 'matched too many widgets';
|
||||
}
|
||||
final Element element = elements.single;
|
||||
|
||||
RenderObject renderObject = element.renderObject;
|
||||
while (!renderObject.isRepaintBoundary) {
|
||||
renderObject = renderObject.parent;
|
||||
assert(renderObject != null);
|
||||
}
|
||||
assert(!renderObject.debugNeedsPaint);
|
||||
final OffsetLayer layer = renderObject.layer;
|
||||
final Future<ui.Image> imageFuture = layer.toImage(renderObject.paintBounds);
|
||||
|
||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
|
||||
return binding.runAsync<String>(() async {
|
||||
|
Loading…
Reference in New Issue
Block a user