Flutter CustomPaint与Canvas实战:从基础绘制到复杂动效。
在Flutter开发中,我们常用的Text、Image、Container等组件,本质上都是Flutter底层通过Canvas绘制而成的。而当原生组件无法满足我们的个性化需求——比如绘制自定义图表、复杂形状、动态效果、自定义控件时,CustomPaint 与 Canvas 就是最核心的解决方案。
很多开发者对CustomPaint望而却步,觉得它“复杂、难上手”,其实核心逻辑很简单:CustomPaint是一个“画布容器”,而Canvas是真正的“画笔”,我们只需要在Canvas上调用各种绘制方法,就能实现任意想要的图形和效果。
本文将以「实战为核心」,从基础到进阶,搭配6个可直接复制运行的示例,覆盖80%的实际开发场景,帮你快速掌握CustomPaint与Canvas的使用技巧,看完就能上手写自定义绘制。
前置说明:本文所有示例基于Flutter 3.10+,无需额外引入依赖,代码可直接复制到项目中运行,每个示例都附带详细注释,新手也能轻松看懂。
一、核心概念:CustomPaint与Canvas是什么?
在开始实战前,先搞懂3个核心类的关系,避免 confusion:
-
CustomPaint:Flutter提供的“画布组件”,本质是一个StatefulWidget,它的作用是提供一个Canvas,并控制画布的大小、背景等基础属性,我们需要通过它的
painter属性传入自定义的绘制逻辑。 -
CustomPainter:自定义绘制的“逻辑载体”,我们必须继承这个类,并重写
paint(Canvas canvas, Size size)方法——这是绘制的核心方法,所有图形都在这个方法里通过Canvas绘制。同时需要重写shouldRepaint方法,控制是否重绘(优化性能)。 -
Canvas:真正的“画笔”,提供了各种绘制方法(绘制点、线、矩形、圆形、路径等),我们通过调用它的方法,就能在画布上“画画”。
核心流程:CustomPaint → 传入CustomPainter → 在CustomPainter的paint方法中,通过Canvas绘制图形 → 渲染到屏幕。
小技巧:Canvas的绘制是“叠加式”的——后绘制的图形会覆盖先绘制的图形,类似PS的图层叠加,这个特性在绘制复杂图形时非常重要。
二、基础实战:5个入门示例(覆盖核心绘制方法)
入门示例以“简单、实用”为原则,覆盖Canvas最常用的绘制方法,每个示例都可独立运行,帮你快速熟悉API。
示例1:绘制基础图形(点、线、矩形、圆形)
这是最基础的示例,覆盖Canvas的4个核心基础绘制方法,掌握它,就掌握了CustomPaint的入门用法。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Canvas基础绘制',
home: Scaffold(
appBar: AppBar(title: const Text('基础图形绘制')),
body: Center(
// 1. 自定义画布:设置宽高、背景
child: CustomPaint(
size: const Size(300, 300), // 画布大小
painter: BasicShapePainter(), // 传入自定义绘制逻辑
foregroundPainter: null, // 前景绘制(覆盖在背景之上)
backgroundImage: null, // 画布背景图
),
),
),
);
}
}
// 2. 自定义绘制逻辑:继承CustomPainter
class BasicShapePainter extends CustomPainter {
// 画笔:控制线条颜色、宽度、样式等
final Paint _paint = Paint()
..color = Colors.blue // 颜色
..strokeWidth = 3 // 线条宽度
..style = PaintingStyle.stroke; // 绘制样式:stroke(描边)、fill(填充)
@override
void paint(Canvas canvas, Size size) {
// 画布中心坐标(方便后续绘制居中图形)
final center = Offset(size.width / 2, size.height / 2);
// 1. 绘制点(参数:偏移量、画笔)
canvas.drawPoint(center, _paint..color = Colors.red, Offset.zero);
// 2. 绘制线(参数:起点、终点、画笔)
canvas.drawLine(
Offset(50, 50), // 起点
Offset(250, 50), // 终点
_paint..color = Colors.green,
);
// 3. 绘制矩形(参数:矩形区域、画笔)
// Rect.fromLTWH:left、top、width、height
canvas.drawRect(
Rect.fromLTWH(50, 100, 200, 80),
_paint..color = Colors.orange,
);
// 4. 绘制圆形(参数:中心点、半径、画笔)
canvas.drawCircle(
center,
60, // 半径
_paint..color = Colors.purple,
);
}
// 重写:控制是否重绘(优化性能)
// 返回true:每次刷新都重绘;返回false:只有当Painter实例改变时才重绘
@override
bool shouldRepaint(covariant BasicShapePainter oldDelegate) {
return false;
}
}
关键说明:
-
Paint是画笔,可控制颜色、线条宽度、绘制样式(描边/填充)、线条圆角、阴影等,后续所有示例都会用到。 -
Offset是坐标类,代表画布上的一个点(x轴、y轴),画布的左上角是原点 (0,0),向右是x轴正方向,向下是y轴正方向。 -
Rect是矩形类,常用fromLTWH(左、上、宽、高)和fromCenter(中心点、宽、高)两种构造方法。
示例2:绘制路径(Path)—— 自定义不规则图形
当基础图形满足不了需求(比如绘制三角形、多边形、自定义曲线)时,就需要用 Path(路径)。Path可以拼接多个点,形成任意不规则图形,是自定义绘制的核心。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Canvas Path绘制',
home: Scaffold(
appBar: AppBar(title: const Text('自定义路径绘制')),
body: Center(
child: CustomPaint(
size: const Size(300, 300),
painter: PathPainter(),
),
),
),
);
}
}
class PathPainter extends CustomPainter {
final Paint _paint = Paint()
..color = Colors.pink
..strokeWidth = 2
..style = PaintingStyle.fill; // 填充模式,绘制闭合图形
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
// 1. 创建路径
final Path path = Path();
// 2. 拼接路径(绘制三角形)
path.moveTo(center.dx, center.dy - 80); // 移动到三角形顶点
path.lineTo(center.dx - 80, center.dy + 40); // 连接到左下角
path.lineTo(center.dx + 80, center.dy + 40); // 连接到右下角
path.close(); // 闭合路径(连接最后一个点和第一个点)
// 3. 绘制路径
canvas.drawPath(path, _paint);
// 额外示例:绘制一条曲线(贝塞尔曲线)
final Path curvePath = Path();
curvePath.moveTo(50, 250); // 起点
// 二阶贝塞尔曲线:控制点、终点
curvePath.quadraticBezierTo(
center.dx, 200, // 控制点(决定曲线弧度)
250, 250, // 终点
);
canvas.drawPath(
curvePath,
_paint
..color = Colors.blue
..style = PaintingStyle.stroke
..strokeWidth = 3,
);
}
@override
bool shouldRepaint(covariant PathPainter oldDelegate) {
return false;
}
}
关键说明:
-
Path.moveTo(x, y):将画笔移动到指定坐标,不绘制线条,用于开始一个新的路径。 -
Path.lineTo(x, y):从当前画笔位置绘制一条直线到指定坐标。 -
Path.close():闭合路径,将最后一个点和第一个点连接起来,形成闭合图形(必须调用,否则填充无效)。 -
贝塞尔曲线:
quadraticBezierTo(二阶贝塞尔)、cubicTo(三阶贝塞尔),常用于绘制平滑曲线(比如自定义按钮、波浪效果)。
示例3:绘制文本(Canvas.drawText)
虽然Flutter有Text组件,但有时候需要在自定义图形上叠加文本(比如图表的坐标轴、自定义控件的文字),这时候就需要用Canvas的 drawText 方法。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Canvas文本绘制',
home: Scaffold(
appBar: AppBar(title: const Text('文本绘制示例')),
body: Center(
child: CustomPaint(
size: const Size(300, 200),
painter: TextPainterDemo(),
),
),
),
);
}
}
class TextPainterDemo extends CustomPainter {
// 文本画笔:控制文本样式
final TextPainter _textPainter = TextPainter(
textDirection: TextDirection.ltr, // 文本方向(从左到右)
textAlign: TextAlign.center, // 文本对齐方式
);
// 文本样式
final TextStyle _textStyle = const TextStyle(
color: Colors.black87,
fontSize: 20,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
);
@override
void paint(Canvas canvas, Size size) {
// 1. 准备文本(TextSpan)
final textSpan = TextSpan(
text: 'Flutter CustomPaint',
style: _textStyle,
);
// 2. 配置文本绘制参数
_textPainter
..text = textSpan
..layout(
minWidth: 0,
maxWidth: size.width, // 文本最大宽度(超出会换行)
);
// 3. 绘制文本(参数:画布、偏移量)
_textPainter.paint(
canvas,
Offset(
(size.width - _textPainter.width) / 2, // 水平居中
(size.height - _textPainter.height) / 2, // 垂直居中
),
);
// 额外示例:绘制带背景的文本
final backgroundPaint = Paint()..color = Colors.yellow.withOpacity(0.3);
// 先绘制矩形背景,再绘制文本(叠加顺序)
canvas.drawRect(
Rect.fromLTWH(
(size.width - _textPainter.width) / 2 - 10,
(size.height - _textPainter.height) / 2 - 5,
_textPainter.width + 20,
_textPainter.height + 10,
),
backgroundPaint,
);
// 再次绘制文本(覆盖在背景上)
_textPainter.paint(
canvas,
Offset(
(size.width - _textPainter.width) / 2,
(size.height - _textPainter.height) / 2,
),
);
}
@override
bool shouldRepaint(covariant TextPainterDemo oldDelegate) {
return false;
}
}
关键说明:
-
绘制文本需要用
TextPainter,而不是直接用Canvas的drawText(Flutter推荐用法,更灵活)。 -
layout()方法必须调用,用于计算文本的宽度和高度,否则无法正常绘制。 -
文本居中技巧:通过计算画布大小和文本大小的差值,设置偏移量,实现水平+垂直居中。
示例4:绘制图片(Canvas.drawImage)
有时候需要在自定义画布上绘制图片(比如自定义头像、背景图、图标),Canvas提供了 drawImage 方法,支持绘制本地图片和网络图片(网络图片需先缓存)。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // 用于加载本地图片
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Canvas图片绘制',
home: Scaffold(
appBar: AppBar(title: const Text('图片绘制示例')),
body: Center(
// 注意:加载图片是异步操作,用FutureBuilder包裹
child: FutureBuilder<ImageProvider>(
future: _loadLocalImage(), // 加载本地图片
builder: (context, snapshot) {
if (snapshot.hasData) {
return CustomPaint(
size: const Size(300, 300),
painter: ImagePainter(image: snapshot.data!),
);
} else {
return const CircularProgressIndicator(); // 加载中
}
},
),
),
),
);
}
// 加载本地图片(需在pubspec.yaml中配置图片路径)
Future<ImageProvider> _loadLocalImage() async {
// 1. 加载图片字节数据
final byteData = await rootBundle.load('assets/images/flutter.png');
// 2. 转换为Uint8List
final uint8List = byteData.buffer.asUint8List();
// 3. 转换为ImageProvider
return MemoryImage(uint8List);
}
}
class ImagePainter extends CustomPainter {
final ImageProvider image;
ImagePainter({required this.image});
@override
void paint(Canvas canvas, Size size) {
// 绘制背景矩形
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = Colors.grey[200]!,
);
// 绘制图片(异步加载,需用ImageStreamListener监听)
image.resolve(const ImageConfiguration()).addListener(
ImageStreamListener(
(ImageInfo info, bool _) {
// 绘制图片(参数:图片、目标矩形、源矩形、画笔)
canvas.drawImageRect(
info.image, // 加载好的图片
Rect.fromLTWH(0, 0, info.image.width.toDouble(), info.image.height.toDouble()), // 源矩形(图片本身)
Rect.fromLTWH(50, 50, 200, 200), // 目标矩形(画布上的位置和大小)
Paint(),
);
// 额外示例:绘制圆形图片(裁剪图片)
final circlePath = Path()
..addOval(Rect.fromLTWH(50, 50, 200, 200)); // 圆形路径
canvas.save(); // 保存当前画布状态
canvas.clipPath(circlePath); // 裁剪画布(只显示圆形区域)
canvas.drawImageRect(
info.image,
Rect.fromLTWH(0, 0, info.image.width.toDouble(), info.image.height.toDouble()),
Rect.fromLTWH(50, 50, 200, 200),
Paint(),
);
canvas.restore(); // 恢复画布状态(避免影响后续绘制)
},
),
);
}
@override
bool shouldRepaint(covariant ImagePainter oldDelegate) {
return oldDelegate.image != image;
}
}
关键说明:
-
加载本地图片需在
pubspec.yaml中配置assets路径,否则会加载失败。 -
图片绘制是异步操作,需用
ImageStreamListener监听图片加载完成,再进行绘制。 -
圆形图片实现:通过
clipPath裁剪画布为圆形,再绘制图片,即可实现圆形头像效果。
示例5:绘制渐变与阴影(提升视觉效果)
基础绘制加上渐变和阴影,能让自定义图形更具视觉冲击力。Canvas支持线性渐变、径向渐变、角度渐变,以及阴影效果,只需通过Paint配置即可。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Canvas渐变与阴影',
home: Scaffold(
appBar: AppBar(title: const Text('渐变与阴影示例')),
body: Center(
child: CustomPaint(
size: const Size(300, 300),
painter: GradientShadowPainter(),
),
),
),
);
}
}
class GradientShadowPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
// 1. 线性渐变(从左到右)
final linearGradient = LinearGradient(
colors: [Colors.blue, Colors.purple], // 渐变颜色
begin: Alignment.centerLeft, // 渐变开始位置
end: Alignment.centerRight, // 渐变结束位置
);
final linearPaint = Paint()
..shader = linearGradient.createShader(
Rect.fromLTWH(50, 50, 200, 80), // 渐变区域
)
..style = PaintingStyle.fill;
canvas.drawRect(Rect.fromLTWH(50, 50, 200, 80), linearPaint);
// 2. 径向渐变(从中心向外扩散)
final radialGradient = RadialGradient(
colors: [Colors.orange, Colors.red],
center: center, // 渐变中心
radius: 80, // 渐变半径
);
final radialPaint = Paint()
..shader = radialGradient.createShader(
Rect.fromCircle(center: center, radius: 80),
)
..style = PaintingStyle.fill
// 阴影配置
..shadowColor = Colors.black38
..shadowBlurRadius = 10 // 阴影模糊度
..shadowOffset = const Offset(5, 5); // 阴影偏移量
canvas.drawCircle(center, 80, radialPaint);
// 3. 角度渐变(围绕中心旋转渐变)
final sweepGradient = SweepGradient(
colors: [Colors.green, Colors.yellow, Colors.green],
startAngle: 0, // 开始角度
endAngle: 2 * 3.1415926, // 结束角度(360度)
);
final sweepPaint = Paint()
..shader = sweepGradient.createShader(
Rect.fromCircle(center: center, radius: 40),
)
..style = PaintingStyle.fill;
canvas.drawCircle(center, 40, sweepPaint);
}
@override
bool shouldRepaint(covariant GradientShadowPainter oldDelegate) {
return false;
}
}
关键说明:
-
渐变通过
Gradient类实现,需通过createShader方法将渐变转换为画笔的shader属性。 -
阴影通过Paint的
shadowColor(阴影颜色)、shadowBlurRadius(模糊度)、shadowOffset(偏移量)配置,只有填充模式(fill)下阴影才会生效。
三、进阶实战:示例6——自定义动态进度条(结合动画)
前面的示例都是静态绘制,实际开发中,我们常需要动态绘制(比如进度条、加载动画、图表动效)。这个示例结合Flutter动画,实现一个自定义动态进度条,覆盖“动态绘制+性能优化”,贴近真实开发场景。
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '动态进度条实战',
home: const Scaffold(
appBar: AppBar(title: Text('自定义动态进度条')),
body: Padding(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 动态进度条(结合动画)
CustomProgressBar(progress: 0.7), // 初始进度70%
const SizedBox(height: 20),
CustomProgressBar(progress: 0.3, color: Colors.red), // 自定义颜色
],
),
),
),
);
}
}
// 自定义动态进度条组件(StatefulWidget,支持动画)
class CustomProgressBar extends StatefulWidget {
final double progress; // 进度(0.0 ~ 1.0)
final Color color; // 进度条颜色
final double height; // 进度条高度
const CustomProgressBar({
super.key,
required this.progress,
this.color = Colors.blue,
this.height = 10,
});
@override
State<CustomProgressBar> createState() => _CustomProgressBarState();
}
class _CustomProgressBarState extends State<CustomProgressBar>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _progressAnimation;
@override
void initState() {
super.initState();
// 初始化动画控制器
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1), // 动画时长1秒
);
// 进度动画(从0过渡到目标进度)
_progressAnimation = Tween<double>(
begin: 0.0,
end: widget.progress,
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
)..addListener(() {
setState(() {}); // 触发重绘
});
// 启动动画
_controller.forward();
}
@override
void didUpdateWidget(covariant CustomProgressBar oldWidget) {
super.didUpdateWidget(oldWidget);
// 当进度变化时,更新动画
if (oldWidget.progress != widget.progress) {
_progressAnimation = Tween<double>(
begin: _progressAnimation.value,
end: widget.progress,
).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
)..addListener(() {
setState(() {});
});
_controller.forward(from: 0);
}
}
@override
void dispose() {
_controller.dispose(); // 释放动画资源
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(MediaQuery.of(context).size.width - 40, widget.height),
// 传入当前进度,用于绘制
painter: ProgressBarPainter(
progress: _progressAnimation.value,
color: widget.color,
height: widget.height,
),
);
}
}
// 进度条绘制逻辑
class ProgressBarPainter extends CustomPainter {
final double progress;
final Color color;
final double height;
ProgressBarPainter({
required this.progress,
required this.color,
required this.height,
});
// 背景画笔(灰色底色)
final Paint _backgroundPaint = Paint()..color = Colors.grey[200]!;
// 进度画笔(自定义颜色,带圆角和阴影)
late final Paint _progressPaint = Paint()
..color = color
..style = PaintingStyle.fill
..shadowColor = color.withOpacity(0.3)
..shadowBlurRadius = 2;
@override
void paint(Canvas canvas, Size size) {
// 1. 绘制进度条背景(圆角矩形)
final backgroundRect = RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, height),
Radius.circular(height / 2), // 圆角半径=高度的一半,实现胶囊形状
);
canvas.drawRRect(backgroundRect, _backgroundPaint);
// 2. 绘制进度(根据progress计算宽度)
final progressWidth = size.width * progress;
if (progressWidth > 0) {
final progressRect = RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, progressWidth, height),
Radius.circular(height / 2),
);
canvas.drawRRect(progressRect, _progressPaint);
}
}
// 性能优化:只有进度、颜色、高度变化时,才重绘
@override
bool shouldRepaint(covariant ProgressBarPainter oldDelegate) {
return oldDelegate.progress != progress ||
oldDelegate.color != color ||
oldDelegate.height != height;
}
}
关键说明:
-
动态绘制的核心是“动画+重绘”:通过AnimationController控制动画,在动画回调中调用setState,触发CustomPaint重绘,从而实现动态效果。
-
性能优化:重写
shouldRepaint方法,只有当绘制参数(进度、颜色、高度)变化时,才进行重绘,避免不必要的性能消耗。 -
胶囊形状进度条:通过
RRect.fromRectAndRadius绘制圆角矩形,圆角半径等于高度的一半,即可实现胶囊效果。
四、实战总结与避坑指南
1. 核心总结
-
CustomPaint是画布容器,CustomPainter是绘制逻辑载体,Canvas是画笔,三者协同完成自定义绘制。
-
基础绘制:掌握点、线、矩形、圆形、路径、文本、图片的绘制方法,就能满足大部分简单需求。
-
进阶技巧:渐变、阴影能提升视觉效果,动画能实现动态绘制,shouldRepaint能优化性能。
-
实战思路:先确定绘制需求 → 定义Paint(画笔) → 拼接Path(路径,可选) → 调用Canvas绘制方法 → 优化性能。
2. 常见坑点与解决方案
-
坑点1:绘制的图形不显示? 解决方案:检查CustomPaint的size是否设置(默认size为0,无法显示);检查Paint的style是否正确(stroke描边时,strokeWidth不能为0);检查绘制的坐标是否超出画布范围。
-
坑点2:图片绘制失败? 解决方案:检查本地图片是否在pubspec.yaml中配置;图片加载是异步的,需用ImageStreamListener监听加载完成后再绘制;避免在paint方法中直接加载图片(会阻塞绘制)。
-
坑点3:动态绘制卡顿? 解决方案:重写shouldRepaint方法,避免不必要的重绘;减少绘制次数(比如合并路径,避免多次draw操作);避免在paint方法中做耗时操作(比如网络请求、复杂计算)。
-
坑点4:文本绘制不居中? 解决方案:通过TextPainter的layout方法获取文本宽度和高度,再计算偏移量,实现居中;确保TextPainter的textDirection和textAlign配置正确。
3. 拓展方向
掌握了以上示例,你已经能应对80%的自定义绘制场景,后续可以进一步拓展:
-
复杂图表:绘制折线图、柱状图、饼图(结合Path和动画)。
-
自定义控件:绘制自定义按钮、滑块、进度指示器、底部导航栏。
-
高级动效:绘制粒子动画、波浪动画、路径动画(结合Path和Animation)。
-
性能优化:使用
RepaintBoundary隔离绘制区域,避免全局重绘;使用PictureRecorder缓存绘制结果,减少重复绘制。
最后,自定义绘制的核心是“多练”——多尝试修改示例中的参数(比如颜色、尺寸、路径),多结合实际需求写自定义组件,慢慢就能熟练掌握Canvas的各种用法。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)