Flutter CustomPaint 自定义绘制实战:从基础到五子棋
在 Flutter 开发中,当系统自带组件无法满足复杂的 UI 设计需求时,CustomPaint 提供了强大的自定义绘制能力。本文深入解析其核心参数与层级逻辑,并通过绘制基础图形及完整五子棋案例,展示 Canvas 的高效使用方法与性能优化技巧。
认识 CustomPaint 架构
CustomPaint 是 Flutter 中用于自由绘制的 Widget,其绘制逻辑以当前 Canvas(画布)的左上角为原点。它的结构主要包含三个关键参数:
child:放置在画布中间的子组件。painter:负责背景层的绘制内容。foregroundPainter:负责前景层的绘制内容。
这三者的渲染顺序非常明确:painter 最先执行,接着渲染 child 组件,最后执行 foregroundPainter。这意味着如果前景画笔与背景画笔在同一位置绘制,前景会覆盖背景。CustomPainter 类中的 paint 方法负责具体的绘图指令,接收 canvas 和 size 两个参数。
图层交互测试案例
为了直观理解各层级的关系,可以通过以下代码创建一个组合示例。在此案例中,我们定义了红色背景的 child,蓝色背景的 painter,以及绿色前景的 foregroundPainter。
import 'package:flutter/material.dart';
class MyPaint extends StatelessWidget {
const MyPaint({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('CustomPaint Test')),
body: Center(
child: CustomPaint(
size: Size(300, 300), // 指定画布尺寸
painter: MyBackgroundPainter(),
foregroundPainter: MyForegroundPainter(),
child: Container(
height: 80,
width: 80,
color: Colors.red,
alignment: Alignment.center,
child: Text('Child', style: TextStyle(color: Colors.white)),
),
),
),
);
}
}
// 背景绘制器:绘制蓝色圆形和线条
class MyBackgroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 4;
// 绘制大圆
canvas.drawCircle(Offset(100, 100), 60, paint);
// 绘制线条
canvas.drawLine(
Offset(50, 50),
Offset(200, 200),
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// 前景绘制器:绘制绿色圆形,将覆盖 Child 的一部分
class MyForegroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.green.withOpacity(0.8);
// 绘制小圆,位于 Child 上方
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 40, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
运行上述代码可见:底层为蓝色区域,中间为红色 Container,顶层覆盖了半透明的绿色圆圈。这种分层机制允许我们在不同层级添加阴影、装饰或交互反馈。
常用绘制方法速查
Canvas 类提供了丰富的基础图形绘制接口,以下是核心方法的使用要点:
drawCircle:绘制圆形,需指定中心点Offset、半径和Paint属性。drawRect:绘制矩形,通常配合Offset.zero & size填充整个画布。drawLine:绘制线段,起点和终点均为Offset。drawPath:绘制复杂路径,适合不规则图形。drawImage:绘制图片资源。drawText/drawParagraph:绘制文本(新版 Flutter 推荐使用 Paragraph 构建器)。
第 1 步:绘制基础点与 Line/PointMode
在使用 drawPoints 时,PointMode 决定了点的连接方式:
PointMode.points:仅绘制独立的点。PointMode.lines:两两成对绘制线段(奇数个点时忽略最后一个)。PointMode.polygon:将所有点首尾相连连成一条折线。
class PointsDemo extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.red
..strokeWidth = 15
..strokeCap = StrokeCap.round;
final points = [
Offset(0, 0),
Offset(size.width / 2, size.height / 2),
Offset(size.width, size.height),
];
// 使用 points 模式绘制独立红点
canvas.drawPoints(PointMode.points, points, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
第 2 步:组合复杂形状与 Path
对于弧形、椭圆和圆角矩形等形状,通常需要结合 PaintingStyle.fill(填充)和 PaintingStyle.stroke(描边)来调整视觉效果。
class ShapesDemo extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final strokePaint = Paint()
..color = Colors.red
..strokeWidth = 4
..style = PaintingStyle.stroke;
final fillPaint = Paint()
..color = Colors.blue.withOpacity(0.3)
..style = PaintingStyle.fill;
// 绘制直线
canvas.drawLine(Offset(20, 20), Offset(size.width - 20, 20), strokePaint);
// 绘制闭合路径(三角形)
final path = Path()
..moveTo(20, size.height - 20)
..lineTo(size.width / 2, 20)
..lineTo(size.width - 20, size.height - 20)
..close();
canvas.drawPath(path, fillPaint);
// 绘制椭圆
canvas.drawOval(Rect.fromLTWH(20, 50, size.width - 40, 100), strokePaint);
// 绘制圆弧 (90度)
canvas.drawArc(
Rect.fromLTWH(20, 170, 100, 100),
0,
pi / 2,
false,
strokePaint,
);
// 绘制圆角矩形
canvas.drawRRect(
RRect.fromLTRBR(
20, 170, 120, 270,
Radius.circular(10),
),
fillPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
实战:绘制五子棋棋盘
下面是一个完整的五子棋棋盘绘制示例。该示例展示了如何计算网格间距、绘制背景色、网格线以及放置黑白棋子。同时演示了如何通过 shouldRepaint 控制重绘逻辑。
class GobangBoard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
size: Size(300, 300),
painter: GobangPainter(),
),
);
}
}
class GobangPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 计算格子宽度和高度
final cellSize = size.width / 15;
final cellHeight = size.height / 15;
// 1. 绘制棋盘背景 (纸黄色)
var bgPaint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill
..color = Color(0xFFCDB175);
canvas.drawRect(Offset.zero & size, bgPaint);
// 2. 绘制网格线
var linePaint = Paint()
..style = PaintingStyle.stroke
..color = Colors.black87
..strokeWidth = 1.0;
for (int i = 0; i <= 15; ++i) {
// 横线
canvas.drawLine(
Offset(0, i * cellHeight),
Offset(size.width, i * cellHeight),
linePaint,
);
// 竖线
canvas.drawLine(
Offset(i * cellSize, 0),
Offset(i * cellSize, size.height),
linePaint,
);
}
// 3. 绘制中心黑子
var piecePaint = Paint()
..style = PaintingStyle.fill
..color = Colors.black;
canvas.drawCircle(
Offset(size.width / 2 - cellSize/2, size.height / 2 - cellHeight/2),
cellSize / 2 - 2,
piecePaint,
);
// 4. 绘制白子
piecePaint.color = Colors.white;
canvas.drawCircle(
Offset(size.width / 2 + cellSize/2, size.height / 2 - cellHeight/2),
cellSize / 2 - 2,
piecePaint,
);
}
@override
// 由于本例静态绘制,返回 true 确保首次加载必绘;实际项目中应根据状态变化决定
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
性能优化:避免无谓的重绘
自定义绘制在 Flutter 中属于高开销操作。为了提升 UI 流畅度,务必关注 shouldRepaint 回调的使用:
- 正确利用 shouldRepaint:
每次 Widget 树重新 Build 时,Flutter 都会调用此方法判断是否需要重绘。- 如果绘制内容与外部状态无关,直接返回
false。 - 如果依赖外部数据(如动画进度、用户输入),应在该方法中比较新旧状态,仅在数据变化时返回
true。
- 如果绘制内容与外部状态无关,直接返回
- 分层绘制策略:
如五子棋示例中,棋盘背景是不变的,只有棋子位置会变。如果将两者合并在一个CustomPaint中,落子时将导致整个背景被重绘。
优化方案:将不变的背景封装为单独的CustomPainter(返回false),将变化的棋子单独封装。这样只需要重绘棋子部分,大幅降低渲染压力。
总结
CustomPaint 赋予了 Flutter 开发者完全掌控 2D 图形渲染的能力。核心在于理解 painter(背景)、child(中层)和 foregroundPainter(前景)的叠加顺序,并熟练使用 Canvas 的几何绘制 API。在实际工程中,通过合理拆分图层和优化 shouldRepaint 逻辑,可以构建出既美观又高效的自定义 UI 组件。
以上就是 Flutter CustomPaint 自定义绘画示例详解的详细内容,更多关于 Flutter CustomPaint 自定义绘制性能优化的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









