如何在Flutter中使用CustomPainter实现自定义绘制?

在 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的尺寸)
  @override
  void 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,避免无意义重绘
  @override
  bool shouldRepaint(covariant MyCustomPainter oldDelegate) {
    return oldDelegate.radius != radius || oldDelegate.primaryColor != primaryColor;
  }
}
 
步骤 2:通过CustomPaint组件渲染将自定义CustomPainter传入CustomPaintpainter参数,嵌入 Flutter 界面树中。
dart
 
 
 
 
 
class CustomPaintDemo extends StatelessWidget {
  const CustomPaintDemo({super.key});

  @override
  Widget 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. 结合动画实现动态绘制

通过AnimationControllerListener,让绘制参数随动画变化,实现动态效果:
dart
 
 
 
 
 
class AnimatedCustomPainter extends StatefulWidget {
  const AnimatedCustomPainter({super.key});

  @override
  State<AnimatedCustomPainter> createState() => _AnimatedCustomPainterState();
}

class _AnimatedCustomPainterState extends State<AnimatedCustomPainter>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _radiusAnimation;

  @override
  void 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),
    );
  }

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

  @override
  Widget 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包裹,避免其他组件动效触发绘制重绘;
    dart
     
     
     
     
     
    RepaintBoundary(
      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. 检查画笔颜色与背景是否一致
文本绘制乱码 / 不显示 必须指定TextPaintertextDirection(如TextDirection.ltr
图像绘制不显示 图像加载是异步的,需通过ImageStreamListenerFutureBuilder等待加载完成
绘制性能差、卡顿 1. 优化shouldRepaint();2. 使用RepaintBoundary;3. 减少复杂路径计算

五、总结

CustomPainter的核心是通过Canvas操作绘制指令,通过Paint定义样式,通过shouldRepaint控制性能,其使用流程可总结为:
  1. 继承CustomPainter,封装绘制参数;
  2. paint()中通过CanvasPaint实现绘制逻辑;
  3. 重写shouldRepaint()优化重绘;
  4. 通过CustomPaint组件将绘制内容渲染到界面;
  5. 进阶场景结合动画、坐标系变换实现复杂效果,并做好性能优化。
掌握以上方法,可实现从简单图形到复杂动效的所有自定义绘制需求,比如图表、个性化 UI、游戏画面等。
 
 
posted @ 2025-12-09 17:53  高手大8  阅读(4)  评论(0)    收藏  举报