Flutter 画笔(Paint)、drawRect(绘制矩形)、PaintingStyle
'# Flutter 画笔(Paint)、drawRect(绘制矩形)、PaintingStyle
一、背景与问题
在 Flutter 开发中,Canvas 是构建自定义 UI 的核心工具。当我们需要绘制图形时,必须通过 Paint 对象定义绘制样式,通过 Canvas 接口进行实际绘制。其中 drawRect 是最基础的绘制操作之一,而 PaintingStyle 则决定了绘制的填充方式。
在实际开发中,开发者常常遇到以下问题:
- 为什么绘制的矩形边缘出现锯齿?
- 为什么
PaintingStyle.fill和PaintingStyle.stroke的效果差异如此显著? - 如何在不牺牲性能的前提下实现复杂的绘制效果?
这些问题的根源在于对 Paint 和 PaintingStyle 工作原理的深入理解不足。本文将从底层原理出发,结合实际案例,探讨这些核心概念的使用技巧。
二、基本原理
1. Paint 对象的结构
Paint 对象包含以下关键属性:
Paint paint = Paint()
..color = Colors.blue
..strokeWidth = 2.0
..style = PaintingStyle.fill;color:定义颜色,支持Color类型strokeWidth:定义笔触宽度(仅对PaintingStyle.stroke有效)style:定义绘制样式(fill/stroke/fillAndStroke)isAntiAlias:是否启用抗锯齿(默认 true)maskFilter:定义模糊效果(如MaskFilter.blur())
2. PaintingStyle 的本质区别
PaintingStyle 的核心差异在于:
fill:填充整个区域(默认行为)stroke:仅绘制边框(需要设置strokeWidth)fillAndStroke:同时填充和绘制边框
当使用 stroke 模式时,绘制的图形会比实际尺寸大 strokeWidth/2 的范围,这是由于 Canvas 的坐标系特性决定的。
三、环境准备
确保你的开发环境满足以下要求:
- Flutter SDK 2.10+
- Dart 3.0+
- Android Studio 或 VS Code
- 安装 Flutter 插件(如 flutter_launcher_icons)
创建新项目时建议使用以下结构:
lib/
├── custom_paint/
│ ├── custom_paint.dart
│ └── main.dart
├── utils/
│ └── paint_utils.dart
└── widgets/
└── custom_widgets.dart四、核心实现
1. 基础绘制示例
import 'package:flutter/material.dart';
class BasicPaint extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: MyPainter(),
size: Size.infinite,
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.orange
..style = PaintingStyle.fill;
canvas.drawRect(
Rect.fromLTWH(50, 50, 200, 100),
paint
);
}
@override
bool shouldRepaint(covariant MyPainter oldDelegate) => false;
}关键代码解释:
CustomPaint是 Flutter 提供的绘制容器CustomPainter接口需要实现paint和shouldRepaint方法Rect.fromLTWH定义矩形的左上角坐标和尺寸Paint对象的style属性决定了填充方式
2. 不同 PaintingStyle 的效果对比
class StyleComparison extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: StylePainter(),
size: Size.infinite,
);
}
}
class StylePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.green
..strokeWidth = 4.0
..isAntiAlias = false;
// 填充模式
canvas.drawRect(
Rect.fromLTWH(50, 50, 200, 100),
paint
);
// 边框模式
paint.style = PaintingStyle.stroke;
canvas.drawRect(
Rect.fromLTWH(150, 150, 200, 100),
paint
);
// 双重模式
paint.style = PaintingStyle.fillAndStroke;
canvas.drawRect(
Rect.fromLTWH(250, 250, 200, 100),
paint
);
}
@override
bool shouldRepaint(covariant StylePainter oldDelegate) => false;
}关键点分析:
- 使用
PaintingStyle.stroke时,绘制的矩形会比实际尺寸大strokeWidth/2的范围 isAntiAlias设置为 false 可以禁用抗锯齿,适用于需要锐利边缘的场景fillAndStroke模式会同时进行填充和边框绘制
3. 高级绘制技巧
class AdvancedPaint extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: AdvancedPainter(),
size: Size.infinite,
);
}
}
class AdvancedPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.purple
..strokeWidth = 8.0
..style = PaintingStyle.stroke
..maskFilter = MaskFilter.blur(BlurStyle.normal, 10.0);
// 绘制带模糊效果的矩形
canvas.drawRect(
Rect.fromLTWH(50, 50, 200, 100),
paint
);
}
@override
bool shouldRepaint(covariant AdvancedPainter oldDelegate) => false;
}关键技巧:
- 使用
MaskFilter.blur()实现模糊效果 - 通过
strokeWidth控制模糊程度 - 可以组合多种
Paint属性实现复杂效果
五、完整案例
1. 自定义进度条组件
class CustomProgressBar extends StatelessWidget {
final double value;
const CustomProgressBar({super.key, required this.value});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: ProgressBarPainter(value: value),
size: const Size(200, 30),
);
}
}
class ProgressBarPainter extends CustomPainter {
final double value;
ProgressBarPainter({required this.value});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill
..isAntiAlias = true;
final rect = Rect.fromLTWH(0, 0, size.width * value, size.height);
// 绘制背景
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()
..color = Colors.grey[300]
..style = PaintingStyle.fill
);
// 绘制进度条
canvas.drawRect(rect, paint);
// 绘制文字
final textPainter = TextPainter(
text: TextSpan(text: "${(value * 100).toInt()}%", style: const TextStyle(color: Colors.white)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: size.width);
textPainter.paint(canvas, Offset((size.width - textPainter.width) / 2, 10));
}
@override
bool shouldRepaint(covariant ProgressBarPainter oldDelegate) => value != oldDelegate.value;
}完整案例说明:
- 使用
CustomPaint创建自定义进度条 - 通过
value控制进度条长度 - 使用
TextPainter实现文字渲染 - 通过
Paint的isAntiAlias属性保证文字清晰度
六、源码解析
1. Canvas 绘制流程
当调用 canvas.drawRect() 时,Flutter 会:
- 创建
Path对象(矩形路径) - 应用
Paint属性(颜色、样式、模糊等) - 调用
Canvas的drawRect方法 - 通过
Skia引擎进行实际绘制
2. PaintingStyle 的实现差异
在 Skia 源码中,PaintingStyle 的实现差异体现在:
fill模式调用drawRect方法stroke模式调用drawRect但会调整绘制区域fillAndStroke模式会同时调用两次绘制
七、进阶使用
1. 动态绘制效果
class DynamicPainter extends CustomPainter {
final AnimationController controller;
final Animation<double> animation;
DynamicPainter({
required this.controller,
required this.animation,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.green
..style = PaintingStyle.fill
..isAntiAlias = true;
final rect = Rect.fromLTWH(
0,
0,
size.width * animation.value,
size.height
);
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant DynamicPainter oldDelegate) => false;
}应用场景:
- 动画进度条
- 动态数据可视化
- 状态指示器
2. 复合绘制效果
class CompositePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
final paint2 = Paint()
..color = Colors.blue
..style = PaintingStyle.stroke
..strokeWidth = 4.0;
canvas.drawRect(
Rect.fromLTWH(50, 50, 200, 100),
paint1
);
canvas.drawRect(
Rect.fromLTWH(150, 150, 200, 100),
paint2
);
}
@override
bool shouldRepaint(covariant CompositePainter oldDelegate) => false;
}八、性能与工程实践
1. 性能优化技巧
- 避免频繁重绘:通过
shouldRepaint方法控制重绘逻辑 - 使用
paint方法的缓存机制:避免重复创建Paint对象 - 减少绘制区域:只绘制可见区域
- 启用硬件加速:默认情况下 Flutter 会自动启用
2. 异常处理
class SafePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
try {
final paint = Paint()
..color = Colors.black
..style = PaintingStyle.fill;
canvas.drawRect(
Rect.fromLTWH(0, 0, 100, 100),
paint
);
} catch (e) {
print("Painting error: $e");
}
}
@override
bool shouldRepaint(covariant SafePainter oldDelegate) => false;
}3. 安全性考虑
- 避免在
paint方法中执行复杂的计算 - 避免在绘制过程中访问外部状态
- 对用户输入的绘图参数进行校验
九、常见问题与踩坑
1. 锯齿问题
错误代码:
canvas.drawRect(
Rect.fromLTWH(50, 50, 200, 100),
Paint()..color = Colors.blue
);问题分析:未启用抗锯齿导致锯齿
解决办法:
Paint()
..color = Colors.blue
..isAntiAlias = true2. 绘制区域超出范围
错误代码:
canvas.drawRect(
Rect.fromLTWH(0, 0, 500, 500),
Paint()
);问题分析:绘制区域超出 CustomPaint 的 size 范围
解决办法:始终基于 size 参数计算绘制区域
3. 动画卡顿
错误代码:
canvas.drawRect(
Rect.fromLTWH(0, 0, 100 * value, 100),
Paint()
);问题分析:在 paint 方法中直接使用动画值
解决办法:在 CustomPainter 中存储动画状态
十、最佳实践
1. 推荐方案
- 使用
CustomPaint实现自定义绘制组件 - 使用
PaintingStyle.stroke实现边框效果 - 使用
MaskFilter实现模糊效果 - 在需要高精度时启用
isAntiAlias
2. 不推荐方案
- 在
paint方法中进行复杂计算 - 直接使用
PaintingStyle.fillAndStroke实现复杂效果 - 在非必要的场景使用
CustomPaint(如普通文本显示)
3. 工程实践建议
- 使用
Paint对象的缓存机制 - 对绘制区域进行边界检查
- 在需要时使用
Picture缓存绘制结果 - 对复杂绘制进行分层处理
十一、总结
Flutter 的 Paint 和 PaintingStyle 系统是构建自定义 UI 的核心工具。通过深入理解这些概念的底层原理,我们可以:
- 实现更复杂的视觉效果
- 优化绘制性能
- 避免常见的绘制错误
- 提升 UI 的可维护性
在实际开发中,建议:
- 使用
PaintingStyle.stroke实现边框效果 - 通过
MaskFilter实现模糊效果 - 合理使用
Paint的属性组合 - 在需要时启用抗锯齿
- 对复杂绘制进行分层处理
通过本文的深入探讨,相信读者能够更灵活地运用 Flutter 的绘制系统,构建出更高质量的 UI 组件。
评论已关闭