Flutter CustomPaint 自定义绘制实战:从基础到五子棋

拾柒
拾柒
发布时间:2026-10-15 | 3人浏览过
原创

在 Flutter 开发中,当系统自带组件无法满足复杂的 UI 设计需求时,CustomPaint 提供了强大的自定义绘制能力。本文深入解析其核心参数与层级逻辑,并通过绘制基础图形及完整五子棋案例,展示 Canvas 的高效使用方法与性能优化技巧。

认识 CustomPaint 架构

CustomPaint 是 Flutter 中用于自由绘制的 Widget,其绘制逻辑以当前 Canvas(画布)的左上角为原点。它的结构主要包含三个关键参数:

  • child:放置在画布中间的子组件。
  • painter:负责背景层的绘制内容。
  • foregroundPainter:负责前景层的绘制内容。

这三者的渲染顺序非常明确:painter 最先执行,接着渲染 child 组件,最后执行 foregroundPainter。这意味着如果前景画笔与背景画笔在同一位置绘制,前景会覆盖背景。CustomPainter 类中的 paint 方法负责具体的绘图指令,接收 canvas 和 size 两个参数。

图层交互测试案例

为了直观理解各层级的关系,可以通过以下代码创建一个组合示例。在此案例中,我们定义了红色背景的 child,蓝色背景的 painter,以及绿色前景的 foregroundPainter。

import 'package:flutter/material.dart';

class MyPaint extends StatelessWidget {
  const MyPaint({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('CustomPaint Test')),
      body: Center(
        child: CustomPaint(
          size: Size(300, 300), // 指定画布尺寸
          painter: MyBackgroundPainter(),
          foregroundPainter: MyForegroundPainter(),
          child: Container(
            height: 80,
            width: 80,
            color: Colors.red,
            alignment: Alignment.center,
            child: Text('Child', style: TextStyle(color: Colors.white)),
          ),
        ),
      ),
    );
  }
}

// 背景绘制器:绘制蓝色圆形和线条
class MyBackgroundPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 4;
    // 绘制大圆
    canvas.drawCircle(Offset(100, 100), 60, paint);
    // 绘制线条
    canvas.drawLine(
      Offset(50, 50),
      Offset(200, 200),
      paint,
    );
  }

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

// 前景绘制器:绘制绿色圆形,将覆盖 Child 的一部分
class MyForegroundPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.green.withOpacity(0.8);
    // 绘制小圆,位于 Child 上方
    canvas.drawCircle(Offset(size.width / 2, size.height / 2), 40, paint);
  }

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

运行上述代码可见:底层为蓝色区域,中间为红色 Container,顶层覆盖了半透明的绿色圆圈。这种分层机制允许我们在不同层级添加阴影、装饰或交互反馈。

常用绘制方法速查

Canvas 类提供了丰富的基础图形绘制接口,以下是核心方法的使用要点:

  • drawCircle:绘制圆形,需指定中心点 Offset、半径和 Paint 属性。
  • drawRect:绘制矩形,通常配合 Offset.zero & size 填充整个画布。
  • drawLine:绘制线段,起点和终点均为 Offset。
  • drawPath:绘制复杂路径,适合不规则图形。
  • drawImage:绘制图片资源。
  • drawText / drawParagraph:绘制文本(新版 Flutter 推荐使用 Paragraph 构建器)。

第 1 步:绘制基础点与 Line/PointMode

在使用 drawPoints 时,PointMode 决定了点的连接方式:

  • PointMode.points:仅绘制独立的点。
  • PointMode.lines:两两成对绘制线段(奇数个点时忽略最后一个)。
  • PointMode.polygon:将所有点首尾相连连成一条折线。
class PointsDemo extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..strokeWidth = 15
      ..strokeCap = StrokeCap.round;
    
    final points = [
      Offset(0, 0),
      Offset(size.width / 2, size.height / 2),
      Offset(size.width, size.height),
    ];
    
    // 使用 points 模式绘制独立红点
    canvas.drawPoints(PointMode.points, points, paint);
  }

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

第 2 步:组合复杂形状与 Path

对于弧形、椭圆和圆角矩形等形状,通常需要结合 PaintingStyle.fill(填充)和 PaintingStyle.stroke(描边)来调整视觉效果。

class ShapesDemo extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final strokePaint = Paint()
      ..color = Colors.red
      ..strokeWidth = 4
      ..style = PaintingStyle.stroke;
    
    final fillPaint = Paint()
      ..color = Colors.blue.withOpacity(0.3)
      ..style = PaintingStyle.fill;

    // 绘制直线
    canvas.drawLine(Offset(20, 20), Offset(size.width - 20, 20), strokePaint);

    // 绘制闭合路径(三角形)
    final path = Path()
      ..moveTo(20, size.height - 20)
      ..lineTo(size.width / 2, 20)
      ..lineTo(size.width - 20, size.height - 20)
      ..close();
    canvas.drawPath(path, fillPaint);

    // 绘制椭圆
    canvas.drawOval(Rect.fromLTWH(20, 50, size.width - 40, 100), strokePaint);

    // 绘制圆弧 (90度)
    canvas.drawArc(
      Rect.fromLTWH(20, 170, 100, 100),
      0,
      pi / 2,
      false,
      strokePaint,
    );

    // 绘制圆角矩形
    canvas.drawRRect(
      RRect.fromLTRBR(
        20, 170, 120, 270,
        Radius.circular(10),
      ),
      fillPaint,
    );
  }

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

实战:绘制五子棋棋盘

下面是一个完整的五子棋棋盘绘制示例。该示例展示了如何计算网格间距、绘制背景色、网格线以及放置黑白棋子。同时演示了如何通过 shouldRepaint 控制重绘逻辑。

class GobangBoard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: CustomPaint(
        size: Size(300, 300),
        painter: GobangPainter(),
      ),
    );
  }
}

class GobangPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 计算格子宽度和高度
    final cellSize = size.width / 15;
    final cellHeight = size.height / 15;

    // 1. 绘制棋盘背景 (纸黄色)
    var bgPaint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.fill
      ..color = Color(0xFFCDB175);
    canvas.drawRect(Offset.zero & size, bgPaint);

    // 2. 绘制网格线
    var linePaint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.black87
      ..strokeWidth = 1.0;

    for (int i = 0; i <= 15; ++i) {
      // 横线
      canvas.drawLine(
        Offset(0, i * cellHeight),
        Offset(size.width, i * cellHeight),
        linePaint,
      );
      // 竖线
      canvas.drawLine(
        Offset(i * cellSize, 0),
        Offset(i * cellSize, size.height),
        linePaint,
      );
    }

    // 3. 绘制中心黑子
    var piecePaint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    
    canvas.drawCircle(
      Offset(size.width / 2 - cellSize/2, size.height / 2 - cellHeight/2),
      cellSize / 2 - 2,
      piecePaint,
    );

    // 4. 绘制白子
    piecePaint.color = Colors.white;
    canvas.drawCircle(
      Offset(size.width / 2 + cellSize/2, size.height / 2 - cellHeight/2),
      cellSize / 2 - 2,
      piecePaint,
    );
  }

  @override
  // 由于本例静态绘制,返回 true 确保首次加载必绘;实际项目中应根据状态变化决定
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

性能优化:避免无谓的重绘

自定义绘制在 Flutter 中属于高开销操作。为了提升 UI 流畅度,务必关注 shouldRepaint 回调的使用:

  1. 正确利用 shouldRepaint:
    每次 Widget 树重新 Build 时,Flutter 都会调用此方法判断是否需要重绘。
    • 如果绘制内容与外部状态无关,直接返回 false。
    • 如果依赖外部数据(如动画进度、用户输入),应在该方法中比较新旧状态,仅在数据变化时返回 true。
  2. 分层绘制策略:
    如五子棋示例中,棋盘背景是不变的,只有棋子位置会变。如果将两者合并在一个 CustomPaint 中,落子时将导致整个背景被重绘。
    优化方案:将不变的背景封装为单独的 CustomPainter(返回 false),将变化的棋子单独封装。这样只需要重绘棋子部分,大幅降低渲染压力。

总结

CustomPaint 赋予了 Flutter 开发者完全掌控 2D 图形渲染的能力。核心在于理解 painter(背景)、child(中层)和 foregroundPainter(前景)的叠加顺序,并熟练使用 Canvas 的几何绘制 API。在实际工程中,通过合理拆分图层和优化 shouldRepaint 逻辑,可以构建出既美观又高效的自定义 UI 组件。

以上就是 Flutter CustomPaint 自定义绘画示例详解的详细内容,更多关于 Flutter CustomPaint 自定义绘制性能优化的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表