Skip to content

Instantly share code, notes, and snippets.

@leoafarias
Created October 6, 2024 17:11
Show Gist options
  • Save leoafarias/036602a69d01a404dcffd937d211fdae to your computer and use it in GitHub Desktop.
Save leoafarias/036602a69d01a404dcffd937d211fdae to your computer and use it in GitHub Desktop.
Slide capture
import 'dart:async';
import 'dart:developer';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import '../../components/organisms/app_shell.dart';
import '../common/helpers/constants.dart';
enum Quality {
low(0.4),
good(1),
better(2),
best(3);
const Quality(
this.pixelRatio,
);
final double pixelRatio;
}
class WidgetCaptureService {
WidgetCaptureService({
this.maxConcurrentGenerations = 3,
this.retryCount = 3,
this.retryDelay = const Duration(milliseconds: 250),
});
static final _generationQueue = <String>{};
final int maxConcurrentGenerations;
final int retryCount;
final Duration retryDelay;
Future<Uint8List> generate({
Quality quality = Quality.low,
required Widget widget,
required BuildContext context,
}) async {
final widgetKey = widget.key ?? UniqueKey();
final queueKey = shortHash('$widgetKey${quality.name}');
try {
while (_generationQueue.length >= maxConcurrentGenerations) {
await Future.delayed(const Duration(milliseconds: 100));
}
_generationQueue.add(queueKey);
final image = await _fromWidgetToImage(
widget,
context: kScaffoldKey.currentContext!,
pixelRatio: quality.pixelRatio,
targetSize: kResolution,
);
return _imageToUint8List(image);
} catch (e, stackTrace) {
log('Error generating image: $e', stackTrace: stackTrace);
rethrow;
} finally {
_generationQueue.remove(queueKey);
}
}
Future<Uint8List> generateWithKey({
required GlobalKey key,
required Quality quality,
}) async {
final boundary =
key.currentContext!.findRenderObject() as RenderRepaintBoundary;
// Get the size of the boundary
final boundarySize = boundary.size;
// adjust the pixel ratio based on the ideal size which is kResolution
final pixelRatio = kResolution.width / boundarySize.width;
final image = await boundary.toImage(
pixelRatio: quality.pixelRatio * pixelRatio,
);
return _imageToUint8List(image);
}
Future<Uint8List> _imageToUint8List(ui.Image image) async {
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
image.dispose();
return byteData!.buffer.asUint8List();
}
Future<ui.Image> _fromWidgetToImage(
Widget widget, {
required double pixelRatio,
required BuildContext context,
Size? targetSize,
}) async {
try {
final child = InheritedTheme.captureAll(
context,
MediaQuery(
data: MediaQuery.of(context),
child: MaterialApp(
theme: Theme.of(context),
debugShowCheckedModeBanner: false,
home: Scaffold(body: widget),
),
),
);
final repaintBoundary = RenderRepaintBoundary();
final platformDispatcher = WidgetsBinding.instance.platformDispatcher;
final view = View.maybeOf(context) ?? platformDispatcher.views.first;
final logicalSize =
targetSize ?? view.physicalSize / view.devicePixelRatio;
int retryCount = this.retryCount;
bool isDirty = false;
final renderView = RenderView(
view: view,
child: RenderPositionedBox(
alignment: Alignment.center,
child: repaintBoundary,
),
configuration: ViewConfiguration(
logicalConstraints: BoxConstraints(
maxWidth: logicalSize.width,
maxHeight: logicalSize.height,
),
physicalConstraints: BoxConstraints(
maxWidth: logicalSize.width * pixelRatio,
maxHeight: logicalSize.height * pixelRatio,
),
devicePixelRatio: pixelRatio,
),
);
final pipelineOwner = PipelineOwner(
onNeedVisualUpdate: () {
isDirty = true;
},
);
final buildOwner = BuildOwner(
focusManager: FocusManager(),
onBuildScheduled: () {
isDirty = true;
},
);
pipelineOwner.rootNode = renderView;
renderView.prepareInitialFrame();
final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
container: repaintBoundary,
child: Directionality(
textDirection: TextDirection.ltr,
child: child,
),
).attachToRenderTree(buildOwner);
while (retryCount > 0) {
isDirty = false;
buildOwner
..buildScope(rootElement)
..finalizeTree();
pipelineOwner
..flushLayout()
..flushCompositingBits()
..flushPaint();
await Future.delayed(retryDelay);
await waitForImageProviders(rootElement);
if (!isDirty) {
log('Image generation completed.');
break;
}
log('Image generation.. waiting...');
retryCount--;
}
final image = await repaintBoundary.toImage(pixelRatio: pixelRatio);
buildOwner.finalizeTree();
return image;
} catch (e) {
log('Error finalizing tree: $e');
rethrow;
}
}
}
Future<void> waitForImageProviders(BuildContext context) async {
final List<Future<void>> futures = [];
void visit(Element element) {
// Handle Image widgets
if (element.widget is Image) {
final image = element.widget as Image;
final provider = image.image;
final stream = provider.resolve(ImageConfiguration.empty);
if (stream.completer != null) {
futures.add(_waitForImageCompleter(stream.completer!));
}
}
// Handle BoxDecoration images
if (element.widget is Container) {
final container = element.widget as Container;
final decoration = container.decoration;
if (decoration is BoxDecoration && decoration.image != null) {
final provider = decoration.image!.image;
final stream = provider.resolve(ImageConfiguration.empty);
if (stream.completer != null) {
futures.add(_waitForImageCompleter(stream.completer!));
}
}
}
// Recursively visit children
element.visitChildren(visit);
}
context.visitChildElements(visit);
await Future.wait(futures);
}
Future<void> _waitForImageCompleter(ImageStreamCompleter completer) {
final Completer<void> imageCompleter = Completer<void>();
void listener(ImageInfo image, bool synchronousCall) {
// completer.removeListener(imageListener);
imageCompleter.complete();
}
final imageListener = ImageStreamListener(listener);
completer.addListener(imageListener);
return imageCompleter.future;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment