Flutter for OpenHarmony游戏集合App实战之五子棋棋盘绘制
本文介绍了使用Flutter的CustomPaint绘制五子棋棋盘的方法。主要内容包括:棋盘采用15×15标准网格,棋子落在交叉点;通过CustomPainter绘制横竖网格线和9个定位星位;详细讲解画笔设置、坐标计算和绘制逻辑;使用Container为棋盘添加木质背景色、圆角和阴影效果。文章重点阐述了绘制过程中的关键点,如偏移量处理、星位位置计算等,并提供了完整的代码实现思路。最终实现了一个美观
通过网盘分享的文件: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要求实现两个方法:paint和shouldRepaint。
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的剩余空间。
页面布局是:
- 顶部状态栏(固定高度)
- 棋盘区域(Expanded,占剩余空间)
- 底部留白(固定高度)
Center让棋盘在区域内居中显示。
完整的绘制流程
总结一下棋盘绘制的完整流程:
- Expanded 占据剩余空间
- Center 居中
- AspectRatio 保持正方形
- Container 添加背景色、圆角、阴影
- CustomPaint 绑制网格线和星位点
- GridView 放置棋子(在CustomPaint的child里)
每一层都有自己的职责,组合起来就是完整的棋盘。
Canvas坐标系
补充一下Canvas的坐标系知识:
- 原点在左上角
- X轴向右为正
- Y轴向下为正
这和数学里的坐标系不一样(数学里Y轴向上为正),但和大多数图形系统一致。
所以Offset(0, 0)是左上角,Offset(width, height)是右下角。
画线时要注意这个坐标系,不然线会画到奇怪的位置。
性能考虑
CustomPaint的性能通常很好,但有几点要注意:
- shouldRepaint返回false:静态内容不要重绘
- Paint对象复用:不要在paint方法里频繁创建Paint
- 避免复杂计算: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
更多推荐


所有评论(0)