Flutter CustomPainter:用代码作画的艺术
·
Flutter CustomPainter:用代码作画的艺术
引言
Flutter 的 CustomPainter 是一个强大的工具,它允许开发者直接在画布上绘制自定义图形。无论是创建复杂的数据可视化图表、自定义动画效果,还是独特的 UI 组件,CustomPainter 都能帮助你实现创意。本文将深入探讨 CustomPainter 的核心概念、使用方法和实际应用。
一、CustomPainter 核心概念
1.1 什么是 CustomPainter
CustomPainter 是 Flutter 中用于自定义绘制的核心类,它允许你在 Canvas 上绘制各种图形、路径、文本和图像。
1.2 基本结构
class MyCustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 在这里进行绘制操作
// canvas: 画布对象
// size: 绘制区域的大小
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// 返回 true 表示需要重新绘制
// 返回 false 表示不需要重新绘制
return false;
}
}
1.3 Paint 对象
Paint 对象定义了绘制的样式:
final paint = Paint()
..color = Colors.blue
..strokeWidth = 2.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.fill; // 或 PaintingStyle.stroke
二、基本图形绘制
2.1 绘制直线
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black
..strokeWidth = 2;
// 绘制直线
canvas.drawLine(
Offset(0, 0),
Offset(size.width, size.height),
paint,
);
}
2.2 绘制矩形
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
// 绘制矩形
canvas.drawRect(
Rect.fromLTWH(
20,
20,
size.width - 40,
size.height - 40,
),
paint,
);
// 绘制圆角矩形
final roundedPaint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(40, 40, size.width - 80, size.height - 80),
const Radius.circular(16),
),
roundedPaint,
);
}
2.3 绘制圆形和椭圆
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 4;
// 绘制圆形
final circlePaint = Paint()
..color = Colors.green;
canvas.drawCircle(center, radius, circlePaint);
// 绘制椭圆
final ovalPaint = Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawOval(
Rect.fromCenter(
center: center,
width: size.width / 2,
height: size.height / 3,
),
ovalPaint,
);
}
2.4 绘制路径
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.purple
..strokeWidth = 3
..style = PaintingStyle.stroke;
final path = Path();
// 移动到起点
path.moveTo(0, size.height / 2);
// 绘制曲线
path.quadraticBezierTo(
size.width / 4,
size.height / 4,
size.width / 2,
size.height / 2,
);
path.quadraticBezierTo(
size.width * 3 / 4,
size.height * 3 / 4,
size.width,
size.height / 2,
);
canvas.drawPath(path, paint);
}
三、高级绘制技巧
3.1 绘制渐变
@override
void paint(Canvas canvas, Size size) {
// 线性渐变
final linearGradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.red, Colors.blue, Colors.green],
stops: const [0.0, 0.5, 1.0],
);
final paint = Paint()
..shader = linearGradient.createShader(
Rect.fromLTWH(0, 0, size.width, size.height),
);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
// 径向渐变
final radialGradient = RadialGradient(
center: Alignment.center,
radius: min(size.width, size.height) / 2,
colors: [Colors.yellow, Colors.orange, Colors.red],
);
final radialPaint = Paint()
..shader = radialGradient.createShader(
Rect.fromLTWH(0, 0, size.width, size.height),
);
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
min(size.width, size.height) / 4,
radialPaint,
);
}
3.2 绘制文本
@override
void paint(Canvas canvas, Size size) {
final textStyle = TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.bold,
);
final textSpan = TextSpan(
text: 'Hello CustomPainter!',
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
// 居中绘制文本
final x = (size.width - textPainter.width) / 2;
final y = (size.height - textPainter.height) / 2;
textPainter.paint(canvas, Offset(x, y));
}
3.3 绘制图像
class ImagePainter extends CustomPainter {
final Image image;
ImagePainter({required this.image});
@override
void paint(Canvas canvas, Size size) {
// 绘制图像
canvas.drawImage(
image,
Offset(0, 0),
Paint(),
);
// 绘制缩放后的图像
final rect = Rect.fromLTWH(
0,
0,
size.width,
size.height,
);
canvas.drawImageRect(
image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
rect,
Paint(),
);
}
@override
bool shouldRepaint(covariant ImagePainter oldDelegate) {
return image != oldDelegate.image;
}
}
四、实战案例:绘制进度环
4.1 需求分析
创建一个自定义进度环组件,支持:
- 自定义进度值
- 自定义颜色和宽度
- 动画效果
4.2 实现代码
class ProgressRing extends StatefulWidget {
final double progress;
final double strokeWidth;
final Color color;
final Color backgroundColor;
const ProgressRing({
super.key,
required this.progress,
this.strokeWidth = 8.0,
this.color = Colors.blue,
this.backgroundColor = Colors.grey,
});
@override
State<ProgressRing> createState() => _ProgressRingState();
}
class _ProgressRingState extends State<ProgressRing> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(
begin: 0,
end: widget.progress,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.forward();
}
@override
void didUpdateWidget(covariant ProgressRing oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.progress != oldWidget.progress) {
_animation = Tween<double>(
begin: _animation.value,
end: widget.progress,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_controller.reset();
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: _ProgressRingPainter(
progress: _animation.value,
strokeWidth: widget.strokeWidth,
color: widget.color,
backgroundColor: widget.backgroundColor,
),
child: child,
);
},
child: Center(
child: Text(
'${(_animation.value * 100).round()}%',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
class _ProgressRingPainter extends CustomPainter {
final double progress;
final double strokeWidth;
final Color color;
final Color backgroundColor;
_ProgressRingPainter({
required this.progress,
required this.strokeWidth,
required this.color,
required this.backgroundColor,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2 - strokeWidth / 2;
// 绘制背景环
final backgroundPaint = Paint()
..color = backgroundColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, backgroundPaint);
// 绘制进度环
final progressPaint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// 计算弧长
final startAngle = -math.pi / 2; // 从顶部开始
final sweepAngle = 2 * math.pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
progressPaint,
);
}
@override
bool shouldRepaint(covariant _ProgressRingPainter oldDelegate) {
return progress != oldDelegate.progress ||
strokeWidth != oldDelegate.strokeWidth ||
color != oldDelegate.color ||
backgroundColor != oldDelegate.backgroundColor;
}
}
// 使用示例
ProgressRing(
progress: 0.75,
strokeWidth: 12.0,
color: Colors.green,
)
五、实战案例:绘制数据可视化图表
5.1 实现代码
class BarChartPainter extends CustomPainter {
final List<double> data;
final List<Color> colors;
final String title;
BarChartPainter({
required this.data,
required this.colors,
required this.title,
});
@override
void paint(Canvas canvas, Size size) {
final padding = 40.0;
final chartWidth = size.width - padding * 2;
final chartHeight = size.height - padding * 2 - 30;
// 绘制标题
final titleStyle = TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold,
);
final titleSpan = TextSpan(text: title, style: titleStyle);
final titlePainter = TextPainter(
text: titleSpan,
textDirection: TextDirection.ltr,
);
titlePainter.layout();
titlePainter.paint(
canvas,
Offset((size.width - titlePainter.width) / 2, 10),
);
// 找到最大值
final maxValue = data.reduce((a, b) => a > b ? a : b);
// 计算柱状图宽度和间距
final barWidth = chartWidth / (data.length * 2 - 1);
final spacing = barWidth;
// 绘制坐标轴
final axisPaint = Paint()
..color = Colors.grey
..strokeWidth = 2;
// Y轴
canvas.drawLine(
Offset(padding, padding),
Offset(padding, size.height - padding),
axisPaint,
);
// X轴
canvas.drawLine(
Offset(padding, size.height - padding),
Offset(size.width - padding, size.height - padding),
axisPaint,
);
// 绘制刻度和标签
for (int i = 0; i <= 4; i++) {
final value = maxValue * i / 4;
final y = size.height - padding - (chartHeight * i / 4);
// 刻度线
canvas.drawLine(
Offset(padding - 5, y),
Offset(padding, y),
axisPaint,
);
// 标签
final labelStyle = TextStyle(
color: Colors.grey,
fontSize: 10,
);
final labelSpan = TextSpan(text: '${value.round()}', style: labelStyle);
final labelPainter = TextPainter(
text: labelSpan,
textDirection: TextDirection.ltr,
);
labelPainter.layout();
labelPainter.paint(
canvas,
Offset(padding - 30 - labelPainter.width, y - labelPainter.height / 2),
);
}
// 绘制柱状图
for (int i = 0; i < data.length; i++) {
final barHeight = (data[i] / maxValue) * chartHeight;
final x = padding + i * (barWidth + spacing);
final y = size.height - padding - barHeight;
final barPaint = Paint()
..color = colors[i % colors.length]
..style = PaintingStyle.fill;
// 绘制柱子
canvas.drawRect(
Rect.fromLTWH(x, y, barWidth, barHeight),
barPaint,
);
// 添加阴影效果
final shadowPaint = Paint()
..color = Colors.black.withOpacity(0.1)
..style = PaintingStyle.fill;
canvas.drawRect(
Rect.fromLTWH(x + 2, y + 2, barWidth, barHeight),
shadowPaint,
);
}
}
@override
bool shouldRepaint(covariant BarChartPainter oldDelegate) {
return data != oldDelegate.data ||
colors != oldDelegate.colors ||
title != oldDelegate.title;
}
}
// 使用示例
CustomPaint(
painter: BarChartPainter(
data: [45, 78, 32, 91, 56, 83],
colors: [Colors.blue, Colors.green, Colors.orange, Colors.red, Colors.purple, Colors.pink],
title: '月度销售额',
),
size: const Size(300, 200),
)
六、实战案例:绘制动画波浪效果
6.1 实现代码
class WavePainter extends CustomPainter {
final Animation<double> animation;
WavePainter({required this.animation});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue.withOpacity(0.5)
..style = PaintingStyle.fill;
final path = Path();
// 波浪参数
final waveHeight = 20.0;
final waveWidth = 50.0;
final offset = animation.value * waveWidth;
// 从左下角开始
path.moveTo(0, size.height);
// 绘制波浪
for (double x = -waveWidth; x < size.width + waveWidth; x += waveWidth) {
path.quadraticBezierTo(
x + waveWidth / 4 + offset,
size.height / 2 - waveHeight,
x + waveWidth / 2 + offset,
size.height / 2,
);
path.quadraticBezierTo(
x + waveWidth * 3 / 4 + offset,
size.height / 2 + waveHeight,
x + waveWidth + offset,
size.height / 2,
);
}
// 闭合路径
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant WavePainter oldDelegate) {
return animation != oldDelegate.animation;
}
}
// 使用示例
class WaveAnimation extends StatefulWidget {
@override
State<WaveAnimation> createState() => _WaveAnimationState();
}
class _WaveAnimationState extends State<WaveAnimation> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat();
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WavePainter(animation: _animation),
size: const Size(300, 200),
);
}
}
七、性能优化建议
7.1 使用 RepaintBoundary
RepaintBoundary(
child: CustomPaint(
painter: MyPainter(),
),
);
7.2 缓存绘制结果
class CachedPainter extends CustomPainter {
final Picture? _cachedPicture;
CachedPainter(this._cachedPicture);
@override
void paint(Canvas canvas, Size size) {
if (_cachedPicture != null) {
canvas.drawPicture(_cachedPicture!);
return;
}
// 绘制逻辑
// ...
// 缓存结果
final recorder = PictureRecorder();
final recordCanvas = Canvas(recorder);
// 在 recordCanvas 上绘制
// ...
}
@override
bool shouldRepaint(covariant CachedPainter oldDelegate) {
return _cachedPicture == null;
}
}
7.3 避免不必要的绘制
@override
bool shouldRepaint(covariant MyPainter oldDelegate) {
// 只有在数据变化时才重新绘制
return data != oldDelegate.data;
}
八、总结与展望
8.1 CustomPainter 的价值
CustomPainter 为 Flutter 开发者提供了无限的创意空间:
- 自定义图形:创建独特的 UI 组件
- 数据可视化:绘制图表、仪表盘等
- 动画效果:实现复杂的动画效果
- 性能优化:精细控制绘制过程
8.2 最佳实践建议
- 分离绘制逻辑:将复杂的绘制逻辑分解为多个方法
- 使用缓存:对于静态内容使用 Picture 缓存
- 性能监控:使用 Flutter DevTools 监控绘制性能
- 代码组织:将自定义绘制器放在单独的文件中
8.3 未来发展趋势
随着 Flutter 的发展,CustomPainter 也在不断进化:
- 更好的性能优化工具
- 更丰富的绘制 API
- 与其他框架的更好集成
参考资料:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)