Last active
July 16, 2023 23:40
-
-
Save clragon/2a38b107b60146ae4cbe14691c006265 to your computer and use it in GitHub Desktop.
Allows capturing image snapshots of arbitrary child widgets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:async'; | |
| import 'dart:typed_data'; | |
| import 'package:flutter/rendering.dart'; | |
| import 'package:flutter/widgets.dart'; | |
| import 'dart:ui' as ui; | |
| /// A controller for a [PrintoutFrame]. | |
| /// | |
| /// Allows capturing the rendered image of the child widget. | |
| /// Use with [PrintoutFrame] around the target widget. | |
| /// | |
| /// This widget will not capture PlatformView widgets. | |
| /// Based on: [SnapshotWidget]. | |
| class PrintoutController extends ChangeNotifier { | |
| /// The rendered image of the child widget. | |
| Uint8List? _image; | |
| /// The rendered image of the child widget. | |
| /// Null if no capture was scheduled or the capture is not yet complete. | |
| Uint8List? get image => _image; | |
| /// Used to debounce multiple captures. | |
| Completer<void>? _completer; | |
| /// Whether a capture is scheduled. | |
| /// This is false right after the first paint, but this does not mean the capture is complete. | |
| final ValueNotifier<bool> _capturing = ValueNotifier(false); | |
| /// Schedules a capture of the rendered image of the child widget. | |
| /// | |
| /// This method completes once the image was captured. | |
| /// It may be called in quick succession, | |
| /// but a new capture is only scheduled once the previous one is complete. | |
| Future<void> captureImage() { | |
| if (_completer == null || _completer!.isCompleted) { | |
| _completer = Completer<void>(); | |
| } else { | |
| return _completer!.future; | |
| } | |
| _capturing.value = true; | |
| return _completer!.future; | |
| } | |
| /// Sets the result of a capture. | |
| /// | |
| /// This method is called by [PrintoutFrame]. | |
| /// It is not intended to be called by user code. | |
| void setImage(Uint8List image) { | |
| _capturing.value = false; | |
| _image = image; | |
| _completer!.complete(); | |
| notifyListeners(); | |
| } | |
| /// Removes the previously captured image. | |
| void clear() { | |
| _image = null; | |
| notifyListeners(); | |
| } | |
| } | |
| /// A widget that captures the rendered image of its child. | |
| /// | |
| /// Use a [PrintoutController] to schedule a capture and access the result. | |
| class PrintoutFrame extends SingleChildRenderObjectWidget { | |
| /// Creates a widget that captures the rendered image of its child. | |
| const PrintoutFrame({ | |
| super.key, | |
| required this.controller, | |
| required super.child, | |
| }); | |
| /// The controller that schedules captures and stores the rendered image. | |
| final PrintoutController controller; | |
| @override | |
| RenderObject createRenderObject(BuildContext context) { | |
| return _RenderPrintoutFrame(controller: controller); | |
| } | |
| @override | |
| void updateRenderObject( | |
| BuildContext context, covariant RenderObject renderObject) { | |
| (renderObject as _RenderPrintoutFrame).controller = controller; | |
| } | |
| } | |
| class _RenderPrintoutFrame extends RenderProxyBox { | |
| PrintoutController controller; | |
| _RenderPrintoutFrame({required this.controller}); | |
| @override | |
| void attach(PipelineOwner owner) { | |
| controller._capturing.addListener(_onImageChanged); | |
| super.attach(owner); | |
| } | |
| @override | |
| void detach() { | |
| controller._capturing.removeListener(_onImageChanged); | |
| super.detach(); | |
| } | |
| /// Calls [markNeedsPaint] when a capture is scheduled. | |
| void _onImageChanged() { | |
| if (controller._capturing.value) { | |
| markNeedsPaint(); | |
| } | |
| } | |
| Future<ui.Image?> _paintAndDetachToImage() async { | |
| final OffsetLayer offsetLayer = OffsetLayer(); | |
| final PaintingContext context = | |
| PaintingContext(offsetLayer, Offset.zero & size); | |
| super.paint(context, Offset.zero); | |
| // ignore: invalid_use_of_protected_member | |
| context.stopRecordingIfNeeded(); | |
| ui.Image childImage = offsetLayer.toImageSync(Offset.zero & size); | |
| offsetLayer.dispose(); | |
| ByteData? data = await childImage.toByteData( | |
| format: ui.ImageByteFormat.png, | |
| ); | |
| if (data == null) { | |
| throw StateError( | |
| 'WidgetImageFrame: Unexpected failure to capture child widget image', | |
| ); | |
| } | |
| childImage.dispose(); | |
| controller.setImage(data.buffer.asUint8List()); | |
| return null; | |
| } | |
| @override | |
| void paint(PaintingContext context, Offset offset) { | |
| if (controller._capturing.value) { | |
| /// This is technically a side effect, but we only want to execute this once. | |
| /// [_onImageChanged] ensures that this is not a continuous loop. | |
| controller._capturing.value = false; | |
| _paintAndDetachToImage(); | |
| } | |
| super.paint(context, offset); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment