I need to draw some images in Flutter using geometric primitives, to both show in-app and cache for later use. What I'm doing right now is something similar to this:
import 'dart:ui';
final imageDimension = 600;
final recorder = PictureRecorder();
final canvas = Canvas(
recorder,
Rect.fromPoints(const Offset(0.0, 0.0),
Offset(imageDimension.toDouble(), imageDimension.toDouble())));
/// tens of thousands of canvas operations here:
// canvas.drawCircle(...);
// canvas.drawLine(...);
final picture = recorder.endRecording();
// the following call can take ~10s
final image = await picture.toImage(imageDimension, imageDimension);
final dataBytes = await image.toByteData(format: ImageByteFormat.png);
Here's an example of the outcome:
I know the image operations in this amount are heavy and I don't mind them taking some time. The problem is since they're CPU bound, they lock up the UI (even though they are async and outside of any widget build methods). There doesn't seem to be any way to break the picture.toImage()
call into smaller batches to make the UI more responsive.
My question is: Is there a way in Flutter to render a complex image built from geometric primitives in a way that doesn't impact the UI responsiveness?
My first idea was to do the heavy calculation inside a compute()
isolate, but that won't work since the calculations use some native code:
E/flutter (20500): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Invalid argument(s): Illegal argument in isolate message : (object extends NativeWrapper - Library:'dart:ui' Class: Picture)
E/flutter (20500): #0 spawnFunction (dart:_internal-patch/internal_patch.dart:190:54)
E/flutter (20500): #1 Isolate.spawn (dart:isolate-patch/isolate_patch.dart:362:7)
E/flutter (20500): #2 compute (package:flutter/src/foundation/_isolates_io.dart:22:41)
E/flutter (20500): #3 PatternRenderer.renderImage (package:bastono/services/pattern_renderer.dart:95:32)
E/flutter (20500): #4 TrackRenderService.getTrackRenderImage (package:bastono/services/track_render_service.dart:111:49)
E/flutter (20500): <asynchronous suspension>
I think there is an alternative approach with CustomPaint
painting just a couple of elements every frame and after it's all done screenshot it somehow using RenderRepaintBoundary.toImage(), but there's a couple of problems with this approach:
RenderRepaintBoundary
for the widget I use for rendering.Edit: it seems like the screenshot package allows for taking screenshots that are not rendered on the screen. I don't know about its performance characteristics yet, but it seems like it could work together with the CustomPaint
class. It still feels like a very convoluted workaround though, I'd be happy to see other options.
The solution I'm going with for now is based on pskink's suggestion - periodically rendering the canvas to an image and bootstrapping the new canvas with the rendered image for the next batch of operations.
Since the graphics operations take in the order of ~100ms I decided not to go with scheduleTask, but delaying the processing after every batch to give the other tasks a chance to execute. It is a bit hacky, but should be good enough for now.
Here's a simplified code of the solution:
import 'package:flutter/material.dart';
import 'package:dartx/dartx.dart';
import 'dart:ui' as dart_ui;
Future<List<Offset>> fetchWaypoints() async {
// some logic here
}
Future<Uint8List> renderPreview(String imageName) async {
const imageDimension = 1080;
dart_ui.PictureRecorder recorder = dart_ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
final paint = Paint()..color = Colors.black;
canvas.drawCircle(Offset.zero, 42.0, paint);
final waypoints = await fetchWaypoints();
dart_ui.Image intermediateImage =
await recorder.endRecording().toImage(imageDimension, imageDimension);
for (List<Offset> chunk in waypoints.chunked(100)) {
recorder = dart_ui.PictureRecorder();
canvas = Canvas(recorder);
canvas.drawImage(intermediateImage, Offset.zero, Paint());
for (Offset offset in chunk) {
canvas.drawCircle(offset, 1.0, paint);
}
intermediateImage =
await recorder.endRecording().toImage(imageDimension, imageDimension);
// give the other tasks a chance to execute
await Future.delayed(Duration.zero);
}
final byteData =
await intermediateImage.toByteData(format: dart_ui.ImageByteFormat.png);
return Uint8List.view(byteData!.buffer);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With