Flutter CustomPainter:用代码作画的艺术
Flutter CustomPainter用代码作画的艺术引言Flutter 的 CustomPainter 是一个强大的工具它允许开发者直接在画布上绘制自定义图形。无论是创建复杂的数据可视化图表、自定义动画效果还是独特的 UI 组件CustomPainter 都能帮助你实现创意。本文将深入探讨 CustomPainter 的核心概念、使用方法和实际应用。一、CustomPainter 核心概念1.1 什么是 CustomPainterCustomPainter 是 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 StateProgressRing createState() _ProgressRingState(); } class _ProgressRingState extends StateProgressRing with SingleTickerProviderStateMixin { late AnimationController _controller; late Animationdouble _animation; override void initState() { super.initState(); _controller AnimationController( vsync: this, duration: const Duration(milliseconds: 500), ); _animation Tweendouble( 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 Tweendouble( 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 Listdouble data; final ListColor 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 Animationdouble 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 StateWaveAnimation createState() _WaveAnimationState(); } class _WaveAnimationState extends StateWaveAnimation with SingleTickerProviderStateMixin { late AnimationController _controller; late Animationdouble _animation; override void initState() { super.initState(); _controller AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); _animation Tweendouble(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 使用 RepaintBoundaryRepaintBoundary( 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与其他框架的更好集成参考资料Flutter Documentation: CustomPainterFlutter Documentation: CanvasFlutter Documentation: Paint