From a043ac41f6bba370c8c6184884d4f30b5b7edd62 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 23 Apr 2018 12:28:17 -0700 Subject: [PATCH] Adding an API for capturing an image of a RenderRepaintBoundary. (#16758) This adds a toImage function to RenderRepaintBoundary that returns an uncompressed raw image of the RenderRepaintBoundary and its children. A device pixel ratio different from the physical ratio may be specified for the captured image. A value of 1.0 will give an image in logical pixels. --- .../flutter/lib/src/rendering/proxy_box.dart | 43 ++++++++++++- .../test/rendering/proxy_box_test.dart | 62 ++++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index a3c79fe47f..d06f61775d 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2,7 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' as ui show ImageFilter, Gradient; +import 'dart:async'; + +import 'dart:ui' as ui show ImageFilter, Gradient, SceneBuilder, Scene, Image; import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; @@ -2453,6 +2455,45 @@ class RenderRepaintBoundary extends RenderProxyBox { @override bool get isRepaintBoundary => true; + /// Capture an image of the current state of this render object and its + /// children. + /// + /// The returned [ui.Image] has uncompressed raw RGBA bytes in the dimensions + /// of the render object, multiplied by the [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. + /// + /// See also: + /// + /// * [dart:ui.Scene.toImage] for more information about the image returned. + Future toImage({double pixelRatio: 1.0}) async { + assert(!debugNeedsPaint); + final ui.SceneBuilder builder = new ui.SceneBuilder(); + final Matrix4 transform = new Matrix4.diagonal3Values(pixelRatio, pixelRatio, 1.0); + transform.translate(-layer.offset.dx, -layer.offset.dy, 0.0); + builder.pushTransform(transform.storage); + layer.addToScene(builder, Offset.zero); + final ui.Scene scene = builder.build(); + try { + // Size is rounded up to the next pixel to make sure we don't clip off + // anything. + return await scene.toImage( + (pixelRatio * size.width).ceil(), + (pixelRatio * size.height).ceil(), + ); + } finally { + scene.dispose(); + } + } + + /// The number of times that this render object repainted at the same time as /// its parent. Repaint boundaries are only useful when the parent and child /// paint at different times. When both paint at the same time, the repaint diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index 38b53ee9d1..fc4f34d0f6 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; + +import 'dart:ui' as ui show Image; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; @@ -15,9 +18,9 @@ void main() { RenderFittedBox makeFittedBox() { return new RenderFittedBox( child: new RenderCustomPaint( - painter: new TestCallbackPainter( - onPaint: () { painted = true; } - ), + painter: new TestCallbackPainter(onPaint: () { + painted = true; + }), ), ); } @@ -134,4 +137,57 @@ void main() { debugDefaultTargetPlatformOverride = null; }); }); + + test('RenderRepaintBoundary can capture images of itself', () async { + RenderRepaintBoundary boundary = new RenderRepaintBoundary(); + layout(boundary, constraints: new BoxConstraints.tight(const Size(100.0, 200.0))); + pumpFrame(phase: EnginePhase.composite); + ui.Image image = await boundary.toImage(); + expect(image.width, equals(100)); + expect(image.height, equals(200)); + + // Now with pixel ratio set to something other than 1.0. + boundary = new RenderRepaintBoundary(); + layout(boundary, constraints: new BoxConstraints.tight(const Size(100.0, 200.0))); + pumpFrame(phase: EnginePhase.composite); + image = await boundary.toImage(pixelRatio: 2.0); + expect(image.width, equals(200)); + expect(image.height, equals(400)); + + // Try building one with two child layers and make sure it renders them both. + boundary = new RenderRepaintBoundary(); + final RenderStack stack = new RenderStack()..alignment = Alignment.topLeft; + final RenderDecoratedBox blackBox = new RenderDecoratedBox( + decoration: const BoxDecoration(color: const Color(0xff000000)), + child: new RenderConstrainedBox( + additionalConstraints: new BoxConstraints.tight(const Size.square(20.0)), + )); + stack.add(new RenderOpacity() + ..opacity = 0.5 + ..child = blackBox); + final RenderDecoratedBox whiteBox = new RenderDecoratedBox( + decoration: const BoxDecoration(color: const Color(0xffffffff)), + child: new RenderConstrainedBox( + additionalConstraints: new BoxConstraints.tight(const Size.square(10.0)), + )); + final RenderPositionedBox positioned = new RenderPositionedBox( + widthFactor: 2.0, + heightFactor: 2.0, + alignment: Alignment.topRight, + child: whiteBox, + ); + stack.add(positioned); + boundary.child = stack; + layout(boundary, constraints: new BoxConstraints.tight(const Size(20.0, 20.0))); + pumpFrame(phase: EnginePhase.composite); + image = await boundary.toImage(); + expect(image.width, equals(20)); + expect(image.height, equals(20)); + final ByteData data = await image.toByteData(); + expect(data.lengthInBytes, equals(20 * 20 * 4)); + expect(data.elementSizeInBytes, equals(1)); + const int stride = 20 * 4; + expect(data.getUint32(0), equals(0x00000080)); + expect(data.getUint32(stride - 4), equals(0xffffffff)); + }); }