通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip

前言

请添加图片描述
五子棋是个经典棋类游戏,规则简单但策略丰富。要做五子棋游戏,第一步就是把棋盘画出来。

棋盘看起来就是一堆横竖线,但要画得好看、画得准确,还是有些讲究的。这篇就来聊聊怎么用Flutter的CustomPaint绘制一个标准的五子棋棋盘。

棋盘长什么样

标准五子棋棋盘是15×15的网格,棋子下在线的交叉点上,不是格子里面。

棋盘上还有9个小黑点,叫星位,用来帮助定位。中间那个叫天元,四个角附近各有一个,四条边中间各有一个。

我们要画的就是这些:横线、竖线、星位点。

为什么用CustomPaint

Flutter提供了很多现成的Widget,但没有"棋盘"这种东西。遇到这种自定义图形,就要用CustomPaint

CustomPaint让你可以直接在Canvas上画任何东西:线条、圆形、矩形、曲线、文字……只要你能想到的,都能画。

child: CustomPaint(
  painter: BoardPainter(boardSize),
  child: GridView.builder(

CustomPaint有两个重要属性:

  • painter: 一个CustomPainter对象,负责绑制内容
  • child: 子Widget,会显示在绑制内容之上

我们把棋盘线画在底层,棋子放在上层的GridView里,这样棋盘和棋子就分离了,各管各的。

💡 为什么不把棋子也画在Canvas上? 可以,但没必要。棋子需要响应点击,用Widget更方便。Canvas绑制的内容是"死"的,不能直接响应手势。

BoardPainter类

绑制逻辑写在BoardPainter里:

class BoardPainter extends CustomPainter {
  final int size;
  BoardPainter(this.size);

继承CustomPainter,构造函数接收棋盘大小。五子棋是15,围棋是19,这样写通用性更好。

CustomPainter要求实现两个方法:paintshouldRepaint

paint方法:画笔准备


void paint(Canvas canvas, Size canvasSize) {
  final paint = Paint()
    ..color = Colors.black54
    ..strokeWidth = 1;

paint方法是绑制的核心,Flutter会把Canvas和尺寸传进来。

首先创建一个Paint对象,这是画笔。设置两个属性:

  • color: 线条颜色,用半透明黑色(black54),不会太刺眼
  • strokeWidth: 线条宽度,1像素,细一点好看

..是Dart的级联操作符,可以连续调用同一个对象的方法或设置属性,最后返回这个对象本身。等价于:

final paint = Paint();
paint.color = Colors.black54;
paint.strokeWidth = 1;

但级联写法更简洁。

计算格子大小

  double cellSize = canvasSize.width / size;
  double offset = cellSize / 2;

canvasSize.width是画布宽度,除以size(15)得到每个格子的大小。

offset是偏移量,等于半个格子。为什么要这个?

因为棋子下在交叉点上,不是格子中心。如果线从边缘开始画,第一条线就贴着边,交叉点也贴着边,棋子会有一半在棋盘外面。

加上offset后,第一条线距离边缘半个格子,最后一条线也距离边缘半个格子,棋子就完整地在棋盘内了。

💡 这个offset我也是调了几次才对的。一开始没加,棋子显示不全,后来想明白了交叉点和格子的关系。

画网格线

  // Draw grid lines
  for (int i = 0; i < size; i++) {
    // Horizontal lines
    canvas.drawLine(
      Offset(offset, offset + i * cellSize),
      Offset(canvasSize.width - offset, offset + i * cellSize),
      paint,
    );

用for循环画15条横线。

canvas.drawLine需要三个参数:

  • 起点: Offset对象,表示坐标
  • 终点: Offset对象
  • 画笔: Paint对象

横线的特点是Y坐标相同,X坐标从左到右。

起点:Offset(offset, offset + i * cellSize)

  • X = offset,距离左边缘半个格子
  • Y = offset + i * cellSize,第i条线的Y坐标

终点:Offset(canvasSize.width - offset, offset + i * cellSize)

  • X = canvasSize.width - offset,距离右边缘半个格子
  • Y = 和起点一样

这样每条横线都是从左到右,等间距排列。

画竖线

    // Vertical lines
    canvas.drawLine(
      Offset(offset + i * cellSize, offset),
      Offset(offset + i * cellSize, canvasSize.height - offset),
      paint,
    );
  }

竖线和横线类似,只是X坐标相同,Y坐标从上到下。

起点:Offset(offset + i * cellSize, offset)

  • X = offset + i * cellSize,第i条线的X坐标
  • Y = offset,距离上边缘半个格子

终点:Offset(offset + i * cellSize, canvasSize.height - offset)

  • X = 和起点一样
  • Y = canvasSize.height - offset,距离下边缘半个格子

横线和竖线在同一个for循环里画,效率更高。

画星位点

  // Draw star points
  final starPaint = Paint()..color = Colors.black;
  List<List<int>> starPoints = [
    [3, 3], [3, 11], [11, 3], [11, 11], [7, 7],
    [3, 7], [7, 3], [7, 11], [11, 7]
  ];

星位点用另一个画笔,纯黑色,不需要设置strokeWidth因为我们画的是实心圆。

starPoints是星位的坐标列表,9个点:

  • 四个角:[3,3]、[3,11]、[11,3]、[11,11]
  • 天元:[7,7]
  • 四边中点:[3,7]、[7,3]、[7,11]、[11,7]

为什么是3、7、11?因为棋盘是0-14共15条线,3和11距离边缘4格,7是正中间。这是标准五子棋的星位位置。

  for (var point in starPoints) {
    canvas.drawCircle(
      Offset(offset + point[0] * cellSize, offset + point[1] * cellSize),
      3,
      starPaint,
    );
  }

遍历每个星位,用drawCircle画实心圆。

参数:

  • 圆心: 根据坐标计算,和画线的逻辑一样
  • 半径: 3像素,小圆点
  • 画笔: starPaint

💡 半径3是试出来的,太大会挡住棋子,太小看不清。3刚好合适。

shouldRepaint方法


bool shouldRepaint(covariant CustomPainter oldDelegate) => false;

这个方法告诉Flutter什么时候需要重新绘制。

返回false表示永远不重绘。因为棋盘是静态的,画一次就够了,不会变化。

如果棋盘大小可能变化,就要返回true或者比较新旧size:


bool shouldRepaint(covariant BoardPainter oldDelegate) {
  return oldDelegate.size != size;
}

但我们的棋盘大小固定是15,所以直接返回false,省点性能。

棋盘容器

CustomPaint外面还包了一层Container:

child: Container(
  margin: const EdgeInsets.all(8),
  decoration: BoxDecoration(
    color: const Color(0xFFDEB887),
    borderRadius: BorderRadius.circular(8),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.3),
        blurRadius: 8,
        offset: const Offset(2, 2),
      ),
    ],
  ),

这个Container负责棋盘的外观装饰

背景色

color: const Color(0xFFDEB887),

0xFFDEB887是一个十六进制颜色值,对应的是浅棕色,模拟木质棋盘的感觉。

这个颜色叫"BurlyWood",是CSS标准色之一。我没有自己调色,直接用了这个经典的木纹色。

圆角

borderRadius: BorderRadius.circular(8),

四个角都是8像素的圆角,看起来柔和一些,不会太生硬。

阴影

boxShadow: [
  BoxShadow(
    color: Colors.black.withOpacity(0.3),
    blurRadius: 8,
    offset: const Offset(2, 2),
  ),
],

加一个阴影让棋盘有立体感,像是浮在页面上。

  • color: 30%透明度的黑色,不会太重
  • blurRadius: 模糊半径8像素,阴影边缘柔和
  • offset: 向右下偏移2像素,模拟光源在左上方

AspectRatio保持正方形

child: AspectRatio(
  aspectRatio: 1,
  child: Container(

棋盘必须是正方形,不能被拉伸变形。

AspectRatio组件可以强制子Widget保持指定的宽高比。aspectRatio: 1表示宽高比1:1,就是正方形。

不管屏幕多宽多高,棋盘都会是正方形,只是大小会变化。

Expanded填充剩余空间

Expanded(
  child: Center(
    child: AspectRatio(

Expanded让棋盘区域占据Column的剩余空间。

页面布局是:

  1. 顶部状态栏(固定高度)
  2. 棋盘区域(Expanded,占剩余空间)
  3. 底部留白(固定高度)

Center让棋盘在区域内居中显示。

完整的绘制流程

总结一下棋盘绘制的完整流程:

  1. Expanded 占据剩余空间
  2. Center 居中
  3. AspectRatio 保持正方形
  4. Container 添加背景色、圆角、阴影
  5. CustomPaint 绑制网格线和星位点
  6. GridView 放置棋子(在CustomPaint的child里)

每一层都有自己的职责,组合起来就是完整的棋盘。

Canvas坐标系

补充一下Canvas的坐标系知识:

  • 原点在左上角
  • X轴向右为正
  • Y轴向下为正

这和数学里的坐标系不一样(数学里Y轴向上为正),但和大多数图形系统一致。

所以Offset(0, 0)是左上角,Offset(width, height)是右下角。

画线时要注意这个坐标系,不然线会画到奇怪的位置。

性能考虑

CustomPaint的性能通常很好,但有几点要注意:

  1. shouldRepaint返回false:静态内容不要重绘
  2. Paint对象复用:不要在paint方法里频繁创建Paint
  3. 避免复杂计算:paint方法会被频繁调用,复杂计算放到外面

我们的棋盘实现已经考虑了这些:

  • shouldRepaint返回false
  • Paint对象在方法开头创建一次
  • 坐标计算很简单,就是乘法和加法

15×15的网格,画30条线和9个点,性能完全没问题。

小结

这篇讲了五子棋棋盘的绘制,核心知识点:

  • CustomPaint:Flutter自定义绑制的入口,painter属性指定绘制逻辑
  • CustomPainter:实现paint和shouldRepaint方法
  • Paint对象:画笔,设置颜色、线宽等属性
  • Canvas.drawLine:画直线,需要起点、终点、画笔
  • Canvas.drawCircle:画圆,需要圆心、半径、画笔
  • offset偏移:让网格线不贴边,棋子能完整显示
  • 星位点:9个小黑点,帮助定位
  • AspectRatio:保持棋盘正方形
  • BoxDecoration:背景色、圆角、阴影,让棋盘更好看

棋盘画好了,下一步就是放棋子了。黑白棋子怎么画、怎么交替落子,那是另一个话题。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐