【第五阶段—高级特性和架构】第七章:CustomPainter—绘图大师

文章目录

从零开始学会自定义绘制

循序渐进掌握Flutter最强大的绘图工具


第一步:什么是CustomPainter?

生活比喻

想象你是一个画家,Flutter给了你:

Canvas(画布) = 你的绘图区域,就像白纸

Paint(画笔) = 设置颜色、粗细的工具

绘图方法 = 不同的绘画技法(画线、画圆等)

为什么需要CustomPainter?

标准Widget的局限性:

  • Container只能画矩形
  • CircleAvatar只能画圆形
  • Icon只能显示预设图标

CustomPainter的强大之处:

  • 任意形状:星星、心形、复杂图案
  • 数据可视化:图表、进度条、仪表盘
  • 游戏图形:角色、道具、特效
  • 艺术效果:渐变、阴影、纹理

使用场景对比

需求 标准Widget CustomPainter
简单矩形 Container 过度设计
圆形头像 CircleAvatar 不必要
五角星 无法实现 完美解决
柱状图 复杂组合 简单直接
自定义图标 受限制 无限可能

第二步:基础知识详解

CustomPainter的基本结构

CustomPainter就像一个画家的工作模板:

🎨 继承关系 :所有自定义绘制器都必须继承CustomPainter

📝 两个核心方法 :每个绘制器都需要实现两个关键方法

🔄 生命周期管理:Flutter会在合适的时机调用这些方法

基本结构解析:

  1. paint方法 - 🎨 绘制核心

    • 这里是你的"创作空间"
    • 接收Canvas(画布)和Size(尺寸)参数
    • 所有的绘制逻辑都在这里实现
  2. shouldRepaint方法 - ⚡ 性能优化

    • 决定何时需要重新绘制
    • 返回true:重新绘制(数据变化时)
    • 返回false:使用缓存(静态内容时)
dart 复制代码
class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 在这里进行绘制
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // 决定是否需要重新绘制
    return false;
  }
}

Canvas - 你的画布

Canvas提供了丰富的绘制方法:

dart 复制代码
// 绘制基本图形
canvas.drawCircle(center, radius, paint);     // 圆形
canvas.drawRect(rect, paint);                 // 矩形
canvas.drawLine(p1, p2, paint);              // 直线
canvas.drawOval(rect, paint);                // 椭圆

// 绘制复杂图形
canvas.drawPath(path, paint);                // 路径
canvas.drawRRect(rrect, paint);              // 圆角矩形
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint); // 弧形

// 绘制文本和图像
canvas.drawParagraph(paragraph, offset);     // 段落文本
canvas.drawImage(image, offset, paint);      // 图像

Paint - 你的画笔

Paint控制绘制的样式:

dart 复制代码
final paint = Paint()
  ..color = Colors.blue              // 颜色
  ..strokeWidth = 2.0               // 线条粗细
  ..style = PaintingStyle.fill      // 填充样式
  ..strokeCap = StrokeCap.round     // 线条端点样式
  ..strokeJoin = StrokeJoin.round   // 线条连接样式
  ..isAntiAlias = true;             // 抗锯齿

Paint样式详解:

属性 说明 常用值
color 颜色 Colors.red, Color(0xFF123456)
style 样式 fill(填充), stroke(描边)
strokeWidth 线宽 1.0, 2.5, 10.0
strokeCap 端点 round(圆形), square(方形)
isAntiAlias 抗锯齿 true(平滑), false(锐利)

Path - 复杂路径

Path用于绘制复杂形状:

dart 复制代码
final path = Path()
  ..moveTo(x1, y1)        // 移动到起点
  ..lineTo(x2, y2)        // 画直线到
  ..quadraticBezierTo(    // 二次贝塞尔曲线
      cpx, cpy, x3, y3)
  ..cubicTo(              // 三次贝塞尔曲线
      cp1x, cp1y, cp2x, cp2y, x4, y4)
  ..close();              // 闭合路径

坐标系统

Flutter的坐标系统:

text 复制代码
(0,0) ────────── X轴 →
  │
  │    屏幕区域
  │
  │
  Y轴
  ↓

重要概念:

  • 原点(0,0)在左上角
  • X轴向右为正
  • Y轴向下为正
  • Size.width 和 Size.height 是绘制区域的尺寸

为什么是Canvas绘制而不是Paint?

🎨 Canvas = 画布 :负责"在哪里画"和"画什么形状"

🖌️ Paint = 画笔:负责"用什么样式画"(颜色、粗细等)

就像现实中画画:你在画布上画圆形,用画笔决定颜色。Canvas决定动作,Paint决定样式!


第三步:实战演示

从简单开始:绘制一个圆

dart 复制代码
class CirclePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;
    
    // 在中心绘制圆形
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 4;
    
    canvas.drawCircle(center, radius, paint);
  }

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

进阶:完整项目演示

dart 复制代码
import 'package:flutter/material.dart';
import 'dart:math' as math; // 导入数学库,用于三角函数计算

/// 应用程序入口点
/// 创建一个展示CustomPainter功能的演示应用
void main() {
  runApp(MaterialApp(
    home: CustomPainterDemo(),
  ));
}

/// CustomPainter演示页面
/// 这个页面展示了CustomPainter的两个实用示例:
/// 1. 星星绘制 - 展示如何使用Path绘制复杂形状
/// 2. 柱状图 - 展示如何绘制数据可视化图表
class CustomPainterDemo extends StatelessWidget {
  /// 构建页面UI结构
  /// 使用Column垂直排列两个演示组件
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('CustomPainter演示')),
      body: Padding(
        padding: EdgeInsets.all(20), // 为整个页面添加20像素的内边距
        child: Column(
          children: [
            // 星星图案演示区域
            Text('自定义星星', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            SizedBox(height: 16), // 添加垂直间距
            
            // 五角星组件 - 展示复杂形状的绘制
            StarPainter(
              size: 100,           // 星星大小:100x100像素
              color: Colors.yellow, // 星星颜色:黄色
              points: 5,           // 星星角数:5个角
            ),
            
            SizedBox(height: 30), // 区域之间的较大间距
            
            // 柱状图演示区域
            Text('自定义柱状图', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            SizedBox(height: 16),
            
            // 柱状图组件 - 展示数据可视化
            BarChartPainter(
              data: [0.3, 0.7, 0.5, 0.9, 0.4], // 数据数组,值范围 0.0-1.0
              width: 300,                      // 图表宽度:300像素
              height: 150,                     // 图表高度:150像素
            ),
          ],
        ),
      ),
    );
  }
}

/// 星星绘制器组件(就像用圆规画星星)
/// 
/// 这是一个可自定义的星星绘制组件,具有以下特性:
/// - 可调节星星的大小和颜色
/// - 可设置星星的角数(默认5角)
/// - 使用数学计算精确绘制对称的星形
/// - 适用于装饰、评分显示等场景
class StarPainter extends StatelessWidget {
  /// 星星的大小(宽度和高度),单位为像素
  final double size;
  
  /// 星星的颜色
  final Color color;
  
  /// 星星的角数,默认5角(五角星)
  final int points;

  /// 构造函数
  /// [size] 星星大小,必需参数
  /// [color] 星星颜色,必需参数
  /// [points] 星星角数,默认5角
  const StarPainter({
    Key? key,
    required this.size,
    required this.color,
    this.points = 5,
  }) : super(key: key);

  /// 构建星星组件的UI
  /// 使用CustomPaint绘制星形,将绘制逻辑委托给_StarPainter
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(size, size), // 设置绘制区域为正方形
      painter: _StarPainter(color: color, points: points), // 使用自定义绘制器
    );
  }
}

/// 星星绘制器的CustomPainter实现
/// 
/// 这个类负责在Canvas上绘制星形,使用数学计算确定每个点的位置
class _StarPainter extends CustomPainter {
  /// 星星的颜色
  final Color color;
  
  /// 星星的角数
  final int points;

  /// 构造函数
  /// 初始化绘制星星所需的参数
  _StarPainter({required this.color, required this.points});

  /// 核心绘制方法
  /// 使用数学计算和Path绘制对称的星形
  /// [canvas] 绘制画布
  /// [size] 绘制区域大小
  @override
  void paint(Canvas canvas, Size size) {
    // 创建画笔,设置填充样式
    final paint = Paint()
      ..color = color                    // 设置星星颜色
      ..style = PaintingStyle.fill;      // 填充样式,不是只绘边框

    // 创建绘制路径,用于连接星星的所有点
    final path = Path();
    
    // 计算星星的中心点和半径
    final center = Offset(size.width / 2, size.height / 2); // 中心点
    final outerRadius = size.width / 2;                     // 外半径(星星尖角的距离)
    final innerRadius = outerRadius * 0.4;                 // 内半径(星星凹陷的距离)

    // 计算星星的各个点(就像连接星座的点)
    // 每个星角需要两个点:一个尖角点(外半径)和一个凹陷点(内半径)
    for (int i = 0; i < points * 2; i++) {
      // 计算当前点的角度(从顶部开始,顺时针方向)
      final angle = (i * math.pi) / points - math.pi / 2;
      
      // 交替使用外半径和内半径:偶数索引使用外半径(尖角),奇数索引使用内半径(凹陷)
      final radius = i.isEven ? outerRadius : innerRadius;
      
      // 使用三角函数计算点的坐标
      final x = center.dx + radius * math.cos(angle); // x坐标
      final y = center.dy + radius * math.sin(angle); // y坐标

      // 构建路径:第一个点移动到,其余点连线到
      if (i == 0) {
        path.moveTo(x, y); // 移动到起始点
      } else {
        path.lineTo(x, y); // 从当前点连线到新点
      }
    }
    
    // 关闭路径,连接最后一个点到第一个点
    path.close();

    // 在画布上绘制星形路径
    canvas.drawPath(path, paint);
  }

  /// 判断是否需要重新绘制
  /// 返回false表示星星形状不变,不需要重新绘制,提高性能
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

/// 柱状图绘制器组件(就像画家画柱子)
/// 
/// 这是一个简单而实用的柱状图组件,具有以下特性:
/// - 支持多个数据点的显示
/// - 自动计算柱子宽度和间距
/// - 彩虹色柱子,美观易读
/// - 显示百分比数值标签
/// - 圆角设计,现代化外观
class BarChartPainter extends StatelessWidget {
  /// 数据数组,每个值的范围为 0.0-1.0,代表百分比
  final List<double> data;
  
  /// 柱状图的宽度,单位为像素
  final double width;
  
  /// 柱状图的高度,单位为像素
  final double height;

  /// 构造函数
  /// [data] 数据数组,必需参数
  /// [width] 图表宽度,必需参数
  /// [height] 图表高度,必需参数
  const BarChartPainter({
    Key? key,
    required this.data,
    required this.width,
    required this.height,
  }) : super(key: key);

  /// 构建柱状图组件的UI
  /// 使用CustomPaint绘制柱状图,将绘制逻辑委托给_BarChartPainter
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(width, height), // 设置绘制区域大小
      painter: _BarChartPainter(data: data), // 使用自定义绘制器
    );
  }
}

/// 柱状图绘制器的CustomPainter实现
/// 
/// 这个类负责在Canvas上绘制柱状图,包括柱子和数值标签
class _BarChartPainter extends CustomPainter {
  /// 数据数组
  final List<double> data;

  /// 构造函数
  /// 初始化绘制柱状图所需的数据
  _BarChartPainter({required this.data});

  /// 核心绘制方法
  /// 绘制柱状图的所有元素:柱子、颜色、标签
  /// [canvas] 绘制画布
  /// [size] 绘制区域大小
  @override
  void paint(Canvas canvas, Size size) {
    // 计算柱子的宽度和间距
    // 每个柱子占据80%的空间,20%用作间距
    final barWidth = size.width / data.length * 0.8;  // 柱子宽度
    final spacing = size.width / data.length * 0.2;   // 间距宽度

    // 遍历所有数据点,绘制每个柱子
    for (int i = 0; i < data.length; i++) {
      // 计算柱子的尺寸和位置
      final barHeight = size.height * data[i];                    // 柱子高度:数据值 × 总高度
      final left = i * (barWidth + spacing) + spacing / 2;       // 柱子左边位置
      final top = size.height - barHeight;                       // 柱子顶部位置(从底部向上)

      // 为每个柱子设置不同颜色(就像彩虹色彩)
      // 使用HSV颜色模式创建彩虹效果:色相均匀分布,饱和度和亮度固定
      final paint = Paint()
        ..color = HSVColor.fromAHSV(
          1.0,                           // 透明度:完全不透明
          i * 360.0 / data.length,       // 色相:根据索引均匀分布在360度范围内
          0.8,                           // 饱和度:80%,颜色鲜艳但不过于刺眼
          0.9,                           // 亮度:90%,明亮但不过于刺眼
        ).toColor()
        ..style = PaintingStyle.fill;    // 填充样式

      // 创建柱子的矩形区域
      final rect = Rect.fromLTWH(left, top, barWidth, barHeight);
      
      // 绘制圆角柱子(使用RRect而不是普通矩形)
      canvas.drawRRect(
        RRect.fromRectAndRadius(rect, Radius.circular(4)), // 4像素圆角
        paint,
      );

      // 绘制数值标签(就像给柱子贴标签)
      // 使用TextPainter在Canvas上绘制文本
      final textPainter = TextPainter(
        text: TextSpan(
          text: '${(data[i] * 100).toInt()}%', // 将数据转换为百分比显示
          style: TextStyle(
            color: Colors.black87,           // 深灰色文字,清晰可见
            fontSize: 12,                   // 字体大小
            fontWeight: FontWeight.bold,     // 粗体,增加可读性
          ),
        ),
        textDirection: TextDirection.ltr,   // 文本方向:从左到右
      );

      // 计算文本尺寸和位置
      textPainter.layout(); // 计算文本的实际尺寸
      
      // 绘制文本在柱子顶部中心位置
      textPainter.paint(
        canvas,
        Offset(
          left + barWidth / 2 - textPainter.width / 2,  // 水平居中:柱子中心 - 文本宽度的一半
          top - textPainter.height - 4,                 // 垂直位置:柱子顶部上方4像素
        ),
      );
    }
  }

  /// 判断是否需要重新绘制
  /// 返回true表示数据可能变化,需要重新绘制
  /// 在实际项目中,可以比较数据是否变化来优化性能
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

第四步:知识点全面总结

CustomPainter核心概念

1. 三大核心组件
组件 作用 比喻
Canvas 绘制表面 🎨 画布
Paint 绘制样式 🖌️ 画笔
Size 绘制区域 📏 画框
2. 生命周期方法
dart 复制代码
class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 🎨 核心绘制逻辑
    // 每次需要绘制时都会调用
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // 🔄 性能优化关键
    // true: 重新绘制  false: 使用缓存
    return false;
  }
}

🖌️ Canvas绘制方法大全

基础图形
dart 复制代码
// 🔵 圆形和椭圆
canvas.drawCircle(center, radius, paint);
canvas.drawOval(rect, paint);

// 📐 矩形和圆角矩形
canvas.drawRect(rect, paint);
canvas.drawRRect(rrect, paint);

// 📏 线条
canvas.drawLine(p1, p2, paint);
canvas.drawPoints(PointMode.lines, points, paint);

// 🌙 弧形
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
复杂图形
dart 复制代码
// 🗺️ 路径绘制
canvas.drawPath(path, paint);

// 📝 文本绘制
canvas.drawParagraph(paragraph, offset);

// 🖼️ 图像绘制
canvas.drawImage(image, offset, paint);
canvas.drawImageRect(image, src, dst, paint);

🎨 Paint属性详解

颜色设置
dart 复制代码
paint.color = Colors.red;                    // 纯色
paint.color = Color(0xFF123456);            // 十六进制
paint.color = Color.fromRGBO(255, 0, 0, 1); // RGBA
paint.color = HSVColor.fromAHSV(1, 120, 0.8, 0.9).toColor(); // HSV
样式设置
dart 复制代码
// 填充样式
paint.style = PaintingStyle.fill;    // 填充
paint.style = PaintingStyle.stroke;  // 描边

// 线条样式
paint.strokeWidth = 2.0;             // 线宽
paint.strokeCap = StrokeCap.round;   // 端点:round, square, butt
paint.strokeJoin = StrokeJoin.round; // 连接:round, miter, bevel

// 其他属性
paint.isAntiAlias = true;            // 抗锯齿
paint.filterQuality = FilterQuality.high; // 滤镜质量

🗺️ Path路径操作

基础路径
dart 复制代码
final path = Path();

// 移动和连线
path.moveTo(x, y);        // 移动到点
path.lineTo(x, y);        // 直线到点
path.close();             // 闭合路径

// 弧线
path.arcTo(rect, startAngle, sweepAngle, forceMoveTo);
path.quadraticBezierTo(cpx, cpy, x, y);  // 二次贝塞尔
path.cubicTo(cp1x, cp1y, cp2x, cp2y, x, y); // 三次贝塞尔
路径操作
dart 复制代码
// 路径变换
path.transform(matrix4);
path.shift(offset);

// 路径组合
Path.combine(PathOperation.union, path1, path2);     // 并集
Path.combine(PathOperation.intersect, path1, path2); // 交集
Path.combine(PathOperation.difference, path1, path2); // 差集

📏 坐标系统和变换

坐标系统
dart 复制代码
// Flutter坐标系(左上角为原点)
(0,0) ────────── (width,0)
  │                  │
  │     绘制区域      │
  │                  │
(0,height) ──── (width,height)
Canvas变换
dart 复制代码
// 平移
canvas.translate(dx, dy);

// 旋转(弧度)
canvas.rotate(angle);

// 缩放
canvas.scale(sx, sy);

// 保存和恢复状态
canvas.save();    // 保存当前变换状态
// ... 进行变换和绘制
canvas.restore(); // 恢复到保存的状态

性能优化技巧

shouldRepaint优化
dart 复制代码
@override
bool shouldRepaint(MyPainter oldDelegate) {
  // ✅ 好的做法:比较具体属性
  return color != oldDelegate.color || 
         size != oldDelegate.size;
  
  // ❌ 避免:总是返回true
  // return true;
  
  // ❌ 避免:总是返回false(数据变化时不更新)
  // return false;
}
绘制优化
dart 复制代码
@override
void paint(Canvas canvas, Size size) {
  // ✅ 复用Paint对象
  final paint = Paint()..color = Colors.blue;
  
  // ✅ 避免在循环中创建对象
  final rect = Rect.fromLTWH(0, 0, 100, 100);
  for (int i = 0; i < 10; i++) {
    canvas.drawRect(rect.shift(Offset(i * 20, 0)), paint);
  }
  
  // ❌ 避免:在循环中创建新对象
  // for (int i = 0; i < 10; i++) {
  //   final paint = Paint()..color = Colors.blue; // 每次都创建
  //   canvas.drawRect(Rect.fromLTWH(i * 20, 0, 100, 100), paint);
  // }
}

🎯 实用技巧和最佳实践

文本绘制
dart 复制代码
void drawText(Canvas canvas, String text, Offset position) {
  final textPainter = TextPainter(
    text: TextSpan(
      text: text,
      style: TextStyle(color: Colors.black, fontSize: 16),
    ),
    textDirection: TextDirection.ltr,
  );
  
  textPainter.layout();
  textPainter.paint(canvas, position);
}
渐变效果
dart 复制代码
final gradient = LinearGradient(
  colors: [Colors.blue, Colors.red],
  begin: Alignment.topLeft,
  end: Alignment.bottomRight,
);

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

canvas.drawRect(rect, paint);
阴影效果
dart 复制代码
final shadowPaint = Paint()
  ..color = Colors.black.withOpacity(0.3)
  ..maskFilter = MaskFilter.blur(BlurStyle.normal, 3);

// 先绘制阴影
canvas.drawCircle(center.translate(2, 2), radius, shadowPaint);
// 再绘制主体
canvas.drawCircle(center, radius, mainPaint);

🚀 进阶学习方向

🎮 动画绘制

原理:通过Animation提供的0.0-1.0变化值,实时改变绘制参数,产生动画效果。

dart 复制代码
class AnimatedPainter extends CustomPainter {
  final Animation<double> animation;
  
  AnimatedPainter(this.animation) : super(repaint: animation);
  
  @override
  void paint(Canvas canvas, Size size) {
    // 使用animation.value进行动画绘制
    final progress = animation.value;
    // ... 绘制逻辑
  }
}

🖱️ 交互绘制

原理:收集用户触摸事件的坐标点,存储在列表中,然后在绘制时连接这些点形成轨迹。

dart 复制代码
class InteractivePainter extends CustomPainter {
  final List<Offset> points;
  
  InteractivePainter(this.points);
  
  @override
  void paint(Canvas canvas, Size size) {
    // 绘制用户触摸的轨迹
    if (points.isNotEmpty) {
      final path = Path()..moveTo(points.first.dx, points.first.dy);
      for (final point in points.skip(1)) {
        path.lineTo(point.dx, point.dy);
      }
      canvas.drawPath(path, paint);
    }
  }
}

📊 数据可视化

  • 饼图 :使用drawArc绘制扇形
  • 折线图 :使用Path连接数据点
  • 散点图 :使用drawCircle绘制数据点
  • 热力图:使用颜色渐变表示数据密度

🎨 艺术效果

  • 粒子系统:绘制大量小图形模拟粒子
  • 波浪效果:使用正弦函数创建波浪路径
  • 几何图案:使用数学函数创建复杂图案
  • 滤镜效果 :使用ColorFilterImageFilter

💡 总结

CustomPainter是Flutter中最强大的绘图工具,掌握它可以让你:

创造无限可能的UI效果

实现高性能的自定义组件

开发专业的数据可视化

制作精美的动画和交互

记住关键点:

  • 🎨 Canvas是你的画布,Paint是你的画笔
  • 📏 理解坐标系统和变换
  • ⚡ 合理使用shouldRepaint优化性能
  • 🔄 结合动画创造动态效果

绘图学的就是一种思想,学会了CustomPainter的绘画方式其实也就掌握了其它平台上的绘画方式,比如:Android上的自定义组件、Js上的绘制组件都是大同小异。希望你能掌握学习技巧,继续探索CustomPainter的无限可能,创造出独特的视觉效果!🎨✨

相关推荐
用户7502734994771 小时前
我用百度文心快码开发了一款积木工坊:用AI让每个孩子都成为小小建筑师
flutter
名字被你们想完了2 小时前
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(一)
flutter
灰灰勇闯IT2 小时前
Flutter×VS Code:跨端开发的高效协作指南(2025最新配置)
笔记·flutter·harmonyos
●VON3 小时前
Flutter vs React Native vs 原生开发:有何不同?
学习·flutter·react native·react.js·openharmony
白茶三许3 小时前
【OpenHarmony】深入理解 Flutter 异步编程:从基础到实战
flutter·开源·openharmony·gitcode
西西学代码3 小时前
flutter---日历
flutter
kirk_wang3 小时前
Flutter 桌面/Web 开发:用 MouseRegion 打造原生级交互体验
前端·flutter·交互
●VON4 小时前
从零开始:用 Flutter 构建一个简洁高效的待办事项应用V1.0.0
学习·flutter·arm·openharmony·开源鸿蒙
●VON4 小时前
Flutter for OpenHarmony前置知识《Flutter 基础组件初探:第一章》
学习·flutter·跨平台·开发·openharmony·开源鸿蒙