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
。处理每一帧重绘的更好方法是使用 AnimatedBuilder
和 vsync
,因此将在每一帧调用 CustomPainter
的 paint
方法。如果您熟悉的话,这类似于 Web 浏览器世界中的 Window.requestAnimationFrame
。如果您熟悉计算机图形的工作原理,这里的 vsync
代表“垂直同步”。本质上,您的 paint
方法将在具有 60 Hz 屏幕的设备上每秒调用 60 次,并且它将在 120 Hz 屏幕上每秒绘制 120 次。这是在不同类型的设备上实现流畅动画的正确且可扩展的方式。
在考虑保留部分 canvas 之前,还有其他值得优化的地方。例如,简单地看一下你的代码,你有这一行:
_currentY = _rng.nextInt(size.height.toInt()).toDouble();
在这里我假设你想在 0
和 size.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;
}
我有一个 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
以将该小部件转换为图像。详情请参阅
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
。处理每一帧重绘的更好方法是使用 AnimatedBuilder
和 vsync
,因此将在每一帧调用 CustomPainter
的 paint
方法。如果您熟悉的话,这类似于 Web 浏览器世界中的 Window.requestAnimationFrame
。如果您熟悉计算机图形的工作原理,这里的 vsync
代表“垂直同步”。本质上,您的 paint
方法将在具有 60 Hz 屏幕的设备上每秒调用 60 次,并且它将在 120 Hz 屏幕上每秒绘制 120 次。这是在不同类型的设备上实现流畅动画的正确且可扩展的方式。
在考虑保留部分 canvas 之前,还有其他值得优化的地方。例如,简单地看一下你的代码,你有这一行:
_currentY = _rng.nextInt(size.height.toInt()).toDouble();
在这里我假设你想在 0
和 size.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;
}