当内置Widget无法满足你的视觉需求时,CustomPaint和Canvas是你的终极武器。本课将深入Flutter的自定义绘制体系——从基本图形绘制到复杂动画,从仪表盘到粒子系统,释放你的创造力。
// CustomPaint基本结构
CustomPaint(
size: Size(300, 300), // 绘制区域大小
painter: MyPainter(), // 背景绘制器
foregroundPainter: MyForegroundPainter(), // 前景绘制器
child: Text('我在Canvas上面'), // 子Widget在painter之上
)
// 自定义Painter
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 创建画笔
final paint = Paint()
..color = Colors.blue
..strokeWidth = 3
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// 画圆
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
size.width / 3,
paint,
);
// 画线
canvas.drawLine(
Offset(0, size.height / 2),
Offset(size.width, size.height / 2),
paint..color = Colors.red,
);
// 画矩形
canvas.drawRect(
Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: size.width * 0.6,
height: size.height * 0.4,
),
paint..color = Colors.green,
);
}
@override
bool shouldRepaint(covariant MyPainter oldDelegate) => false; // 静态图不重绘
}
class GradientPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 线性渐变
final linearGradient = const LinearGradient(
colors: [Colors.purple, Colors.blue, Colors.cyan],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
final rect = Offset.zero & size;
final paint = Paint()
..shader = linearGradient.createShader(rect)
..style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
// 径向渐变
final radialPaint = Paint()
..shader = const RadialGradient(
colors: [Colors.yellow, Colors.orange, Colors.red],
center: Alignment(0.3, -0.3),
radius: 0.8,
).createShader(rect);
canvas.drawCircle(
Offset(size.width * 0.65, size.height * 0.35),
size.width * 0.25,
radialPaint,
);
// 扫描渐变(Sweep)
final sweepPaint = Paint()
..shader = const SweepGradient(
colors: [Colors.red, Colors.yellow, Colors.green, Colors.blue, Colors.red],
startAngle: 0,
endAngle: 3.14 * 2,
).createShader(rect);
canvas.drawCircle(
Offset(size.width * 0.3, size.height * 0.7),
size.width * 0.2,
sweepPaint,
);
}
@override
bool shouldRepaint(covariant GradientPainter old) => false;
}
class GaugePainter extends CustomPainter {
final double value; // 0.0 - 1.0
final Color activeColor;
GaugePainter({required this.value, this.activeColor = Colors.blue});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 20;
// 背景弧
final bgPaint = Paint()
..color = Colors.grey[300]!
..style = PaintingStyle.stroke
..strokeWidth = 12
..strokeCap = StrokeCap.round;
const startAngle = 0.75 * 3.14; // 135度
const sweepAngle = 1.5 * 3.14; // 270度
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle, sweepAngle, false, bgPaint,
);
// 前景弧(带渐变)
final fgPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 12
..strokeCap = StrokeCap.round
..shader = LinearGradient(
colors: [Colors.green, activeColor, Colors.red],
).createShader(Rect.fromCircle(center: center, radius: radius));
final valueAngle = startAngle + sweepAngle * value;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle, sweepAngle * value, false, fgPaint,
);
// 刻度线
final tickPaint = Paint()
..color = Colors.grey[600]!
..strokeWidth = 2;
for (int i = 0; i <= 10; i++) {
final angle = startAngle + sweepAngle * (i / 10);
final innerRadius = radius - 20;
final outerRadius = radius - (i % 5 == 0 ? 30 : 25);
canvas.drawLine(
Offset(center.dx + innerRadius * cos(angle),
center.dy + innerRadius * sin(angle)),
Offset(center.dx + outerRadius * cos(angle),
center.dy + outerRadius * sin(angle)),
tickPaint..strokeWidth = i % 5 == 0 ? 3 : 1,
);
}
// 中心数值
final textPainter = TextPainter(
text: TextSpan(
text: '${(value * 100).toInt()}',
style: TextStyle(
fontSize: 48, fontWeight: FontWeight.bold,
color: activeColor,
),
),
textDirection: TextDirection.ltr,
)..layout();
textPainter.paint(
center - Offset(textPainter.width / 2, textPainter.height / 2),
);
}
@override
bool shouldRepaint(covariant GaugePainter old) => value != old.value;
}
// 使用动画驱动
class AnimatedGauge extends StatefulWidget {
final double targetValue;
@override
State<AnimatedGauge> createState() => _AnimatedGaugeState();
}
class _AnimatedGaugeState extends State<AnimatedGauge>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
_controller.forward();
}
@override
void didUpdateWidget(AnimatedGauge old) {
super.didUpdateWidget(old);
if (old.targetValue != widget.targetValue) {
_controller.reset();
_controller.forward();
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, _) => CustomPaint(
size: const Size(200, 200),
painter: GaugePainter(value: widget.targetValue * _animation.value),
),
);
}
}
class ParticleSystemPainter extends CustomPainter {
final List<Particle> particles;
ParticleSystemPainter(this.particles);
@override
void paint(Canvas canvas, Size size) {
for (final p in particles) {
final paint = Paint()
..color = p.color.withOpacity(p.opacity)
..style = PaintingStyle.fill;
canvas.drawCircle(p.position, p.radius, paint);
// 拖尾效果
if (p.trail.isNotEmpty) {
final trailPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = p.radius * 0.5
..strokeCap = StrokeCap.round;
for (int i = 1; i < p.trail.length; i++) {
trailPaint.color = p.color.withOpacity(
p.opacity * (i / p.trail.length) * 0.5,
);
canvas.drawLine(p.trail[i - 1], p.trail[i], trailPaint);
}
}
}
}
@override
bool shouldRepaint(covariant ParticleSystemPainter old) => true;
}
class Particle {
Offset position;
Offset velocity;
double radius;
Color color;
double opacity;
List<Offset> trail;
Particle({
required this.position, required this.velocity,
required this.radius, required this.color,
this.opacity = 1.0, this.trail = const [],
});
void update() {
trail = [...trail, position];
if (trail.length > 8) trail = trail.sublist(trail.length - 8);
position += velocity;
velocity = Offset(velocity.dx * 0.99, velocity.dy + 0.1); // 重力
opacity -= 0.01;
}
}
环形图是数据可视化的常用组件。结合CustomPainter和AnimationController,创建带弹出动画的环形图。
class DonutChartPainter extends CustomPainter {
final List<ChartSegment> segments;
final double animationProgress; // 0.0 ~ 1.0
DonutChartPainter({
required this.segments,
required this.animationProgress,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 20;
final strokeWidth = radius * 0.35;
final innerRadius = radius - strokeWidth;
double startAngle = -3.14 / 2; // 从12点方向开始
final total = segments.fold(0.0, (sum, s) => sum + s.value);
for (final segment in segments) {
final sweepAngle = (segment.value / total) * 2 * 3.14 * animationProgress;
final paint = Paint()
..color = segment.color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.butt;
// 绘制弧线
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius - strokeWidth / 2),
startAngle, sweepAngle, false, paint,
);
// 间隔线
final gapAngle = 0.02; // 间隔角度
startAngle += sweepAngle + gapAngle;
}
// 中心文字
final textPainter = TextPainter(
text: TextSpan(
text: '${(animationProgress * 100).toInt()}%',
style: TextStyle(
fontSize: radius * 0.35,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
textDirection: TextDirection.ltr,
)..layout();
textPainter.paint(
center - Offset(textPainter.width / 2, textPainter.height / 2),
);
}
@override
bool shouldRepaint(covariant DonutChartPainter old) =>
animationProgress != old.animationProgress ||
segments != old.segments;
}
class ChartSegment {
final String label;
final double value;
final Color color;
const ChartSegment({required this.label, required this.value, required this.color});
}
// 使用
final segments = [
ChartSegment(label: '食品', value: 1200, color: Colors.red),
ChartSegment(label: '交通', value: 800, color: Colors.blue),
ChartSegment(label: '娱乐', value: 600, color: Colors.green),
ChartSegment(label: '购物', value: 900, color: Colors.orange),
];
Path是Canvas最强大的工具之一——贝塞尔曲线让你绘制任意光滑曲线。
class WavePainter extends CustomPainter {
final double animationValue;
WavePainter({required this.animationValue});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.fill;
// 绘制多层波浪
for (int wave = 0; wave < 3; wave++) {
final path = Path();
final baseY = size.height * (0.5 + wave * 0.12);
final amplitude = 20.0 - wave * 5;
final speed = 1.0 + wave * 0.3;
path.moveTo(0, baseY);
for (double x = 0; x <= size.width; x++) {
final y = baseY +
amplitude * sin((x / size.width * 2 * 3.14 * speed) +
animationValue * 2 * 3.14 * speed);
path.lineTo(x, y);
}
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
paint.color = [
Colors.blue.withOpacity(0.3),
Colors.blue.withOpacity(0.2),
Colors.blue.withOpacity(0.1),
][wave];
canvas.drawPath(path, paint);
}
}
@override
bool shouldRepaint(covariant WavePainter old) => true;
}
// 贝塞尔曲线示例:绘制光滑的图表线条
Path drawSmoothLine(List<Offset> points) {
final path = Path();
if (points.isEmpty) return path;
path.moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length - 1; i++) {
final current = points[i];
final next = points[i + 1];
final controlX = (current.dx + next.dx) / 2;
final controlY = (current.dy + next.dy) / 2;
path.quadraticBezierTo(
current.dx, current.dy,
controlX, controlY,
);
}
// 最后一个点
path.lineTo(points.last.dx, points.last.dy);
return path;
}
你已掌握自定义绘制与Canvas!
下一课预告:平台适配与响应式设计——让你的App在手机/平板/桌面都能完美展现!