1. Flutter CustomPaint 基础概念解析
CustomPaint 是 Flutter 中一个强大的绘制组件,它允许开发者直接在画布(Canvas)上进行自定义绘制。与常规的 Widget 不同,CustomPaint 提供了更底层的绘图能力,适合实现各种复杂的自定义 UI 元素。
1.1 CustomPaint 的核心构成
CustomPaint 主要由三个核心部分组成:
- painter:背景绘制器,在子元素下方绘制
- foregroundPainter:前景绘制器,在子元素上方绘制
- child:子组件(可选)
实际绘制工作是通过继承 CustomPainter 类并实现 paint()和 shouldRepaint()方法完成的。paint()方法中我们可以获取到 Canvas 对象和 Size 对象,前者提供了各种绘图API,后者给出了绘制区域的大小。
1.2 CustomPainter 的生命周期
理解 CustomPainter 的生命周期对于性能优化至关重要:
- 首次创建时调用 paint()方法
- 当 shouldRepaint()返回 true 时重新绘制
- 当父组件重建时,如果 shouldRepaint()返回 false 则不会重绘
提示:合理实现 shouldRepaint()可以避免不必要的重绘,这对性能敏感的应用尤为重要。通常我们会比较新旧参数是否发生变化来决定是否需要重绘。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CustomPaint 的核心绘图能力
2.1 基本图形绘制
Canvas 提供了丰富的基础图形绘制方法:
- drawLine():绘制直线
- drawRect():绘制矩形
- drawCircle():绘制圆形
- drawPath():绘制自定义路径
- drawArc():绘制弧线
- drawOval():绘制椭圆
每个绘制方法都需要一个 Paint 对象来定义绘制样式。Paint 可以配置颜色、线宽、填充模式、抗锯齿等属性。
2.2 路径(Path)的高级使用
Path 类提供了构建复杂图形的能力:
dart复制final path = Path()
..moveTo(0, 0) // 移动到起点
..lineTo(100, 0) // 画直线
..quadraticBezierTo(150, 50, 100, 100) // 二次贝塞尔曲线
..cubicTo(80, 120, 20, 120, 0, 100) // 三次贝塞尔曲线
..close(); // 闭合路径
canvas.drawPath(path, paint);
路径操作还包括:
- addRect():添加矩形路径
- addOval():添加椭圆路径
- addArc():添加弧线路径
- addPolygon():添加多边形路径
2.3 变换与图层
Canvas 支持多种变换操作:
dart复制canvas.save(); // 保存当前状态
canvas.translate(100, 100); // 平移
canvas.rotate(pi/4); // 旋转45度
canvas.scale(2.0); // 放大2倍
// 绘制内容...
canvas.restore(); // 恢复之前保存的状态
图层操作可以通过 saveLayer()实现,它会在新的离屏缓冲区上绘制,适合实现特殊效果:
dart复制canvas.saveLayer(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..blendMode = BlendMode.multiply,
);
// 绘制内容...
canvas.restore();
3. 实战:实现自定义图表组件
3.1 柱状图实现
下面是一个简单的柱状图实现示例:
dart复制class BarChartPainter extends CustomPainter {
final List<double> data;
BarChartPainter(this.data);
@override
void paint(Canvas canvas, Size size) {
final barWidth = size.width / data.length * 0.8;
final maxValue = data.reduce(max);
final scale = size.height / maxValue;
for (var i = 0; i < data.length; i++) {
final barHeight = data[i] * scale;
final rect = Rect.fromLTWH(
i * size.width / data.length + barWidth * 0.1,
size.height - barHeight,
barWidth,
barHeight,
);
canvas.drawRect(
rect,
Paint()..color = Colors.blue.withOpacity(0.7),
);
}
}
@override
bool shouldRepaint(covariant BarChartPainter oldDelegate) {
return oldDelegate.data != data;
}
}
3.2 折线图实现
折线图的实现需要考虑数据点的连接和坐标系的绘制:
dart复制class LineChartPainter extends CustomPainter {
final List<double> data;
LineChartPainter(this.data);
@override
void paint(Canvas canvas, Size size) {
final path = Path();
final pointDistance = size.width / (data.length - 1);
final maxValue = data.reduce(max);
final scale = size.height / maxValue;
// 绘制折线
for (var i = 0; i < data.length; i++) {
final x = i * pointDistance;
final y = size.height - data[i] * scale;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
// 绘制数据点
canvas.drawCircle(
Offset(x, y),
4,
Paint()..color = Colors.red,
);
}
canvas.drawPath(
path,
Paint()
..color = Colors.blue
..strokeWidth = 2
..style = PaintingStyle.stroke,
);
}
@override
bool shouldRepaint(covariant LineChartPainter oldDelegate) {
return oldDelegate.data != data;
}
}
4. 高级技巧与性能优化
4.1 使用 RepaintBoundary 优化性能
当 CustomPaint 位于频繁更新的组件树中时,可以使用 RepaintBoundary 来隔离重绘范围:
dart复制RepaintBoundary(
child: CustomPaint(
painter: MyPainter(),
size: Size.infinite,
),
)
4.2 实现动画效果
结合 AnimationController 可以实现平滑的动画效果:
dart复制class AnimatedPainter extends CustomPainter {
final Animation<double> animation;
AnimatedPainter(this.animation) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
// 使用 animation.value 作为参数
final progress = animation.value;
// 绘制逻辑...
}
@override
bool shouldRepaint(covariant AnimatedPainter oldDelegate) {
return oldDelegate.animation != animation;
}
}
4.3 复杂图形的缓存策略
对于复杂的静态图形,可以考虑使用 PictureRecorder 进行预渲染:
dart复制final recorder = PictureRecorder();
final canvas = Canvas(recorder);
// 执行绘制操作...
final picture = recorder.endRecording();
// 在paint方法中直接绘制picture
canvas.drawPicture(picture);
5. 常见问题与解决方案
5.1 绘制模糊问题
当绘制的内容看起来模糊时,通常是因为没有考虑设备像素比:
dart复制@override
void paint(Canvas canvas, Size size) {
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
canvas.save();
canvas.scale(pixelRatio, pixelRatio);
// 绘制逻辑...
canvas.restore();
}
5.2 手势交互实现
要在 CustomPaint 上实现手势交互,可以使用 GestureDetector 包裹:
dart复制GestureDetector(
onTapDown: (details) {
final localPosition = details.localPosition;
// 判断点击位置是否在特定图形内
},
child: CustomPaint(
painter: MyInteractivePainter(),
),
)
5.3 内存优化技巧
对于频繁更新的 CustomPaint:
- 避免在 paint() 方法中创建新对象
- 尽可能复用 Paint 对象
- 对于静态部分使用缓存图片
- 合理设置 shouldRepaint() 逻辑
6. 实际应用案例
6.1 自定义进度条
实现一个带有圆角、渐变色和动画效果的进度条:
dart复制class ProgressPainter extends CustomPainter {
final double progress;
ProgressPainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
final backgroundPaint = Paint()
..color = Colors.grey[300]!;
final foregroundPaint = Paint()
..shader = LinearGradient(
colors: [Colors.blue, Colors.lightBlue],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
// 绘制背景
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
Radius.circular(10),
),
backgroundPaint,
);
// 绘制进度
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width * progress, size.height),
Radius.circular(10),
),
foregroundPaint,
);
}
@override
bool shouldRepaint(covariant ProgressPainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
6.2 不规则形状按钮
实现一个底部不规则的按钮(参考热词中的需求):
dart复制class IrregularButtonPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.blue;
final path = Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width, size.height - 20)
..quadraticBezierTo(
size.width * 0.75, size.height,
size.width * 0.5, size.height - 10,
)
..quadraticBezierTo(
size.width * 0.25, size.height,
0, size.height - 20,
)
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant IrregularButtonPainter oldDelegate) => false;
}
使用方式:
dart复制CustomPaint(
painter: IrregularButtonPainter(),
child: TextButton(
onPressed: () {},
child: Text('不规则按钮'),
),
)
6.3 图形验证码实现
实现滑动完成验证的图形验证码(参考热词中的需求):
dart复制class PuzzlePainter extends CustomPainter {
final Offset puzzlePosition;
final bool isVerified;
PuzzlePainter(this.puzzlePosition, this.isVerified);
@override
void paint(Canvas canvas, Size size) {
// 绘制背景
canvas.drawColor(Colors.grey[200]!, BlendMode.srcOver);
// 绘制拼图轮廓
final puzzlePath = Path()
..addRRect(RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
Radius.circular(8),
));
// 绘制缺口
final holePath = Path()
..addOval(Rect.fromCircle(
center: puzzlePosition,
radius: 20,
));
// 使用差值路径绘制拼图
final resultPath = Path.combine(
PathOperation.difference,
puzzlePath,
holePath,
);
canvas.drawPath(
resultPath,
Paint()
..color = isVerified ? Colors.green : Colors.blue
..style = PaintingStyle.fill,
);
}
@override
bool shouldRepaint(covariant PuzzlePainter oldDelegate) {
return oldDelegate.puzzlePosition != puzzlePosition ||
oldDelegate.isVerified != isVerified;
}
}
7. 与Flutter其他特性的结合
7.1 与Provider状态管理结合
CustomPaint 可以与 Provider 配合实现响应式绘图:
dart复制class DataPainter extends CustomPainter {
final DataModel data;
DataPainter(this.data);
@override
void paint(Canvas canvas, Size size) {
// 使用data中的数据绘图
}
@override
bool shouldRepaint(covariant DataPainter oldDelegate) {
return oldDelegate.data != data;
}
}
// 使用方式
Consumer<DataModel>(
builder: (context, data, child) {
return CustomPaint(
painter: DataPainter(data),
);
},
)
7.2 与Flutter动画系统结合
利用 Flutter 的动画系统创建流畅的绘图动画:
dart复制class AnimatedCirclePainter extends CustomPainter {
final double radius;
AnimatedCirclePainter(this.radius);
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(
size.center(Offset.zero),
radius,
Paint()..color = Colors.blue,
);
}
@override
bool shouldRepaint(covariant AnimatedCirclePainter oldDelegate) {
return oldDelegate.radius != radius;
}
}
// 使用方式
final animation = Tween(begin: 10.0, end: 100.0).animate(controller);
AnimatedBuilder(
animation: animation,
builder: (context, child) {
return CustomPaint(
painter: AnimatedCirclePainter(animation.value),
);
},
)
7.3 与Flutter平台交互
CustomPaint 绘制的图形可以通过 Platform Channel 与原生平台交互:
dart复制// 将绘制内容导出为图片
Future<Uint8List> capturePng(GlobalKey key) async {
final boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
final image = await boundary?.toImage();
final byteData = await image?.toByteData(format: ImageByteFormat.png);
return byteData?.buffer.asUint8List() ?? Uint8List(0);
}
// 然后通过MethodChannel发送到原生平台
MethodChannel('example.com/channel').invokeMethod('saveImage', await capturePng(_key));
