Flutter CustomPainter:用代码作画的艺术

引言

Flutter 的 CustomPainter 是一个强大的工具,它允许开发者直接在画布上绘制自定义图形。无论是创建复杂的数据可视化图表、自定义动画效果,还是独特的 UI 组件,CustomPainter 都能帮助你实现创意。本文将深入探讨 CustomPainter 的核心概念、使用方法和实际应用。

一、CustomPainter 核心概念

1.1 什么是 CustomPainter

CustomPainter 是 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
  State<ProgressRing> createState() => _ProgressRingState();
}

class _ProgressRingState extends State<ProgressRing> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );

    _animation = Tween<double>(
      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 = Tween<double>(
        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 List<double> data;
  final List<Color> 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 Animation<double> 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
  State<WaveAnimation> createState() => _WaveAnimationState();
}

class _WaveAnimationState extends State<WaveAnimation> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat();

    _animation = Tween<double>(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 使用 RepaintBoundary

RepaintBoundary(
  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 开发者提供了无限的创意空间:

  1. 自定义图形:创建独特的 UI 组件
  2. 数据可视化:绘制图表、仪表盘等
  3. 动画效果:实现复杂的动画效果
  4. 性能优化:精细控制绘制过程

8.2 最佳实践建议

  1. 分离绘制逻辑:将复杂的绘制逻辑分解为多个方法
  2. 使用缓存:对于静态内容使用 Picture 缓存
  3. 性能监控:使用 Flutter DevTools 监控绘制性能
  4. 代码组织:将自定义绘制器放在单独的文件中

8.3 未来发展趋势

随着 Flutter 的发展,CustomPainter 也在不断进化:

  • 更好的性能优化工具
  • 更丰富的绘制 API
  • 与其他框架的更好集成

参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐