以不锁定 Flutter UI 的方式将 Canvas 渲染到图像

Rendering a Canvas to an Image in a way that doesn't lock up Flutter UI

我需要使用几何基元在 Flutter 中绘制一些图像,以显示应用内和缓存供以后使用。我现在正在做的事情与此类似:

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);

这是一个结果示例:

我知道这个数量的图像操作很繁重,我不介意他们花一些时间。问题是因为它们是 CPU 绑定的,所以它们锁定了 UI (即使它们是异步的并且在任何小部件构建方法之外)。似乎没有任何方法可以将 picture.toImage() 调用分成更小的批次以使 UI 响应更快。

我的问题是: Flutter 是否有一种方法可以在不影响 UI 响应能力的情况下渲染由几何基元构建的复杂图像?

我的第一个想法是在 compute() 隔离区内进行繁重的计算,但这行不通,因为计算使用了一些本机代码:

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>

我认为 CustomPaint painting just a couple of elements every frame and after it's all done screenshot it somehow using RenderRepaintBoundary.toImage() 有另一种方法,但这种方法有几个问题:

编辑:似乎 screenshot 包允许截取未在屏幕上呈现的屏幕截图。我还不知道它的性能特征,但它似乎可以与 CustomPaint class 一起工作。不过,它仍然感觉像是一个非常复杂的解决方法,我很乐意看到其他选择。

我认为问题是您试图将 Picture 对象从 Isolate 传递到主线程。

如果你最终要做的是制作一个 ByteData,你可以在 Isolate 秒之间交付它。我试着制作了一个简单的片段,它在 DartPad 上运行。

import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/foundation.dart';

void main() async{
  final result = await compute(drawImage, null);
}

Future<ByteData?> drawImage(String? params) async {
  const imageDimension = 600;
  final recorder = PictureRecorder();
  final canvas = Canvas(
      recorder,
      Rect.fromPoints(const Offset(0.0, 0.0),
          Offset(imageDimension.toDouble(), imageDimension.toDouble())));
  // Do your heavy computations with Canvas
  // canvas.drawCircle(...);
  // canvas.drawLine(...);
  final picture = recorder.endRecording();
  final image = await picture.toImage(imageDimension, imageDimension);
  final dataBytes = await image.toByteData(format: ImageByteFormat.png);
  return dataBytes;
}

希望对你有用。

我现在要使用的解决方案是基于 pskink's - 定期将 canvas 渲染到图像并使用下一个渲染图像引导新的 canvas批量操作。

由于图形操作大约需要 100 毫秒,所以我决定不使用 scheduleTask,而是在每批次之后延迟处理,以便让其他任务有机会执行。它有点 hacky,但现在应该足够好了。

下面是解决方案的简化代码:

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);
  }

可能最好的方法是将渲染从 dart:ui 转移到可在 compute 隔离中使用的库。

我需要的image package is written in pure Dart, so it should work well and it does support the primitive operations

我目前正在使用 的 Canvas 解决方案,它在大多数情况下都有效,但我很快就会切换到这个。