Unverified Commit 16c25caa authored by Tomasz Gucio's avatar Tomasz Gucio Committed by GitHub

Add RenderRepaintBoundary.toImageSync() method (#108280)

parent f48478a2
...@@ -3460,6 +3460,72 @@ class RenderRepaintBoundary extends RenderProxyBox { ...@@ -3460,6 +3460,72 @@ class RenderRepaintBoundary extends RenderProxyBox {
return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio); return offsetLayer.toImage(Offset.zero & size, pixelRatio: pixelRatio);
} }
/// Capture an image of the current state of this render object and its
/// children synchronously.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes in the dimensions
/// of the render object, multiplied by the [pixelRatio].
///
/// To use [toImageSync], 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
/// [dart:ui.FlutterView.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.
///
/// This API functions like [toImage], except that rasterization begins eagerly
/// on the raster thread and the image is returned before this is completed.
///
/// {@tool snippet}
///
/// The following is an example of how to go from a `GlobalKey` on a
/// `RepaintBoundary` to an image handle:
///
/// ```dart
/// class ImageCaptureHome extends StatefulWidget {
/// const ImageCaptureHome({super.key});
///
/// @override
/// State<ImageCaptureHome> createState() => _ImageCaptureHomeState();
/// }
///
/// class _ImageCaptureHomeState extends State<ImageCaptureHome> {
/// GlobalKey globalKey = GlobalKey();
///
/// void _captureImage() {
/// final RenderRepaintBoundary boundary = globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
/// final ui.Image image = boundary.toImageSync();
/// print('Image dimensions: ${image.width}x${image.height}');
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return RepaintBoundary(
/// key: globalKey,
/// child: Center(
/// child: TextButton(
/// onPressed: _captureImage,
/// child: const Text('Hello World', textDirection: TextDirection.ltr),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [OffsetLayer.toImageSync] for a similar API at the layer level.
/// * [dart:ui.Scene.toImageSync] for more information about the image returned.
ui.Image toImageSync({ double pixelRatio = 1.0 }) {
assert(!debugNeedsPaint);
final OffsetLayer offsetLayer = layer! as OffsetLayer;
return offsetLayer.toImageSync(Offset.zero & size, pixelRatio: pixelRatio);
}
/// The number of times that this render object repainted at the same time as /// 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 /// 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 /// paint at different times. When both paint at the same time, the repaint
......
...@@ -230,6 +230,94 @@ void main() { ...@@ -230,6 +230,94 @@ void main() {
expect(getPixel(image.width - 1, 20), equals(0xffffffff)); expect(getPixel(image.width - 1, 20), equals(0xffffffff));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/49857 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/49857
test('RenderRepaintBoundary can capture images of itself synchronously', () async {
RenderRepaintBoundary boundary = RenderRepaintBoundary();
layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
pumpFrame(phase: EnginePhase.composite);
ui.Image image = boundary.toImageSync();
expect(image.width, equals(100));
expect(image.height, equals(200));
// Now with pixel ratio set to something other than 1.0.
boundary = RenderRepaintBoundary();
layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
pumpFrame(phase: EnginePhase.composite);
image = boundary.toImageSync(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 = RenderRepaintBoundary();
final RenderStack stack = RenderStack()..alignment = Alignment.topLeft;
final RenderDecoratedBox blackBox = RenderDecoratedBox(
decoration: const BoxDecoration(color: Color(0xff000000)),
child: RenderConstrainedBox(
additionalConstraints: BoxConstraints.tight(const Size.square(20.0)),
),
);
stack.add(
RenderOpacity()
..opacity = 0.5
..child = blackBox,
);
final RenderDecoratedBox whiteBox = RenderDecoratedBox(
decoration: const BoxDecoration(color: Color(0xffffffff)),
child: RenderConstrainedBox(
additionalConstraints: BoxConstraints.tight(const Size.square(10.0)),
),
);
final RenderPositionedBox positioned = RenderPositionedBox(
widthFactor: 2.0,
heightFactor: 2.0,
alignment: Alignment.topRight,
child: whiteBox,
);
stack.add(positioned);
boundary.child = stack;
layout(boundary, constraints: BoxConstraints.tight(const Size(20.0, 20.0)));
pumpFrame(phase: EnginePhase.composite);
image = boundary.toImageSync();
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);
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.debugLayer! as OffsetLayer;
image = layer.toImageSync(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 = layer.toImageSync(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 = layer.toImageSync(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));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/49857
test('RenderOpacity does not composite if it is transparent', () { test('RenderOpacity does not composite if it is transparent', () {
final RenderOpacity renderOpacity = RenderOpacity( final RenderOpacity renderOpacity = RenderOpacity(
opacity: 0.0, opacity: 0.0,
......
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