6

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

我知道当画布大小发生变化时,一切都需要重新绘制。

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

2 回答 2

10

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

查看您发布的代码,某些区域还有改进的余地,但尝试保留画布的某些部分不应该是其中之一。

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

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

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

在这里,我假设您希望在0and之间有一个随机小数size.height,如果是这样,您可以简单地编写_rng.nextDouble() * size.height,而不是将 double 转换为 int 并再次返回,并且(可能是无意的)在此过程中对其进行舍入。但是这些东西带来的性能提升是微不足道的。

想一想,如果 3D 视频游戏可以在手机上流畅运行,并且每一帧都与前一帧大不相同,那么您的动画应该可以流畅运行,而不必担心手动清除部分画布。尝试手动优化画布可能会导致性能损失。

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

例如,这是我使用 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 和 Animations 所需的一切。可能更容易理解:

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;
}
于 2020-10-16T21:21:04.193 回答
1

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

用于绘制图像,您可以使用canvas.drawImage上述评论中 pskink 提到的方法。

但我推荐的解决方案是包装CustomPaintwithRenderRepaint以将该小部件转换为图像。有关详细信息,请参阅 从 Widget 或 Canvas 创建原始图像和(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
        )
      )
    );
  }
}

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

于 2020-10-14T14:26:44.223 回答