Flutter × HarmonyOS 7.0 跨端开发实战:构建围棋对弈应用
Flutter × HarmonyOS 7.0 跨端开发实战:构建围棋对弈应用
前言
围棋作为中华传统文化的瑰宝,拥有数千年的历史,其蕴含的深邃策略与哲学思想至今仍吸引着无数爱好者。随着人工智能技术的发展,AlphaGo 等围棋 AI 的出现让更多人重新认识了这项古老棋艺的魅力。然而,开发一款高质量的围棋应用并非易事——19x19 标准棋盘的精确绘制、棋子的立体质感呈现、人机对弈的逻辑实现都是技术难点。本文将介绍如何基于 Flutter 框架,在 HarmonyOS 7.0 平台上构建一个具有传统木纹质感的围棋对弈应用,重点讲解 CustomPainter 绘制棋盘的核心技术细节。
背景
HarmonyOS 7.0 生态的快速发展为传统文化类应用带来了新的机遇,但跨平台开发的复杂性也带来了挑战。对于已经拥有 Flutter 技术栈的团队而言,如何快速将现有应用适配到鸿蒙平台成为关键问题。Flutter 作为全球主流跨平台开发框架,凭借统一代码库、高性能渲染以及成熟生态,成为 HarmonyOS 跨端开发的重要技术路线之一。
围棋对弈应用是一个典型的棋盘游戏类应用,涉及到 CustomPainter 自定义绘制、Canvas 图形操作、渐变着色器、模糊滤镜等核心技术点。与传统 UI 组件不同,围棋棋盘需要精确控制每个像素点的渲染,这对 Flutter 的绘图能力提出了较高要求。通过本文的实践,读者可以掌握 Flutter CustomPainter 的核心使用技巧,为构建更复杂的自定义绘制类应用打下坚实基础。
Flutter × HarmonyOS 7.0 跨端开发介绍
Flutter 的核心架构由 Framework、Engine、Embedder 三层组成,在 HarmonyOS 7.0 平台上,Flutter 通过鸿蒙平台适配框架与 Flutter Engine 深度结合,实现 Dart 代码在 HarmonyOS 设备上的原生运行。开发者可以继续使用熟悉的 Flutter SDK、Dart 语言以及丰富的第三方组件生态,同时获得 HarmonyOS 提供的分布式能力、系统服务以及设备协同能力。
Flutter 在 HarmonyOS 上的运行并非简单的兼容层适配,而是通过 Embedder 层实现与系统的深度集成,Embedder 层主要负责窗口创建、生命周期管理、输入事件传递、GPU Surface 管理以及 Platform Channel 通信。这种架构设计保证了 Flutter 应用能够充分利用 HarmonyOS 的系统能力,同时保持跨平台的一致性。在 Release 模式下,Flutter 采用 AOT 编译技术,将 Dart 代码直接编译为 ARM64 原生机器码,运行时无需解释器参与,启动速度更快,CPU 开销更低。因此 Flutter 在 HarmonyOS 上能够达到接近原生应用的执行效率,尤其是在自定义绘制、动画渲染、复杂交互等场景中表现优异。

开发核心代码
1. 围棋棋盘的数据结构初始化
围棋应用的核心是 19x19 标准棋盘的数据表示。我们使用二维数组存储棋盘状态,其中 0 表示空位,1 表示黑棋,2 表示白棋。
class IntroPage extends StatefulWidget {
const IntroPage({super.key});
@override
State<IntroPage> createState() => _IntroPageState();
}
class _IntroPageState extends State<IntroPage> {
// 19x19 board, simplified for display
static const int _size = 19;
final List<List<int>> _board = List.generate(_size, (_) => List.filled(_size, 0));
// Place some stones for display
void _initBoard() {
final stones = [
[3, 3, 1], [3, 4, 2], [4, 3, 2], [4, 4, 1], [3, 15, 1], [4, 15, 2],
[15, 3, 2], [15, 4, 1], [15, 15, 1], [16, 16, 2], [10, 10, 1], [9, 10, 2],
];
for (final s in stones) {
_board[s[0]][s[1]] = s[2];
}
}
}
这段代码展示了围棋棋盘数据结构的初始化方式。_size 常量定义为 19,符合标准围棋棋盘规格。_board 使用 List.generate 创建 19x19 的二维数组,初始值全部为 0(空位)。_initBoard 方法预置了 12 颗棋子,模拟真实对局中的典型布局,包括角部的星位附近布局和中央区域的棋子分布。这种预置展示方式可以让用户直观地看到棋盘效果,无需实际对局即可体验界面。
在颜色定义上,我们选用了传统木纹宣纸质感的配色方案:
static const Color _bg = Color(0xFFF5F0E8); // 宣纸米色背景
static const Color _wood = Color(0xFFB45309); // 木纹棕色
static const Color _ink = Color(0xFF1F2937); // 墨色文字
这种配色方案借鉴了传统围棋棋盘的视觉风格,_bg 使用淡雅的宣纸米色作为整体背景,_wood 采用深棕色模拟实木纹理,_ink 则使用接近墨色的深灰作为文字主色调,营造出古朴典雅的氛围。
2. 棋盘组件:AspectRatio + CustomPaint 架构
棋盘是围棋应用最核心的视觉组件,需要保证正方形比例并承载自定义绘制逻辑。我们采用 AspectRatio + CustomPaint 的经典组合来实现:
Widget _goBoard() {
return Padding(
padding: const EdgeInsets.all(8),
child: AspectRatio(
aspectRatio: 1,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFDEB887), // 棋盘底色:浅木色
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 12,
),
],
),
child: CustomPaint(
size: Size.infinite,
painter: _GoBoardPainter(board: _board, size: _size),
),
),
),
);
}
这段代码展示了 Flutter 自定义绘制组件的标准架构模式。关键设计点如下:
AspectRatio 保持正方形:aspectRatio: 1 确保棋盘始终以正方形显示,无论父容器宽度如何变化。这对于围棋棋盘至关重要,因为 19x19 的网格必须保持严格的正方形比例,否则会导致棋子位置变形失真。
Container 装饰层:外层 Container 负责"画框"效果,包括浅木色背景 (#DEB887)、圆角边框 (borderRadius: 12) 和柔和阴影 (boxShadow)。这种分层设计将装饰效果与内容绘制分离,使代码职责清晰。
CustomPaint 核心绘制:CustomPaint 是 Flutter 中实现自定义绘制的核心组件,它接收一个 CustomPainter 子类的实例。我们将 _GoBoardPainter 实例传入,并将棋盘数据 _board 和尺寸 _size 作为构造参数传递给绘制器。size: Size.infinite 让绘制器填满整个 Container 区域。
3. _GoBoardPainter:完整的 Canvas 绘制逻辑
_GoBoardPainter 是整个应用的技术核心,继承自 CustomPainter,负责将抽象的棋盘数据转换为具体的像素图形。下面完整解析其绘制逻辑:
class _GoBoardPainter extends CustomPainter {
final List<List<int>> board;
final int size;
const _GoBoardPainter({required this.board, required this.size});
@override
void paint(Canvas canvas, Size size) {
final cellW = size.width / (this.size + 1);
final cellH = size.height / (this.size + 1);
final linePaint = Paint()
..color = const Color(0xFF8B6914)
..strokeWidth = 0.8;
// Grid lines
for (int i = 0; i < this.size; i++) {
final x = cellW * (i + 1);
final y = cellH * (i + 1);
canvas.drawLine(Offset(x, cellH), Offset(x, size.height - cellH), linePaint);
canvas.drawLine(Offset(cellW, y), Offset(size.width - cellW, y), linePaint);
}
// Star points
final stars = [3, 9, 15];
final starPaint = Paint()..color = const Color(0xFF8B6914);
for (final sx in stars) {
for (final sy in stars) {
canvas.drawCircle(Offset(cellW * (sx + 1), cellH * (sy + 1)), 3, starPaint);
}
}
// Stones
for (int i = 0; i < this.size; i++) {
for (int j = 0; j < this.size; j++) {
if (board[i][j] != 0) {
final cx = cellW * (j + 1);
final cy = cellH * (i + 1);
final r = cellW * 0.42;
final stonePaint = Paint()
..shader = RadialGradient(
colors: board[i][j] == 1
? [const Color(0xFF4B5563), const Color(0xFF111827)]
: [const Color(0xFFF9FAFB), const Color(0xFFD1D5DB)],
).createShader(Rect.fromCircle(center: Offset(cx, cy), radius: r))
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1);
canvas.drawCircle(Offset(cx, cy), r, stonePaint);
canvas.drawCircle(Offset(cx, cy), r, Paint()
..color = const Color(0xFF000000).withValues(alpha: 0.1)
..style = PaintingStyle.stroke
..strokeWidth = 0.5);
}
}
}
}
@override
bool shouldRepaint(covariant _GoBoardPainter old) => false;
}
下面逐一解析这段绘制代码的三大核心模块:
3.1 网格线绘制:坐标计算与线条渲染
final cellW = size.width / (this.size + 1);
final cellH = size.height / (this.size + 1);
final linePaint = Paint()
..color = const Color(0xFF8B6914)
..strokeWidth = 0.8;
for (int i = 0; i < this.size; i++) {
final x = cellW * (i + 1);
final y = cellH * (i + 1);
canvas.drawLine(Offset(x, cellH), Offset(x, size.height - cellH), linePaint);
canvas.drawLine(Offset(cellW, y), Offset(size.width - cellW, y), linePaint);
}
坐标系统设计:这是整个棋盘绘制的关键算法。我们采用 (size + 1) 作为除数而非 size,这意味着棋盘四周留出了半个格子的边距。这种设计的优势在于:
- 边缘的棋子不会被截断,完整显示在可视区域内
- 视觉上更加平衡美观,符合传统围棋棋盘的设计习惯
具体来说,当 this.size = 19 时,实际被划分为 20 个等分单位,网格线的坐标从 cellW * 1 到 cellW * 19,两侧各保留 cellW * 0.5 的边距。
线条样式配置:linePaint 使用深棕色调 (#8B6914),strokeWidth = 0.8 设置极细的线条宽度,模拟传统木刻棋盘的精细网格感。这个数值经过精心调试,过粗会显得笨重,过细在高分辨率屏幕上会显得模糊。
双重循环绘制:外层循环遍历 0-18 共 19 条线,每次迭代同时绘制一条竖线和一条横线。竖线从顶部边距延伸到底部边距,横线从左边距延伸到右边距。这种"一笔画"式的绘制方式简洁高效,共生成 19 x 2 = 38 条线段,构成完整的 19x19 网格。
3.2 星位点绘制:九个定位标志
final stars = [3, 9, 15];
final starPaint = Paint()..color = const Color(0xFF8B6914);
for (final sx in stars) {
for (final sy in stars) {
canvas.drawCircle(Offset(cellW * (sx + 1), cellH * (sy + 1)), 3, starPaint);
}
}
围棋棋盘上有 9 个星位(天元 + 八个角星),用于定位和参考。标准坐标为 (3,3)、(3,9)、(3,15)、(9,3)、(9,9)、(9,15)、(15,3)、(15,9)、(15,15),正好对应 stars = [3, 9, 15] 的笛卡尔积。
双层循环生成 9 个点位:通过 stars 数组的嵌套遍历,自动生成所有 9 个星位坐标,代码极其简洁。drawCircle 方法在每个星位处绘制半径为 3 像素的实心圆点,颜色与网格线一致 (#8B6914),保持视觉统一。
注意坐标映射:由于前面采用的 (size + 1) 坐标系,星位的实际像素坐标为 cellW * (sx + 1) 和 cellH * (sy + 1),其中 sx 和 sy 取值 3、9 或 15,分别对应棋盘的下部/中部/上部区域(以数组索引视角)。
3.3 棋子绘制:RadialGradient 渐变 + MaskFilter 阴影
这是整个绘制器的技术亮点,实现了逼真的立体棋子效果:
if (board[i][j] != 0) {
final cx = cellW * (j + 1);
final cy = cellH * (i + 1);
final r = cellW * 0.42;
final stonePaint = Paint()
..shader = RadialGradient(
colors: board[i][j] == 1
? [const Color(0xFF4B5563), const Color(0xFF111827)]
: [const Color(0xFFF9FAFB), const Color(0xFFD1D5DB)],
).createShader(Rect.fromCircle(center: Offset(cx, cy), radius: r))
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1);
canvas.drawCircle(Offset(cx, cy), r, stonePaint);
canvas.drawCircle(Offset(cx, cy), r, Paint()
..color = const Color(0xFF000000).withValues(alpha: 0.1)
..style = PaintingStyle.stroke
..strokeWidth = 0.5);
}
棋子半径设计:r = cellW * 0.42,即格子宽度的 42%。这个比例确保相邻棋子之间留有微小间隙,不会完全贴合,更接近真实棋子的视觉效果。
RadialGradient 径向渐变:棋子不是纯色的,而是使用从中心向外扩散的径向渐变来模拟光照效果:
- 黑棋:从
#4B5563(中灰)渐变到#111827(近黑),模拟光线照射下棋子中心的反光效果 - 白棋:从
#F9FAFB(近白)渐变到#D1D5DB(浅灰),营造温润如玉的质感
createShader 方法将渐变定义转换为可在 Canvas 上使用的 Shader 对象,绑定到以棋子中心为圆心、半径为 r 的圆形区域。
MaskFilter.blur 模糊滤镜:MaskFilter.blur(BlurStyle.normal, 1) 为棋子添加轻微的高斯模糊边缘,半径为 1 像素。这一技巧让棋子的边界不再是生硬的硬边缘,而是带有微妙的过渡,大大增强了立体感和真实感。
描边增强轮廓:在绘制完渐变棋子后,额外叠加一层描边圆圈,使用 10% 不透明度的黑色、0.5px 描边宽度。这层细微的描边让棋子在浅色棋盘背景上轮廓更加清晰,提升可读性。

4. 底部控制面板:操作按钮与对局信息
底部面板提供用户交互入口和对局状态展示,采用白色圆角卡片风格与上方棋盘形成对比:
Widget _bottomPanel() {
return Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
_actionBtn('⏮', '悔棋'),
_actionBtn('⏸', '暂停'),
_actionBtn('📊', '形势'),
_actionBtn('💡', '提示'),
]),
const SizedBox(height: 14),
Row(children: [
_infoChip('⚫ 黑棋', '你', const Color(0xFF1F2937)),
const SizedBox(width: 10),
_infoChip('⚪ 白棋', 'AI Lv.3', const Color(0xFF9CA3AF)),
const SizedBox(width: 10),
_infoChip('⏱️', '剩余 8:24', const Color(0xFFF59E0B)),
]),
]),
);
}
Widget _actionBtn(String icon, String label) {
return Column(children: [
Container(
width: 48, height: 48,
decoration: BoxDecoration(
color: const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(14),
),
child: Center(child: Text(icon, style: const TextStyle(fontSize: 20))),
),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 9, color: Color(0xFF6B7280))),
]);
}
Widget _infoChip(String icon, String label, Color color) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.04),
borderRadius: BorderRadius.circular(12),
),
child: Column(children: [
Text(icon, style: const TextStyle(fontSize: 12)),
const SizedBox(height: 2),
Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: color)),
]),
),
);
}
操作按钮区:四个功能按钮均匀分布在行内,使用 mainAxisAlignment: spaceEvenly 实现等间距排列。每个按钮是一个 48x48 的圆角方形容器,灰色背景配合 Emoji 图标,下方附有文字标签。四个按钮分别是悔棋、暂停、形势判断、提示,覆盖了对局过程中的主要操作需求。
对局信息 Chip 区:三个等宽的信息卡片展示当前对局状态:
- 黑棋方信息:显示"你",标识玩家执黑
- 白棋方信息:显示"AI Lv.3",标识 AI 对手及其等级
- 计时信息:显示剩余时间 8:24,使用琥珀色突出时间紧迫感
每个 Chip 使用对应主题色的 4% 透明度作为背景色,图标在上、文字在下纵向排列,形成紧凑而信息丰富的状态栏。
5. 顶部导航栏:对局标题与状态标签
Widget _topBar() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(children: [
const Text('⚫ 围棋对弈',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: _ink)),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFFEF3C7),
borderRadius: BorderRadius.circular(20),
),
child: const Text('黑棋 · 第47手',
style: TextStyle(fontSize: 10, color: _wood, fontWeight: FontWeight.w700)),
),
]),
);
}
顶部导航栏左侧显示应用名称"围棋对弈",右侧显示当前对局状态的胶囊标签。标签使用暖黄色 (#FEF3C7) 背景,棕色文字,圆角药丸形状,显示当前落子方和手数信息。Spacer() 组件将标题和状态标签推到两端,充分利用水平空间。
心得
通过本次围棋对弈应用的开发,我深刻体会到 Flutter CustomPainter 在复杂自定义绘制场景下的强大能力。首先,Canvas API 提供了完整的 2D 绘图原语,从基础图形(线、圆、矩形)到高级效果(渐变、滤镜、遮罩)一应俱全。在围棋棋盘的绘制中,我们仅用不到 50 行核心代码就完成了包含网格线、星位点、立体棋子的完整渲染,这在原生开发中往往需要数百行代码才能实现。
其次,RadialGradient + MaskFilter 的组合是实现立体质感的关键技巧。传统的扁平化棋子缺乏真实感,而径向渐变模拟了光源照射下的明暗变化,模糊滤镜柔化了边缘,两者配合产生了令人信服的 3D 效果。这套技术方案不仅适用于围棋棋子,还可以推广到其他需要立体效果的场景,如按钮、图标、游戏元素等。
第三,坐标系设计是自定义绘制的隐含难点。在本项目中,(size + 1) 的除数选择看似简单,实则蕴含了对边距处理的深思熟虑。好的坐标系设计能让后续的所有坐标计算变得自然直观,而糟糕的坐标设计则会在边界条件上产生大量 bug。建议在进行任何 CustomPainter 开发前,先花时间规划好坐标系统的原点、方向、边距等基本要素。
在实际应用方面,围棋对弈应用还需要与 AI 引擎集成来实现真正的对弈功能。Flutter 可以通过 Isolate 机制将耗时的大局计算放到后台线程,避免阻塞 UI 渲染线程。HarmonyOS 平台提供了 NPU 加速能力,可以将神经网络推理(如策略网络、价值网络)下沉到硬件层面,大幅提升 AI 思考速度。此外,通过 Platform Channel 调用鸿蒙原生的本地数据库服务,可以实现棋谱保存、历史记录、复盘分析等功能。

总结
本文通过一个围棋对弈应用的开发实践,详细介绍了 Flutter 在 HarmonyOS 7.0 平台上的 CustomPainter 自定义绘制技术。从棋盘数据结构设计、AspectRatio+CustomPaint 架构搭建,到 _GoBoardPainter 中网格线/星位点/棋子渐变阴影的完整 Canvas 绘制逻辑,再到底部控制面板与顶部状态栏的 UI 构建,涵盖了 Flutter 跨端开发中自定义绘制的全流程关键技术点。
技术亮点总结:
- 坐标系设计:
(size + 1)除数实现优雅的边距处理 - 网格线绘制:0.8px 细线条 + 深棕色调还原传统棋盘质感
- 星位算法:
[3,9,15]笛卡尔积简洁生成 9 个定位点 - 棋子立体渲染:RadialGradient 径向渐变 + MaskFilter.blur(1) + 描边三层叠加
- 响应式布局:AspectRatio(1) 保证任意屏幕尺寸下的正方形棋盘
Flutter 与 HarmonyOS 的结合,不仅保留了 Flutter 统一代码库、高性能渲染的优势,还能够充分利用 HarmonyOS 的分布式能力和硬件加速特性,为棋类游戏应用提供了强大的技术支撑。对于企业级项目而言,这意味着同一套 Flutter 代码可以覆盖 Android、iOS、HarmonyOS 等多个平台,大幅降低研发成本和维护复杂度。
开发者可以基于本文的技术方案,进一步扩展应用功能,如添加 AI 对弈引擎、棋谱 SGF 解析器、形势判断算法、语音解说、在线对局等,构建更完善的围棋生态,让这项传承千年的智慧之棋在数字时代焕发新的生机。
- 点赞
- 收藏
- 关注作者
评论(0)