我有一个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;
}
}
答案 0 :(得分:2)
重新绘制整个画布(甚至在每一帧上)都是完全有效的。尝试重用前一帧通常不会更有效。
看看您发布的代码,在某些方面尚有待改进之处,但是尝试保留画布的某些部分不应是其中之一。
您遇到的真正性能问题是每隔50毫秒从ValueNotifier
事件中反复更改Timer.periodic
。处理每帧上的重画的一种更好的方法是将AnimatedBuilder
与vsync
一起使用,因此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制作的一个小演示:
完整源代码:
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;
}
}
}
答案 1 :(得分:1)
当前唯一可用的解决方案是将进度捕获为图像,然后绘制图像,而不是执行整个画布代码。
要绘制图像,您可以使用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.drawImage
与CustomPaint
进行包装,以将该小部件转换为图像。有关详细信息,请参阅
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
)
)
);
}
}
可以更好地渲染图像方式。