在 Flutter 中,
CustomPainter是实现自定义绘制的核心组件,可灵活绘制图形、路径、文本、渐变甚至复杂动效,其核心逻辑是通过重写paint()(定义绘制逻辑)和shouldRepaint()(控制重绘时机)来实现自定义视觉效果。以下是从基础到进阶的完整实现指南:一、核心概念与基础流程
1. 核心类说明
| 类 / 对象 | 作用 |
|---|---|
CustomPainter |
抽象类,需继承并实现paint()和shouldRepaint(),封装绘制逻辑 |
Canvas |
绘制画布,提供绘制点、线、矩形、路径、文本、图像等所有绘制 API |
Paint |
画笔,定义颜色、线条宽度、填充方式、抗锯齿、渐变等绘制样式 |
CustomPaint |
Flutter 组件,承载CustomPainter,将绘制内容渲染到界面上 |
2. 基础实现步骤
步骤 1:继承
CustomPainter,实现核心方法创建自定义绘制类,重写paint()(绘制逻辑)和shouldRepaint()(重绘判断)。dart
import 'package:flutter/material.dart';// 自定义绘制器(绘制一个带渐变的圆形)
class MyCustomPainter extends CustomPainter {// 可自定义参数,灵活控制绘制效果final double radius;final Color primaryColor;MyCustomPainter({required this.radius, required this.primaryColor});// 核心:绘制逻辑(Canvas是画布,Size是CustomPaint的尺寸)@overridevoid paint(Canvas canvas, Size size) {// 1. 创建画笔final Paint paint = Paint()..color = primaryColor // 基础颜色..style = PaintingStyle.fill // 填充模式(stroke为描边)..isAntiAlias = true; // 抗锯齿// 2. 进阶:添加渐变(替代单一颜色)paint.shader = RadialGradient(center: Alignment.center,radius: 1.0,colors: [primaryColor, primaryColor.withOpacity(0.3)],).createShader(Rect.fromCircle(center: Offset(size.width / 2, size.height / 2),radius: radius,));// 3. 绘制圆形(画布中心为圆心)canvas.drawCircle(Offset(size.width / 2, size.height / 2), // 圆心坐标radius, // 半径paint, // 画笔);}// 关键:判断是否需要重绘(优化性能)// 仅当绘制参数变化时返回true,避免无意义重绘@overridebool shouldRepaint(covariant MyCustomPainter oldDelegate) {return oldDelegate.radius != radius || oldDelegate.primaryColor != primaryColor;}
}
步骤 2:通过
CustomPaint组件渲染将自定义CustomPainter传入CustomPaint的painter参数,嵌入 Flutter 界面树中。dart
class CustomPaintDemo extends StatelessWidget {const CustomPaintDemo({super.key});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('自定义绘制基础')),body: Center(// 承载自定义绘制的核心组件child: CustomPaint(// 绘制区域大小(不设置则自适应子组件/父组件)size: const Size(200, 200),// 前景绘制(覆盖子组件)painter: MyCustomPainter(radius: 80, primaryColor: Colors.blue),// 可选:背景绘制(被前景/子组件覆盖)// backgroundPainter: MyBackgroundPainter(),// 可选:子组件(绘制在画布上层)child: const Text('自定义圆形',style: TextStyle(fontSize: 16, color: Colors.white),),),),);}
}
二、常用绘制 API(Canvas 核心操作)
Canvas提供了丰富的绘制方法,以下是高频使用的场景示例:1. 绘制基础图形
dart
@override
void paint(Canvas canvas, Size size) {final Paint paint = Paint()..color = Colors.red..strokeWidth = 2..isAntiAlias = true;// 1. 绘制点canvas.drawPoints(PointMode.points, // 点模式(points/lines/polygon)[const Offset(50, 50), const Offset(100, 100)], // 点坐标列表paint..strokeWidth = 10, // 点大小由strokeWidth控制);// 2. 绘制直线canvas.drawLine(const Offset(50, 50), // 起点const Offset(150, 150), // 终点paint..color = Colors.green,);// 3. 绘制矩形(普通矩形)canvas.drawRect(const Rect.fromLTWH(50, 50, 100, 80), // 左、上、宽、高paint..color = Colors.yellow..style = PaintingStyle.stroke,);// 4. 绘制圆角矩形canvas.drawRRect(RRect.fromRectAndRadius(const Rect.fromLTWH(50, 150, 100, 80),const Radius.circular(10),),paint..color = Colors.purple,);// 5. 绘制椭圆(矩形内切椭圆)canvas.drawOval(const Rect.fromLTWH(50, 250, 100, 60),paint..color = Colors.orange,);
}
2. 绘制路径(Path):复杂自定义图形
Path是绘制不规则图形的核心,支持移动、画线、贝塞尔曲线等操作:dart
@override
void paint(Canvas canvas, Size size) {final Paint paint = Paint()..color = Colors.pink..style = PaintingStyle.fill..isAntiAlias = true;// 1. 创建路径(绘制五角星)final Path path = Path();// 计算五角星顶点坐标(中心为画布中心)double centerX = size.width / 2;double centerY = size.height / 2;double outerRadius = 80; // 外圆半径double innerRadius = 40; // 内圆半径// 遍历绘制五角星的10个顶点(5个外顶点+5个内顶点)for (int i = 0; i < 10; i++) {double angle = i * 36 * pi / 180; // 每个顶点的角度(36°间隔)double r = i % 2 == 0 ? outerRadius : innerRadius; // 偶索引取外圆,奇索引取内圆double x = centerX + r * cos(angle);double y = centerY + r * sin(angle);if (i == 0) {path.moveTo(x, y); // 移动到第一个顶点} else {path.lineTo(x, y); // 连线到后续顶点}}path.close(); // 闭合路径// 2. 绘制路径canvas.drawPath(path, paint);
}
3. 绘制文本与图像
dart
@override
void paint(Canvas canvas, Size size) {// 1. 绘制文本final TextPainter textPainter = TextPainter(text: const TextSpan(text: 'Flutter自定义绘制',style: TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold),),textDirection: TextDirection.ltr, // 文本方向(必须指定));// 布局文本(计算尺寸)textPainter.layout(minWidth: 0, maxWidth: size.width);// 绘制文本(居中显示)textPainter.paint(canvas,Offset((size.width - textPainter.width) / 2, (size.height - textPainter.height) / 2),);// 2. 绘制图像(需提前加载图片)// 示例:通过ImageProvider加载本地/网络图片final ImageProvider imageProvider = AssetImage('assets/flutter_logo.png');// 注意:图像绘制需异步加载,建议结合FutureBuilder或提前缓存imageProvider.resolve(const ImageConfiguration()).addListener(ImageStreamListener((ImageInfo info, bool _) {canvas.drawImage(info.image,Offset(size.width / 2 - info.image.width / 2, size.height / 2 + 40),Paint(),);}),);
}
三、进阶技巧:动效与性能优化
1. 结合动画实现动态绘制
通过
AnimationController和Listener,让绘制参数随动画变化,实现动态效果:dart
class AnimatedCustomPainter extends StatefulWidget {const AnimatedCustomPainter({super.key});@overrideState<AnimatedCustomPainter> createState() => _AnimatedCustomPainterState();
}class _AnimatedCustomPainterState extends State<AnimatedCustomPainter>with SingleTickerProviderStateMixin {late AnimationController _controller;late Animation<double> _radiusAnimation;@overridevoid initState() {super.initState();// 创建动画控制器(2秒循环)_controller = AnimationController(vsync: this,duration: const Duration(seconds: 2),)..repeat(reverse: true); // 反向循环// 动画值:半径从50到100_radiusAnimation = Tween<double>(begin: 50, end: 100).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut),);}@overridevoid dispose() {_controller.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Center(child: AnimatedBuilder(animation: _controller,builder: (context, child) {return CustomPaint(size: const Size(200, 200),painter: MyCustomPainter(radius: _radiusAnimation.value,primaryColor: Colors.blue,),);},),);}
}
2. 性能优化关键
- 精准控制重绘:
shouldRepaint()仅在绘制参数变化时返回true,避免频繁重绘;dart@override bool shouldRepaint(covariant MyCustomPainter oldDelegate) {// 仅当半径/颜色变化时重绘return oldDelegate.radius != radius || oldDelegate.primaryColor != primaryColor; } - 使用
RepaintBoundary隔离渲染层:若自定义绘制组件与其他动效组件共存,用RepaintBoundary包裹,避免其他组件动效触发绘制重绘;dartRepaintBoundary(child: CustomPaint(painter: MyCustomPainter(radius: 80, primaryColor: Colors.blue)), ) - 减少复杂计算:将绘制中的固定计算(如坐标、渐变)移到
paint()外,避免每次绘制重复计算; - 避免过度抗锯齿:非必要场景关闭
isAntiAlias(默认 false),减少渲染开销。
3. 坐标系变换(平移 / 旋转 / 缩放)
Canvas支持坐标系变换,灵活调整绘制位置和角度:dart
@override
void paint(Canvas canvas, Size size) {final Paint paint = Paint()..color = Colors.green..strokeWidth = 2;final Offset center = Offset(size.width / 2, size.height / 2);// 1. 平移坐标系(画布原点移到中心)canvas.translate(center.dx, center.dy);// 2. 旋转坐标系(顺时针旋转45°)canvas.rotate(pi / 4);// 3. 缩放坐标系(放大1.5倍)canvas.scale(1.5);// 绘制矩形(基于变换后的坐标系)canvas.drawRect(const Rect.fromLTWH(-50, -50, 100, 100),paint..style = PaintingStyle.stroke,);// 重置坐标系(避免影响后续绘制)canvas.restore();
}
四、常见问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 绘制内容不显示 | 1. 检查CustomPaint是否设置size;2. 确认绘制坐标在size范围内;3. 检查画笔颜色与背景是否一致 |
| 文本绘制乱码 / 不显示 | 必须指定TextPainter的textDirection(如TextDirection.ltr) |
| 图像绘制不显示 | 图像加载是异步的,需通过ImageStreamListener或FutureBuilder等待加载完成 |
| 绘制性能差、卡顿 | 1. 优化shouldRepaint();2. 使用RepaintBoundary;3. 减少复杂路径计算 |
五、总结
CustomPainter的核心是通过Canvas操作绘制指令,通过Paint定义样式,通过shouldRepaint控制性能,其使用流程可总结为:- 继承
CustomPainter,封装绘制参数; - 在
paint()中通过Canvas和Paint实现绘制逻辑; - 重写
shouldRepaint()优化重绘; - 通过
CustomPaint组件将绘制内容渲染到界面; - 进阶场景结合动画、坐标系变换实现复杂效果,并做好性能优化。
掌握以上方法,可实现从简单图形到复杂动效的所有自定义绘制需求,比如图表、个性化 UI、游戏画面等。