Skip to content

Instantly share code, notes, and snippets.

@clragon
Last active July 16, 2023 23:40
Show Gist options
  • Select an option

  • Save clragon/2a38b107b60146ae4cbe14691c006265 to your computer and use it in GitHub Desktop.

Select an option

Save clragon/2a38b107b60146ae4cbe14691c006265 to your computer and use it in GitHub Desktop.
Allows capturing image snapshots of arbitrary child widgets
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