Flutter - 在 CustomPainter 中重用之前绘制的 canvas

Flutter - Reuse previously painted canvas in a CustomPainter

我有一个 CustomPainter,我想每隔几毫秒渲染一些项目。但我只想渲染自上次抽奖以来发生变化的项目。我计划手动清除将要更改的区域并在该区域重新绘制。问题是每次调用 paint() 时,Flutter 中的 canvas 似乎都是全新的。我知道我可以跟踪整个状态并每次都重新绘制所有内容,但出于性能原因和特定用例,这是不可取的。以下是可能代表该问题的示例代码:

我知道当 canvas 大小改变时,一切都需要重新绘制。

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';

class CanvasWidget extends StatefulWidget {
  CanvasWidget({Key key}) : super(key: key);

  @override
  _CanvasWidgetState createState() => _CanvasWidgetState();
}

class _CanvasWidgetState extends State<CanvasWidget> {
  final _repaint = ValueNotifier<int>(0);
  TestingPainter _wavePainter;

  @override
  void initState() {
    _wavePainter = TestingPainter(repaint: _repaint);
    Timer.periodic( Duration(milliseconds: 50), (Timer timer) {
      _repaint.value++;
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
       painter: _wavePainter,
    );
  }
}

class TestingPainter extends CustomPainter {
  static const double _numberPixelsToDraw = 3;
  final _rng = Random();

  double _currentX = 0;
  double _currentY = 0;

  TestingPainter({Listenable repaint}): super(repaint: repaint);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint();
    paint.color = Colors.transparent;
    if(_currentX + _numberPixelsToDraw > size.width)
    {
      _currentX = 0;
    }

    // Clear previously drawn points
    var clearArea = Rect.fromLTWH(_currentX, 0, _numberPixelsToDraw, size.height);
    canvas.drawRect(clearArea, paint);

    Path path = Path();
    path.moveTo(_currentX, _currentY);
    for(int i = 0; i < _numberPixelsToDraw; i++)
    {
      _currentX++;
      _currentY = _rng.nextInt(size.height.toInt()).toDouble();
      path.lineTo(_currentX, _currentY);
    }

    // Draw new points in red    
    paint.color = Colors.red;
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

目前唯一可用的解决方案是将进度捕获为图像,然后绘制图像,而不是执行整个 canvas 代码。

要绘制图像,您可以使用上面评论中 pskink 提到的 canvas.drawImage

但我推荐的解决方案是用 RenderRepaint 包装 CustomPaint 以将该小部件转换为图像。详情请参阅 and (https://medium.com/flutter-community/export-your-widget-to-image-with-flutter-dc7ecfa6bafb 简单实现),有条件判断你是不是第一次搭建

class _CanvasWidgetState extends State<CanvasWidget> {
  /// Just to track if its the first frame or not.
  var _flag = false;

  /// Will be used for generating png image.
  final _globalKey = new GlobalKey();

  /// Stores the image bytes
  Uint8List _imageBytes;

  /// No need for this actually;
  /// final _repaint = ValueNotifier<int>(0);
  TestingPainter _wavePainter;

  Future<Uint8List> _capturePng() async {
    try {
      final boundary = _globalKey
         .currentContext.findRenderObject();
      ui.Image image = await boundary.toImage();
      ByteData byteData =
          await image.toByteData(format: ui.ImageByteFormat.png);
      var pngBytes = byteData.buffer.asUint8List();
      var bs64 = base64Encode(pngBytes);
      print(pngBytes);
      print(bs64);
      setState(() {});
      return pngBytes;
    } catch (e) {
      print(e);
    }
  }

  @override
  void initState() {
    _wavePainter = TestingPainter();
    Timer.periodic( Duration(milliseconds: 50), (Timer timer) {
      if (!flag) flag = true;

      /// Save your image before each redraw.
      _imageBytes = _capturePng();   

      /// You don't need a listener if you are using a stful widget.
      /// It will do just fine.
      setState(() {});
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      key: _globalkey,
      child: Container(
        /// Use this if this is not the first frame.
        decoration: _flag ? BoxDecoration(
          image: DecorationImage(
            image: MemoryImage(_imageBytes)
          )
        ) : null,
        child: CustomPainter(
          painter: _wavePainter
        )
      )
    );
  }
}

这样图像就不会成为您自定义画家的一部分,让我告诉你,我尝试使用 canvas 绘制图像,但效率不高,MemoryImage 由 flutter 提供以更好的方式呈现图像。

重绘整个canvas,甚至在每一帧,是完全有效的。尝试重用前一帧通常不会更有效率。

查看您发布的代码,某些方面有改进的余地,但试图保留 canvas 的部分不应该是其中之一。

您遇到的真正性能问题是每 50 毫秒从 Timer.periodic 事件重复更改 ValueNotifier。处理每一帧重绘的更好方法是使用 AnimatedBuildervsync,因此将在每一帧调用 CustomPainterpaint 方法。如果您熟悉的话,这类似于 Web 浏览器世界中的 Window.requestAnimationFrame。如果您熟悉计算机图形的工作原理,这里的 vsync 代表“垂直同步”。本质上,您的 paint 方法将在具有 60 Hz 屏幕的设备上每秒调用 60 次,并且它将在 120 Hz 屏幕上每秒绘制 120 次。这是在不同类型的设备上实现流畅动画的正确且可扩展的方式。

在考虑保留部分 canvas 之前,还有其他值得优化的地方。例如,简单地看一下你的代码,你有这一行:

_currentY = _rng.nextInt(size.height.toInt()).toDouble();

在这里我假设你想在 0size.height 之间有一个随机小数,如果是这样,你可以简单地写 _rng.nextDouble() * size.height,而不是将 double 转换为 int 并返回再次,并(可能无意中)在该过程中将其四舍五入。但是像这样的东西带来的性能提升可以忽略不计。

想想看,如果一个 3D 视频游戏可以 运行 流畅地 phone,每一帧都与前一帧有很大不同,你的动画应该 运行 流畅,无需担心手动清除 canvas 的部分内容。尝试手动优化 canvas 可能反而会导致性能下降。

因此,您真正应该关注的是使用 AnimatedBuilder 而不是 Timer 来触发项目中的 canvas 重绘,作为起点。

例如,这是我使用 AnimatedBuilder 和 CustomPaint 制作的一个小演示:

完整源代码:

import 'dart:math';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  List<SnowFlake> snowflakes = List.generate(100, (index) => SnowFlake());
  AnimationController _controller;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    )..repeat();
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Colors.blue, Colors.lightBlue, Colors.white],
            stops: [0, 0.7, 0.95],
          ),
        ),
        child: AnimatedBuilder(
          animation: _controller,
          builder: (_, __) {
            snowflakes.forEach((snow) => snow.fall());
            return CustomPaint(
              painter: MyPainter(snowflakes),
            );
          },
        ),
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  final List<SnowFlake> snowflakes;

  MyPainter(this.snowflakes);

  @override
  void paint(Canvas canvas, Size size) {
    final w = size.width;
    final h = size.height;
    final c = size.center(Offset.zero);

    final whitePaint = Paint()..color = Colors.white;

    canvas.drawCircle(c - Offset(0, -h * 0.165), w / 6, whitePaint);
    canvas.drawOval(
        Rect.fromCenter(
          center: c - Offset(0, -h * 0.35),
          width: w * 0.5,
          height: w * 0.6,
        ),
        whitePaint);

    snowflakes.forEach((snow) =>
        canvas.drawCircle(Offset(snow.x, snow.y), snow.radius, whitePaint));
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

class SnowFlake {
  double x = Random().nextDouble() * 400;
  double y = Random().nextDouble() * 800;
  double radius = Random().nextDouble() * 2 + 2;
  double velocity = Random().nextDouble() * 4 + 2;

  SnowFlake();

  fall() {
    y += velocity;
    if (y > 800) {
      x = Random().nextDouble() * 400;
      y = 10;
      radius = Random().nextDouble() * 2 + 2;
      velocity = Random().nextDouble() * 4 + 2;
    }
  }
}

这里我生成了 100 个雪花,每帧重绘整个屏幕。您可以轻松地将雪花的数量更改为 1000 或更高,它仍然会 运行 非常顺利。在这里,我也没有尽可能多地使用设备屏幕尺寸,如您所见,有一些硬编码值,如 400 或 800。无论如何,希望这个演示能让你对 Flutter 的图形引擎有一些信心。 :)

这是另一个(较小的)示例,向您展示了在 Flutter 中使用 Canvas 和动画所需的一切。可能更容易理解:

import 'package:flutter/material.dart';

void main() {
  runApp(DemoWidget());
}

class DemoWidget extends StatefulWidget {
  @override
  _DemoWidgetState createState() => _DemoWidgetState();
}

class _DemoWidgetState extends State<DemoWidget>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    )..repeat(reverse: true);
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (_, __) => CustomPaint(
        painter: MyPainter(_controller.value),
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  final double value;

  MyPainter(this.value);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawCircle(
      Offset(size.width / 2, size.height / 2),
      value * size.shortestSide,
      Paint()..color = Colors.blue,
    );
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}