文章目录
-
- 第一步:什么是CustomPainter?
- 第二步:基础知识详解
-
- CustomPainter的基本结构
- [Canvas - 你的画布](#Canvas - 你的画布)
- [Paint - 你的画笔](#Paint - 你的画笔)
- [Path - 复杂路径](#Path - 复杂路径)
- 坐标系统
- 第三步:实战演示
- 第四步:知识点全面总结
-
- CustomPainter核心概念
-
- [1. 三大核心组件](#1. 三大核心组件)
- [2. **生命周期方法**](#2. 生命周期方法)
- [🖌️ **Canvas绘制方法大全**](#🖌️ Canvas绘制方法大全)
- [🎨 **Paint属性详解**](#🎨 Paint属性详解)
- [🗺️ **Path路径操作**](#🗺️ Path路径操作)
- [📏 **坐标系统和变换**](#📏 坐标系统和变换)
- [⚡ **性能优化技巧**](#⚡ 性能优化技巧)
- [🎯 **实用技巧和最佳实践**](#🎯 实用技巧和最佳实践)
- [🚀 进阶学习方向](#🚀 进阶学习方向)
-
- [🎮 **动画绘制**](#🎮 动画绘制)
- [🖱️ **交互绘制**](#🖱️ 交互绘制)
- [📊 **数据可视化**](#📊 数据可视化)
- [🎨 **艺术效果**](#🎨 艺术效果)
- [💡 总结](#💡 总结)
从零开始学会自定义绘制
循序渐进掌握Flutter最强大的绘图工具

第一步:什么是CustomPainter?
生活比喻
想象你是一个画家,Flutter给了你:
Canvas(画布) = 你的绘图区域,就像白纸
Paint(画笔) = 设置颜色、粗细的工具
绘图方法 = 不同的绘画技法(画线、画圆等)
为什么需要CustomPainter?
标准Widget的局限性:
- Container只能画矩形
- CircleAvatar只能画圆形
- Icon只能显示预设图标
CustomPainter的强大之处:
- 任意形状:星星、心形、复杂图案
- 数据可视化:图表、进度条、仪表盘
- 游戏图形:角色、道具、特效
- 艺术效果:渐变、阴影、纹理
使用场景对比
| 需求 | 标准Widget | CustomPainter |
|---|---|---|
| 简单矩形 | Container | 过度设计 |
| 圆形头像 | CircleAvatar | 不必要 |
| 五角星 | 无法实现 | 完美解决 |
| 柱状图 | 复杂组合 | 简单直接 |
| 自定义图标 | 受限制 | 无限可能 |
第二步:基础知识详解
CustomPainter的基本结构
CustomPainter就像一个画家的工作模板:
🎨 继承关系 :所有自定义绘制器都必须继承CustomPainter类
📝 两个核心方法 :每个绘制器都需要实现两个关键方法
🔄 生命周期管理:Flutter会在合适的时机调用这些方法
基本结构解析:
-
paint方法 - 🎨 绘制核心- 这里是你的"创作空间"
- 接收Canvas(画布)和Size(尺寸)参数
- 所有的绘制逻辑都在这里实现
-
shouldRepaint方法 - ⚡ 性能优化- 决定何时需要重新绘制
- 返回
true:重新绘制(数据变化时) - 返回
false:使用缓存(静态内容时)
dart
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 在这里进行绘制
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// 决定是否需要重新绘制
return false;
}
}
Canvas - 你的画布
Canvas提供了丰富的绘制方法:
dart
// 绘制基本图形
canvas.drawCircle(center, radius, paint); // 圆形
canvas.drawRect(rect, paint); // 矩形
canvas.drawLine(p1, p2, paint); // 直线
canvas.drawOval(rect, paint); // 椭圆
// 绘制复杂图形
canvas.drawPath(path, paint); // 路径
canvas.drawRRect(rrect, paint); // 圆角矩形
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint); // 弧形
// 绘制文本和图像
canvas.drawParagraph(paragraph, offset); // 段落文本
canvas.drawImage(image, offset, paint); // 图像
Paint - 你的画笔
Paint控制绘制的样式:
dart
final paint = Paint()
..color = Colors.blue // 颜色
..strokeWidth = 2.0 // 线条粗细
..style = PaintingStyle.fill // 填充样式
..strokeCap = StrokeCap.round // 线条端点样式
..strokeJoin = StrokeJoin.round // 线条连接样式
..isAntiAlias = true; // 抗锯齿
Paint样式详解:
| 属性 | 说明 | 常用值 |
|---|---|---|
| color | 颜色 | Colors.red, Color(0xFF123456) |
| style | 样式 | fill(填充), stroke(描边) |
| strokeWidth | 线宽 | 1.0, 2.5, 10.0 |
| strokeCap | 端点 | round(圆形), square(方形) |
| isAntiAlias | 抗锯齿 | true(平滑), false(锐利) |
Path - 复杂路径
Path用于绘制复杂形状:
dart
final path = Path()
..moveTo(x1, y1) // 移动到起点
..lineTo(x2, y2) // 画直线到
..quadraticBezierTo( // 二次贝塞尔曲线
cpx, cpy, x3, y3)
..cubicTo( // 三次贝塞尔曲线
cp1x, cp1y, cp2x, cp2y, x4, y4)
..close(); // 闭合路径
坐标系统
Flutter的坐标系统:
text
(0,0) ────────── X轴 →
│
│ 屏幕区域
│
│
Y轴
↓
重要概念:
- 原点(0,0)在左上角
- X轴向右为正
- Y轴向下为正
- Size.width 和 Size.height 是绘制区域的尺寸
为什么是Canvas绘制而不是Paint?
🎨 Canvas = 画布 :负责"在哪里画"和"画什么形状"
🖌️ Paint = 画笔:负责"用什么样式画"(颜色、粗细等)
就像现实中画画:你在画布上画圆形,用画笔决定颜色。Canvas决定动作,Paint决定样式!
第三步:实战演示
从简单开始:绘制一个圆
dart
class CirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
// 在中心绘制圆形
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 4;
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
进阶:完整项目演示

dart
import 'package:flutter/material.dart';
import 'dart:math' as math; // 导入数学库,用于三角函数计算
/// 应用程序入口点
/// 创建一个展示CustomPainter功能的演示应用
void main() {
runApp(MaterialApp(
home: CustomPainterDemo(),
));
}
/// CustomPainter演示页面
/// 这个页面展示了CustomPainter的两个实用示例:
/// 1. 星星绘制 - 展示如何使用Path绘制复杂形状
/// 2. 柱状图 - 展示如何绘制数据可视化图表
class CustomPainterDemo extends StatelessWidget {
/// 构建页面UI结构
/// 使用Column垂直排列两个演示组件
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('CustomPainter演示')),
body: Padding(
padding: EdgeInsets.all(20), // 为整个页面添加20像素的内边距
child: Column(
children: [
// 星星图案演示区域
Text('自定义星星', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 16), // 添加垂直间距
// 五角星组件 - 展示复杂形状的绘制
StarPainter(
size: 100, // 星星大小:100x100像素
color: Colors.yellow, // 星星颜色:黄色
points: 5, // 星星角数:5个角
),
SizedBox(height: 30), // 区域之间的较大间距
// 柱状图演示区域
Text('自定义柱状图', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 16),
// 柱状图组件 - 展示数据可视化
BarChartPainter(
data: [0.3, 0.7, 0.5, 0.9, 0.4], // 数据数组,值范围 0.0-1.0
width: 300, // 图表宽度:300像素
height: 150, // 图表高度:150像素
),
],
),
),
);
}
}
/// 星星绘制器组件(就像用圆规画星星)
///
/// 这是一个可自定义的星星绘制组件,具有以下特性:
/// - 可调节星星的大小和颜色
/// - 可设置星星的角数(默认5角)
/// - 使用数学计算精确绘制对称的星形
/// - 适用于装饰、评分显示等场景
class StarPainter extends StatelessWidget {
/// 星星的大小(宽度和高度),单位为像素
final double size;
/// 星星的颜色
final Color color;
/// 星星的角数,默认5角(五角星)
final int points;
/// 构造函数
/// [size] 星星大小,必需参数
/// [color] 星星颜色,必需参数
/// [points] 星星角数,默认5角
const StarPainter({
Key? key,
required this.size,
required this.color,
this.points = 5,
}) : super(key: key);
/// 构建星星组件的UI
/// 使用CustomPaint绘制星形,将绘制逻辑委托给_StarPainter
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(size, size), // 设置绘制区域为正方形
painter: _StarPainter(color: color, points: points), // 使用自定义绘制器
);
}
}
/// 星星绘制器的CustomPainter实现
///
/// 这个类负责在Canvas上绘制星形,使用数学计算确定每个点的位置
class _StarPainter extends CustomPainter {
/// 星星的颜色
final Color color;
/// 星星的角数
final int points;
/// 构造函数
/// 初始化绘制星星所需的参数
_StarPainter({required this.color, required this.points});
/// 核心绘制方法
/// 使用数学计算和Path绘制对称的星形
/// [canvas] 绘制画布
/// [size] 绘制区域大小
@override
void paint(Canvas canvas, Size size) {
// 创建画笔,设置填充样式
final paint = Paint()
..color = color // 设置星星颜色
..style = PaintingStyle.fill; // 填充样式,不是只绘边框
// 创建绘制路径,用于连接星星的所有点
final path = Path();
// 计算星星的中心点和半径
final center = Offset(size.width / 2, size.height / 2); // 中心点
final outerRadius = size.width / 2; // 外半径(星星尖角的距离)
final innerRadius = outerRadius * 0.4; // 内半径(星星凹陷的距离)
// 计算星星的各个点(就像连接星座的点)
// 每个星角需要两个点:一个尖角点(外半径)和一个凹陷点(内半径)
for (int i = 0; i < points * 2; i++) {
// 计算当前点的角度(从顶部开始,顺时针方向)
final angle = (i * math.pi) / points - math.pi / 2;
// 交替使用外半径和内半径:偶数索引使用外半径(尖角),奇数索引使用内半径(凹陷)
final radius = i.isEven ? outerRadius : innerRadius;
// 使用三角函数计算点的坐标
final x = center.dx + radius * math.cos(angle); // x坐标
final y = center.dy + radius * math.sin(angle); // y坐标
// 构建路径:第一个点移动到,其余点连线到
if (i == 0) {
path.moveTo(x, y); // 移动到起始点
} else {
path.lineTo(x, y); // 从当前点连线到新点
}
}
// 关闭路径,连接最后一个点到第一个点
path.close();
// 在画布上绘制星形路径
canvas.drawPath(path, paint);
}
/// 判断是否需要重新绘制
/// 返回false表示星星形状不变,不需要重新绘制,提高性能
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// 柱状图绘制器组件(就像画家画柱子)
///
/// 这是一个简单而实用的柱状图组件,具有以下特性:
/// - 支持多个数据点的显示
/// - 自动计算柱子宽度和间距
/// - 彩虹色柱子,美观易读
/// - 显示百分比数值标签
/// - 圆角设计,现代化外观
class BarChartPainter extends StatelessWidget {
/// 数据数组,每个值的范围为 0.0-1.0,代表百分比
final List<double> data;
/// 柱状图的宽度,单位为像素
final double width;
/// 柱状图的高度,单位为像素
final double height;
/// 构造函数
/// [data] 数据数组,必需参数
/// [width] 图表宽度,必需参数
/// [height] 图表高度,必需参数
const BarChartPainter({
Key? key,
required this.data,
required this.width,
required this.height,
}) : super(key: key);
/// 构建柱状图组件的UI
/// 使用CustomPaint绘制柱状图,将绘制逻辑委托给_BarChartPainter
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(width, height), // 设置绘制区域大小
painter: _BarChartPainter(data: data), // 使用自定义绘制器
);
}
}
/// 柱状图绘制器的CustomPainter实现
///
/// 这个类负责在Canvas上绘制柱状图,包括柱子和数值标签
class _BarChartPainter extends CustomPainter {
/// 数据数组
final List<double> data;
/// 构造函数
/// 初始化绘制柱状图所需的数据
_BarChartPainter({required this.data});
/// 核心绘制方法
/// 绘制柱状图的所有元素:柱子、颜色、标签
/// [canvas] 绘制画布
/// [size] 绘制区域大小
@override
void paint(Canvas canvas, Size size) {
// 计算柱子的宽度和间距
// 每个柱子占据80%的空间,20%用作间距
final barWidth = size.width / data.length * 0.8; // 柱子宽度
final spacing = size.width / data.length * 0.2; // 间距宽度
// 遍历所有数据点,绘制每个柱子
for (int i = 0; i < data.length; i++) {
// 计算柱子的尺寸和位置
final barHeight = size.height * data[i]; // 柱子高度:数据值 × 总高度
final left = i * (barWidth + spacing) + spacing / 2; // 柱子左边位置
final top = size.height - barHeight; // 柱子顶部位置(从底部向上)
// 为每个柱子设置不同颜色(就像彩虹色彩)
// 使用HSV颜色模式创建彩虹效果:色相均匀分布,饱和度和亮度固定
final paint = Paint()
..color = HSVColor.fromAHSV(
1.0, // 透明度:完全不透明
i * 360.0 / data.length, // 色相:根据索引均匀分布在360度范围内
0.8, // 饱和度:80%,颜色鲜艳但不过于刺眼
0.9, // 亮度:90%,明亮但不过于刺眼
).toColor()
..style = PaintingStyle.fill; // 填充样式
// 创建柱子的矩形区域
final rect = Rect.fromLTWH(left, top, barWidth, barHeight);
// 绘制圆角柱子(使用RRect而不是普通矩形)
canvas.drawRRect(
RRect.fromRectAndRadius(rect, Radius.circular(4)), // 4像素圆角
paint,
);
// 绘制数值标签(就像给柱子贴标签)
// 使用TextPainter在Canvas上绘制文本
final textPainter = TextPainter(
text: TextSpan(
text: '${(data[i] * 100).toInt()}%', // 将数据转换为百分比显示
style: TextStyle(
color: Colors.black87, // 深灰色文字,清晰可见
fontSize: 12, // 字体大小
fontWeight: FontWeight.bold, // 粗体,增加可读性
),
),
textDirection: TextDirection.ltr, // 文本方向:从左到右
);
// 计算文本尺寸和位置
textPainter.layout(); // 计算文本的实际尺寸
// 绘制文本在柱子顶部中心位置
textPainter.paint(
canvas,
Offset(
left + barWidth / 2 - textPainter.width / 2, // 水平居中:柱子中心 - 文本宽度的一半
top - textPainter.height - 4, // 垂直位置:柱子顶部上方4像素
),
);
}
}
/// 判断是否需要重新绘制
/// 返回true表示数据可能变化,需要重新绘制
/// 在实际项目中,可以比较数据是否变化来优化性能
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
第四步:知识点全面总结
CustomPainter核心概念
1. 三大核心组件
| 组件 | 作用 | 比喻 |
|---|---|---|
| Canvas | 绘制表面 | 🎨 画布 |
| Paint | 绘制样式 | 🖌️ 画笔 |
| Size | 绘制区域 | 📏 画框 |
2. 生命周期方法
dart
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 🎨 核心绘制逻辑
// 每次需要绘制时都会调用
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// 🔄 性能优化关键
// true: 重新绘制 false: 使用缓存
return false;
}
}
🖌️ Canvas绘制方法大全
基础图形
dart
// 🔵 圆形和椭圆
canvas.drawCircle(center, radius, paint);
canvas.drawOval(rect, paint);
// 📐 矩形和圆角矩形
canvas.drawRect(rect, paint);
canvas.drawRRect(rrect, paint);
// 📏 线条
canvas.drawLine(p1, p2, paint);
canvas.drawPoints(PointMode.lines, points, paint);
// 🌙 弧形
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
复杂图形
dart
// 🗺️ 路径绘制
canvas.drawPath(path, paint);
// 📝 文本绘制
canvas.drawParagraph(paragraph, offset);
// 🖼️ 图像绘制
canvas.drawImage(image, offset, paint);
canvas.drawImageRect(image, src, dst, paint);
🎨 Paint属性详解
颜色设置
dart
paint.color = Colors.red; // 纯色
paint.color = Color(0xFF123456); // 十六进制
paint.color = Color.fromRGBO(255, 0, 0, 1); // RGBA
paint.color = HSVColor.fromAHSV(1, 120, 0.8, 0.9).toColor(); // HSV
样式设置
dart
// 填充样式
paint.style = PaintingStyle.fill; // 填充
paint.style = PaintingStyle.stroke; // 描边
// 线条样式
paint.strokeWidth = 2.0; // 线宽
paint.strokeCap = StrokeCap.round; // 端点:round, square, butt
paint.strokeJoin = StrokeJoin.round; // 连接:round, miter, bevel
// 其他属性
paint.isAntiAlias = true; // 抗锯齿
paint.filterQuality = FilterQuality.high; // 滤镜质量
🗺️ Path路径操作
基础路径
dart
final path = Path();
// 移动和连线
path.moveTo(x, y); // 移动到点
path.lineTo(x, y); // 直线到点
path.close(); // 闭合路径
// 弧线
path.arcTo(rect, startAngle, sweepAngle, forceMoveTo);
path.quadraticBezierTo(cpx, cpy, x, y); // 二次贝塞尔
path.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y); // 三次贝塞尔
路径操作
dart
// 路径变换
path.transform(matrix4);
path.shift(offset);
// 路径组合
Path.combine(PathOperation.union, path1, path2); // 并集
Path.combine(PathOperation.intersect, path1, path2); // 交集
Path.combine(PathOperation.difference, path1, path2); // 差集
📏 坐标系统和变换
坐标系统
dart
// Flutter坐标系(左上角为原点)
(0,0) ────────── (width,0)
│ │
│ 绘制区域 │
│ │
(0,height) ──── (width,height)
Canvas变换
dart
// 平移
canvas.translate(dx, dy);
// 旋转(弧度)
canvas.rotate(angle);
// 缩放
canvas.scale(sx, sy);
// 保存和恢复状态
canvas.save(); // 保存当前变换状态
// ... 进行变换和绘制
canvas.restore(); // 恢复到保存的状态
⚡ 性能优化技巧
shouldRepaint优化
dart
@override
bool shouldRepaint(MyPainter oldDelegate) {
// ✅ 好的做法:比较具体属性
return color != oldDelegate.color ||
size != oldDelegate.size;
// ❌ 避免:总是返回true
// return true;
// ❌ 避免:总是返回false(数据变化时不更新)
// return false;
}
绘制优化
dart
@override
void paint(Canvas canvas, Size size) {
// ✅ 复用Paint对象
final paint = Paint()..color = Colors.blue;
// ✅ 避免在循环中创建对象
final rect = Rect.fromLTWH(0, 0, 100, 100);
for (int i = 0; i < 10; i++) {
canvas.drawRect(rect.shift(Offset(i * 20, 0)), paint);
}
// ❌ 避免:在循环中创建新对象
// for (int i = 0; i < 10; i++) {
// final paint = Paint()..color = Colors.blue; // 每次都创建
// canvas.drawRect(Rect.fromLTWH(i * 20, 0, 100, 100), paint);
// }
}
🎯 实用技巧和最佳实践
文本绘制
dart
void drawText(Canvas canvas, String text, Offset position) {
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(color: Colors.black, fontSize: 16),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, position);
}
渐变效果
dart
final gradient = LinearGradient(
colors: [Colors.blue, Colors.red],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
final paint = Paint()
..shader = gradient.createShader(rect);
canvas.drawRect(rect, paint);
阴影效果
dart
final shadowPaint = Paint()
..color = Colors.black.withOpacity(0.3)
..maskFilter = MaskFilter.blur(BlurStyle.normal, 3);
// 先绘制阴影
canvas.drawCircle(center.translate(2, 2), radius, shadowPaint);
// 再绘制主体
canvas.drawCircle(center, radius, mainPaint);
🚀 进阶学习方向
🎮 动画绘制
原理:通过Animation提供的0.0-1.0变化值,实时改变绘制参数,产生动画效果。
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;
// ... 绘制逻辑
}
}
🖱️ 交互绘制
原理:收集用户触摸事件的坐标点,存储在列表中,然后在绘制时连接这些点形成轨迹。
dart
class InteractivePainter extends CustomPainter {
final List<Offset> points;
InteractivePainter(this.points);
@override
void paint(Canvas canvas, Size size) {
// 绘制用户触摸的轨迹
if (points.isNotEmpty) {
final path = Path()..moveTo(points.first.dx, points.first.dy);
for (final point in points.skip(1)) {
path.lineTo(point.dx, point.dy);
}
canvas.drawPath(path, paint);
}
}
}
📊 数据可视化
- 饼图 :使用
drawArc绘制扇形 - 折线图 :使用
Path连接数据点 - 散点图 :使用
drawCircle绘制数据点 - 热力图:使用颜色渐变表示数据密度
🎨 艺术效果
- 粒子系统:绘制大量小图形模拟粒子
- 波浪效果:使用正弦函数创建波浪路径
- 几何图案:使用数学函数创建复杂图案
- 滤镜效果 :使用
ColorFilter和ImageFilter
💡 总结
CustomPainter是Flutter中最强大的绘图工具,掌握它可以让你:
✅ 创造无限可能的UI效果
✅ 实现高性能的自定义组件
✅ 开发专业的数据可视化
✅ 制作精美的动画和交互
记住关键点:
- 🎨 Canvas是你的画布,Paint是你的画笔
- 📏 理解坐标系统和变换
- ⚡ 合理使用shouldRepaint优化性能
- 🔄 结合动画创造动态效果
绘图学的就是一种思想,学会了CustomPainter的绘画方式其实也就掌握了其它平台上的绘画方式,比如:Android上的自定义组件、Js上的绘制组件都是大同小异。希望你能掌握学习技巧,继续探索CustomPainter的无限可能,创造出独特的视觉效果!🎨✨