Created
October 6, 2024 17:11
-
-
Save leoafarias/036602a69d01a404dcffd937d211fdae to your computer and use it in GitHub Desktop.
Slide capture
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: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