Flutter 自定义绘制完全指南

引言

Flutter 的自定义绘制功能允许开发者创建各种复杂的图形和动画效果。本文将深入探讨 CustomPainter 的各种用法和高级技巧。

基础概念回顾

核心组件

  • CustomPaint: 用于绘制自定义图形的 Widget
  • CustomPainter: 定义绘制逻辑的类
  • Canvas: 绘制画布
  • Paint: 画笔配置

基本语法

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 绘制逻辑
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

// 使用
CustomPaint(
  painter: MyPainter(),
  child: const Text('Content'),
)

高级技巧一:基础绘制

绘制线条

class LinePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;

    canvas.drawLine(
      const Offset(0, 0),
      Offset(size.width, size.height),
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

绘制圆形

class CirclePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = min(size.width, size.height) / 2;

    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;

    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

绘制矩形

class RectanglePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromLTWH(10, 10, size.width - 20, size.height - 20);

    final paint = Paint()
      ..color = Colors.green
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    canvas.drawRect(rect, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

绘制路径

class PathPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final path = Path()
      ..moveTo(0, size.height / 2)
      ..quadraticBezierTo(
        size.width / 4,
        0,
        size.width / 2,
        size.height / 2,
      )
      ..quadraticBezierTo(
        size.width * 3 / 4,
        size.height,
        size.width,
        size.height / 2,
      );

    final paint = Paint()
      ..color = Colors.purple
      ..strokeWidth = 3
      ..style = PaintingStyle.stroke;

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

高级技巧二:复杂绘制

绘制多边形

class PolygonPainter extends CustomPainter {
  final int sides;

  PolygonPainter({this.sides = 6});

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = min(size.width, size.height) / 2 - 10;

    final path = Path();
    for (int i = 0; i < sides; i++) {
      final angle = (i * 2 * pi) / sides - pi / 2;
      final x = center.dx + radius * cos(angle);
      final y = center.dy + radius * sin(angle);
      
      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }
    path.close();

    final paint = Paint()
      ..color = Colors.orange
      ..style = PaintingStyle.fill;

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

绘制渐变

class GradientPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final rect = Offset.zero & size;
    final gradient = LinearGradient(
      colors: [Colors.blue, Colors.purple],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    );

    final paint = Paint()..shader = gradient.createShader(rect);

    canvas.drawRect(rect, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

绘制文字

class TextPainterWidget extends StatelessWidget {
  const TextPainterWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: TextDrawingPainter(),
      size: const Size(200, 100),
    );
  }
}

class TextDrawingPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final textPainter = TextPainter(
      text: const TextSpan(
        text: 'Hello Flutter',
        style: TextStyle(
          color: Colors.black,
          fontSize: 24,
          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,
      ),
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

高级技巧三:动画绘制

动画圆形

class AnimatedCircle extends StatefulWidget {
  const AnimatedCircle({super.key});

  @override
  State<AnimatedCircle> createState() => _AnimatedCircleState();
}

class _AnimatedCircleState extends State<AnimatedCircle>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _radiusAnimation;

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

    _radiusAnimation = Tween<double>(begin: 20, end: 100).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return CustomPaint(
          painter: AnimatedCirclePainter(_radiusAnimation.value),
          size: const Size(200, 200),
        );
      },
    );
  }
}

class AnimatedCirclePainter extends CustomPainter {
  final double radius;

  AnimatedCirclePainter(this.radius);

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);

    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

实战案例:饼图

class PieChartPainter extends CustomPainter {
  final List<double> data;
  final List<Color> colors;

  PieChartPainter({required this.data, required this.colors});

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = min(size.width, size.height) / 2 - 10;

    double startAngle = -pi / 2;
    final total = data.reduce((a, b) => a + b);

    for (int i = 0; i < data.length; i++) {
      final sliceAngle = (data[i] / total) * 2 * pi;

      final path = Path()
        ..moveTo(center.dx, center.dy)
        ..arcTo(
          Rect.fromCircle(center: center, radius: radius),
          startAngle,
          sliceAngle,
          false,
        )
        ..close();

      final paint = Paint()..color = colors[i];
      canvas.drawPath(path, paint);

      startAngle += sliceAngle;
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

// 使用
CustomPaint(
  painter: PieChartPainter(
    data: [30, 20, 50],
    colors: [Colors.red, Colors.green, Colors.blue],
  ),
  size: const Size(200, 200),
)

实战案例:进度条

class ProgressPainter extends CustomPainter {
  final double progress;

  ProgressPainter({required this.progress});

  @override
  void paint(Canvas canvas, Size size) {
    final backgroundPaint = Paint()
      ..color = Colors.grey[200]!
      ..style = PaintingStyle.fill;

    final foregroundPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    final borderRadius = BorderRadius.circular(size.height / 2);
    final backgroundRect = Rect.fromLTWH(0, 0, size.width, size.height);

    canvas.drawRRect(
      RRect.fromRectAndRadius(backgroundRect, borderRadius),
      backgroundPaint,
    );

    final foregroundRect = Rect.fromLTWH(0, 0, size.width * progress, size.height);
    canvas.drawRRect(
      RRect.fromRectAndRadius(foregroundRect, borderRadius),
      foregroundPaint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

实战案例:波形图

class WaveformPainter extends CustomPainter {
  final List<double> data;

  WaveformPainter({required this.data});

  @override
  void paint(Canvas canvas, Size size) {
    if (data.isEmpty) return;

    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;

    final path = Path();
    final stepX = size.width / (data.length - 1);

    for (int i = 0; i < data.length; i++) {
      final x = i * stepX;
      final y = size.height - (data[i] * size.height);

      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

常见问题与解决方案

Q1:绘制不显示?

A:确保设置了大小:

CustomPaint(
  painter: MyPainter(),
  size: const Size(200, 200), // 设置大小
)

Q2:动画卡顿?

A:使用 repaintBoundary

RepaintBoundary(
  child: CustomPaint(painter: AnimatedPainter()),
)

Q3:文字不显示?

A:设置 textDirection

TextPainter(
  text: const TextSpan(text: 'Hello'),
  textDirection: TextDirection.ltr, // 必须设置
)

最佳实践

1. 优化绘制性能

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 只在必要时重绘
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false; // 不需要重绘
  }
}

2. 复用 Paint 对象

class MyPainter extends CustomPainter {
  final Paint _paint = Paint()..color = Colors.blue;

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawCircle(Offset(50, 50), 20, _paint);
  }
}

3. 使用 save/restore

@override
void paint(Canvas canvas, Size size) {
  canvas.save();
  canvas.translate(50, 50);
  canvas.rotate(pi / 4);
  canvas.drawRect(const Rect.fromLTWH(0, 0, 100, 100), _paint);
  canvas.restore();
}

总结

Flutter 的自定义绘制功能非常强大。通过本文的学习,你应该能够:

  1. 绘制基本图形(线条、圆形、矩形、路径)
  2. 创建复杂图形(多边形、渐变、文字)
  3. 实现动画绘制
  4. 创建各种图表(饼图、进度条、波形图)
  5. 优化绘制性能

掌握这些技巧,能够帮助你创建更加复杂和吸引人的视觉效果。

更多推荐