Flutter-在CustomPainter中重用先前绘制的画布

问题描述

我有一个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,_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;
  }
}

解决方法

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

看看您发布的代码,在某些方面尚有待改进之处,但是尝试保留画布的某些部分不应是其中之一。

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

在考虑保留画布的某些部分之前,还有其他值得优化的领域。例如,仅简要查看您的代码,您就有以下代码行:

paint

在这里,我假设您想在_currentY = _rng.nextInt(size.height.toInt()).toDouble(); 0之间有一个随机小数,如果是这样,您可以简单地写size.height,而不是将double强制转换为int并再次返回,并(可能是无意间)在此过程中对其进行了四舍五入。但是这些东西带来的性能提升是微不足道的。

考虑一下,如果3D视频游戏可以在手机上流畅运行,并且每帧都与上一帧有很大不同,则动画应该流畅运行,而不必担心手动清除画布的某些部分。尝试手动优化画布可能会导致性能下降。

因此,您真正应该关注的是,以_rng.nextDouble() * size.height代替AnimatedBuilder来触发项目中的画布重绘。

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

demo snowman

完整源代码:

Timer

在这里,我正在生成100个雪花,每帧重绘整个屏幕。您可以轻松地将雪花数更改为1000或更高,并且仍然可以非常顺畅地运行。如您所见,在这里,我也没有充分使用设备的屏幕尺寸,正如您所看到的,有一些硬编码的值,例如400或800。无论如何,希望该演示可以使您对Flutter的图形引擎有所信心。 :)

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

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

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

要绘制图像,您可以使用pskink在上面的注释中提到的function jba_enable_editor_fixedToolbar_by_default() { $script = "window.onload = function() { const isfixedToolbar = wp.data.select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ); if ( !isfixedToolbar ) { wp.data.dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' ); } }"; wp_add_inline_script( 'wp-blocks',$script ); } add_action( 'enqueue_block_editor_assets','jba_enable_editor_fixedToolbar_by_default' );

但我建议的解决方案是将canvas.drawImageCustomPaint进行包装,以将该小部件转换为图像。有关详细信息,请参阅 Creating raw image from Widget or Canvas和(https://medium.com/flutter-community/export-your-widget-to-image-with-flutter-dc7ecfa6bafb为简要实现),并有条件检查您是否是首次构建。

RenderRepaint

这样,图像就不会成为您自定义画家的一部分,我告诉您,我尝试使用画布绘制图像,但是效率不高,由flutter提供的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 ) ) ); } } 可以更好地渲染图像方式。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...