
个人主页:ujainu
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
文章目录
- 前言
-
- 一、为什么做"简易数独"?
-
- [1. 用户体验优先](#1. 用户体验优先)
- [2. 鸿蒙设计原则融入](#2. 鸿蒙设计原则融入)
- 二、技术架构:从生成到验证
- 三、核心算法:简易数独生成器
-
- [1. 回溯法生成完整盘面](#1. 回溯法生成完整盘面)
- [2. 从完整盘面生成谜题](#2. 从完整盘面生成谜题)
- [四、UI 实现:鸿蒙风格网格渲染](#四、UI 实现:鸿蒙风格网格渲染)
-
- [1. 宫格背景区分](#1. 宫格背景区分)
- [2. 格子组件构建](#2. 格子组件构建)
- 五、实时校验与反馈机制
-
- [1. 冲突检测函数](#1. 冲突检测函数)
- [2. 状态更新与 UI 刷新](#2. 状态更新与 UI 刷新)
- 六、辅助功能:"提示"与"重置"
-
- [1. 提示功能](#1. 提示功能)
- [2. 重置功能](#2. 重置功能)
- 七、完整可运行代码
- 结语
前言
在数字时代,碎片化娱乐充斥着我们的生活。而数独------这一源自18世纪的逻辑谜题,至今仍以其纯粹的规则与深度的思考魅力吸引着全球数亿玩家。它不仅能锻炼逻辑推理能力、提升专注力,还能有效缓解焦虑,成为现代人"数字排毒"的理想选择。
然而,传统 9×9 数独对新手门槛较高,容易劝退初学者。为此,我们基于 Flutter + OpenHarmony 平台,开发了一款面向大众的 简易数独生成器(Mini Sudoku Generator) ,支持 4×4 与 6×6 两种轻量级难度,配合实时校验、智能提示与鸿蒙美学设计,让每个人都能轻松上手、快乐动脑!
本文将完整解析该应用的实现过程,涵盖 谜题生成算法、UI 绘制、状态管理与交互反馈 四大核心模块。全文包含详细代码讲解与可运行示例,适合 Flutter 中级开发者学习复用。
一、为什么做"简易数独"?
1. 用户体验优先
- 降低认知负荷:4×4(2×2 宫格)仅需填入 1~4;6×6(2×3 宫格)使用 1~6,规则一致但规模更小
- 快速完成感:平均 1~3 分钟可解一局,契合移动端碎片时间场景
- 教育友好:适合儿童逻辑启蒙或老年人脑力训练
2. 鸿蒙设计原则融入
- 清晰反馈:输入错误时格子边框红色闪烁,符合 OpenHarmony "即时、明确" 的交互准则
- 视觉层次 :
- 已知数字 → 灰色(不可编辑)
- 用户输入 → 主色蓝色(
#6200EE) - 宫格背景 → 浅灰区分(增强结构识别)
- 圆角美学 :所有格子采用
borderRadius: 8,柔和不刺眼
✅ 核心功能清单:
- 自动生成合法 4×4 / 6×6 数独谜题
- 点击格子弹出数字选择面板
- 实时校验行/列/宫重复(标红提示)
- "提示"按钮填充一个正确空格
- "重置"清空用户输入,保留初始谜题
二、技术架构:从生成到验证
我们将系统划分为三个关键层:
| 模块 | 职责 | 技术点 |
|---|---|---|
| 生成层 | 创建完整数独 + 挖洞成谜题 | 回溯算法(Backtracking) |
| UI 层 | 渲染网格、颜色、动画 | GridView / Container + BoxDecoration |
| 逻辑层 | 管理状态、校验冲突、处理交互 | 二维 List + 函数式验证 |
⚠️ 注意:为控制复杂度,本文使用 简化回溯 生成完整盘面,再随机挖去部分格子形成谜题(非唯一解保证,但对简易数独影响较小)。
三、核心算法:简易数独生成器
1. 回溯法生成完整盘面
以 4×4 为例(宫格大小为 2×2),我们定义:
dart
class SudokuGenerator {
static const int size4 = 4;
static const int boxSize4 = 2;
static const int size6 = 6;
static const int boxSize6 = 3; // 注意:6x6 的宫格是 2x3,但校验时按 2 行 3 列处理
static List<List<int>> generate(int size) {
final grid = List.generate(size, (_) => List.filled(size, 0));
_fillGrid(grid, size);
return grid;
}
static bool _fillGrid(List<List<int>> grid, int size) {
final empty = _findEmpty(grid, size);
if (empty == null) return true; // 已填满
final row = empty[0], col = empty[1];
final numbers = List<int>.generate(size, (i) => i + 1)..shuffle();
for (final num in numbers) {
if (_isValid(grid, row, col, num, size)) {
grid[row][col] = num;
if (_fillGrid(grid, size)) return true;
grid[row][col] = 0; // 回溯
}
}
return false;
}
static List<int>? _findEmpty(List<List<int>> grid, int size) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (grid[i][j] == 0) return [i, j];
}
}
return null;
}
static bool _isValid(List<List<int>> grid, int row, int col, int num, int size) {
final boxSize = size == 4 ? 2 : (size == 6 ? 2 : 3); // 6x6 的行方向 box 大小为 2
// 检查行
for (int x = 0; x < size; x++) {
if (grid[row][x] == num) return false;
}
// 检查列
for (int y = 0; y < size; y++) {
if (grid[y][col] == num) return false;
}
// 检查宫格(重点:6x6 是 2x3 宫格)
int boxRowStart, boxColStart;
if (size == 4) {
boxRowStart = (row ~/ 2) * 2;
boxColStart = (col ~/ 2) * 2;
} else if (size == 6) {
boxRowStart = (row ~/ 2) * 2; // 每 2 行一个宫
boxColStart = (col ~/ 3) * 3; // 每 3 列一个宫
} else {
boxRowStart = (row ~/ 3) * 3;
boxColStart = (col ~/ 3) * 3;
}
for (int i = boxRowStart; i < boxRowStart + (size == 6 ? 2 : boxSize); i++) {
for (int j = boxColStart; j < boxColStart + (size == 6 ? 3 : boxSize); j++) {
if (grid[i][j] == num) return false;
}
}
return true;
}
}
🔍 关键说明:
- 6×6 数独的宫格结构为 2 行 × 3 列,共 6 个宫
_isValid中对size == 6特殊处理宫格边界- 使用
shuffle()打乱数字顺序,增加谜题多样性
2. 从完整盘面生成谜题
dart
static List<List<int>> createPuzzle(List<List<int>> fullGrid, int difficulty) {
final size = fullGrid.length;
final puzzle = List.generate(size, (i) => List.from(fullGrid[i]));
// 难度:4x4 挖 8~10 个;6x6 挖 18~22 个
int cellsToHide = size == 4 ? 8 + difficulty : 18 + difficulty * 2;
final positions = List.generate(size * size, (i) => [i ~/ size, i % size])
..shuffle();
for (int i = 0; i < cellsToHide && i < positions.length; i++) {
final pos = positions[i];
puzzle[pos[0]][pos[1]] = 0;
}
return puzzle;
}
四、UI 实现:鸿蒙风格网格渲染
1. 宫格背景区分
通过判断 (row ~/ boxRows) + (col ~/ boxCols) 的奇偶性,交替使用浅灰背景:
dart
Color _getBoxColor(int row, int col, int size) {
if (size == 4) {
final isEven = ((row ~/ 2) + (col ~/ 2)) % 2 == 0;
return isEven ? Colors.white : const Color(0xFFF0F0F5);
} else if (size == 6) {
final isEven = ((row ~/ 2) + (col ~/ 3)) % 2 == 0;
return isEven ? Colors.white : const Color(0xFFF0F0F5);
}
return Colors.white;
}
2. 格子组件构建
dart
Widget _buildCell(int row, int col, BuildContext context) {
final value = _currentGrid[row][col];
final original = _originalPuzzle[row][col] != 0;
final hasError = _errorCells.contains([row, col]);
return GestureDetector(
onTap: original ? null : () => _showNumberPicker(row, col),
child: Container(
margin: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: _getBoxColor(row, col, _size),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: hasError ? Colors.red : Colors.transparent,
width: hasError ? 2 : 0,
),
),
child: Center(
child: Text(
value == 0 ? '' : '$value',
style: TextStyle(
fontSize: 20,
fontWeight: original ? FontWeight.w400 : FontWeight.bold,
color: original ? Colors.grey[600] : const Color(0xFF6200EE),
),
),
),
),
);
}
💡 交互细节:
- 原始数字不可点击(
onTap: null)- 错误格子显示红色边框(
hasError来自_errorCellsSet)- 用户输入文字加粗 + 主色蓝
五、实时校验与反馈机制
1. 冲突检测函数
dart
Set<List<int>> _getErrorCells() {
final errors = <List<int>>{};
final size = _size;
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
final val = _currentGrid[i][j];
if (val == 0 || _originalPuzzle[i][j] != 0) continue;
// 检查行
for (int k = 0; k < size; k++) {
if (k != j && _currentGrid[i][k] == val) {
errors.add([i, j]);
errors.add([i, k]);
}
}
// 检查列
for (int k = 0; k < size; k++) {
if (k != i && _currentGrid[k][j] == val) {
errors.add([i, j]);
errors.add([k, j]);
}
}
// 检查宫格
int startRow, startCol, boxRows, boxCols;
if (size == 4) {
startRow = (i ~/ 2) * 2;
startCol = (j ~/ 2) * 2;
boxRows = boxCols = 2;
} else {
startRow = (i ~/ 2) * 2;
startCol = (j ~/ 3) * 3;
boxRows = 2;
boxCols = 3;
}
for (int r = startRow; r < startRow + boxRows; r++) {
for (int c = startCol; c < startCol + boxCols; c++) {
if ((r != i || c != j) && _currentGrid[r][c] == val) {
errors.add([i, j]);
errors.add([r, c]);
}
}
}
}
}
return errors;
}
2. 状态更新与 UI 刷新
每次用户输入后调用:
dart
void _updateGrid(int row, int col, int value) {
setState(() {
_currentGrid[row][col] = value;
_errorCells = _getErrorCells();
});
}
✨ 鸿蒙反馈原则体现:
- 错误即时高亮(无延迟)
- 视觉提示明确(红色边框 > 文字变色)
- 不打断操作流(用户可继续输入)
六、辅助功能:"提示"与"重置"
1. 提示功能
随机选择一个空格,填入正确答案:
dart
void _giveHint() {
final emptyCells = <List<int>>[];
for (int i = 0; i < _size; i++) {
for (int j = 0; j < _size; j++) {
if (_currentGrid[i][j] == 0) {
emptyCells.add([i, j]);
}
}
}
if (emptyCells.isNotEmpty) {
final cell = emptyCells[Random().nextInt(emptyCells.length)];
final correctValue = _solution[cell[0]][cell[1]];
_updateGrid(cell[0], cell[1], correctValue);
}
}
2. 重置功能
仅清空用户输入,保留原始谜题:
dart
void _resetPuzzle() {
setState(() {
for (int i = 0; i < _size; i++) {
for (int j = 0; j < _size; j++) {
if (_originalPuzzle[i][j] == 0) {
_currentGrid[i][j] = 0;
}
}
}
_errorCells.clear();
});
}
七、完整可运行代码
以下为整合所有功能的完整实现,可直接在 Flutter + OpenHarmony 环境中运行:
dart
import 'dart:math';
import 'package:flutter/material.dart';
const Color kPrimaryColor = Color(0xFF6200EE);
const Color kBackgroundColor = Color(0xFFF9F9FB);
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: '简易数独',
theme: ThemeData(
primarySwatch: Colors.purple,
primaryColor: kPrimaryColor,
scaffoldBackgroundColor: kBackgroundColor,
appBarTheme: const AppBarTheme(backgroundColor: kPrimaryColor),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(backgroundColor: kPrimaryColor),
),
),
home: const SudokuGame(),
);
}
}
class SudokuGenerator {
static const int size4 = 4;
static const int size6 = 6;
static List<List<int>> generate(int size) {
final grid = List.generate(size, (_) => List.filled(size, 0));
_fillGrid(grid, size);
return grid;
}
static bool _fillGrid(List<List<int>> grid, int size) {
final empty = _findEmpty(grid, size);
if (empty == null) return true;
final row = empty[0], col = empty[1];
final numbers = List<int>.generate(size, (i) => i + 1)..shuffle();
for (final num in numbers) {
if (_isValid(grid, row, col, num, size)) {
grid[row][col] = num;
if (_fillGrid(grid, size)) return true;
grid[row][col] = 0;
}
}
return false;
}
static List<int>? _findEmpty(List<List<int>> grid, int size) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (grid[i][j] == 0) return [i, j];
}
}
return null;
}
static bool _isValid(List<List<int>> grid, int row, int col, int num, int size) {
// Check row
for (int x = 0; x < size; x++) {
if (grid[row][x] == num) return false;
}
// Check column
for (int y = 0; y < size; y++) {
if (grid[y][col] == num) return false;
}
// Check box
int boxRowStart, boxColStart, boxRows, boxCols;
if (size == 4) {
boxRowStart = (row ~/ 2) * 2;
boxColStart = (col ~/ 2) * 2;
boxRows = boxCols = 2;
} else {
boxRowStart = (row ~/ 2) * 2;
boxColStart = (col ~/ 3) * 3;
boxRows = 2;
boxCols = 3;
}
for (int i = boxRowStart; i < boxRowStart + boxRows; i++) {
for (int j = boxColStart; j < boxColStart + boxCols; j++) {
if (grid[i][j] == num) return false;
}
}
return true;
}
static List<List<int>> createPuzzle(List<List<int>> fullGrid, int hiddenCount) {
final size = fullGrid.length;
final puzzle = List.generate(size, (i) => List.from(fullGrid[i]));
final positions = List.generate(size * size, (i) => [i ~/ size, i % size])
..shuffle();
for (int i = 0; i < hiddenCount && i < positions.length; i++) {
final pos = positions[i];
puzzle[pos[0]][pos[1]] = 0;
}
return puzzle;
}
}
class SudokuGame extends StatefulWidget {
const SudokuGame({super.key});
@override
State<SudokuGame> createState() => _SudokuGameState();
}
class _SudokuGameState extends State<SudokuGame> {
late List<List<int>> _solution;
late List<List<int>> _originalPuzzle;
late List<List<int>> _currentGrid;
late Set<List<int>> _errorCells;
int _size = 4;
@override
void initState() {
super.initState();
_newGame();
}
void _newGame() {
final full = SudokuGenerator.generate(_size);
final hidden = _size == 4 ? 8 : 18;
final puzzle = SudokuGenerator.createPuzzle(full, hidden);
setState(() {
_solution = full;
_originalPuzzle = puzzle;
_currentGrid = List.generate(_size, (i) => List.from(puzzle[i]));
_errorCells = {};
});
}
Color _getBoxColor(int row, int col) {
if (_size == 4) {
return ((row ~/ 2) + (col ~/ 2)) % 2 == 0
? Colors.white
: const Color(0xFFF0F0F5);
} else {
return ((row ~/ 2) + (col ~/ 3)) % 2 == 0
? Colors.white
: const Color(0xFFF0F0F5);
}
}
Set<List<int>> _getErrorCells() {
final errors = <List<int>>{};
final size = _size;
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
final val = _currentGrid[i][j];
if (val == 0 || _originalPuzzle[i][j] != 0) continue;
// Check row
for (int k = 0; k < size; k++) {
if (k != j && _currentGrid[i][k] == val) {
errors.add([i, j]);
errors.add([i, k]);
}
}
// Check column
for (int k = 0; k < size; k++) {
if (k != i && _currentGrid[k][j] == val) {
errors.add([i, j]);
errors.add([k, j]);
}
}
// Check box
int startRow, startCol, boxRows, boxCols;
if (size == 4) {
startRow = (i ~/ 2) * 2;
startCol = (j ~/ 2) * 2;
boxRows = boxCols = 2;
} else {
startRow = (i ~/ 2) * 2;
startCol = (j ~/ 3) * 3;
boxRows = 2;
boxCols = 3;
}
for (int r = startRow; r < startRow + boxRows; r++) {
for (int c = startCol; c < startCol + boxCols; c++) {
if ((r != i || c != j) && _currentGrid[r][c] == val) {
errors.add([i, j]);
errors.add([r, c]);
}
}
}
}
}
return errors;
}
void _updateGrid(int row, int col, int value) {
setState(() {
_currentGrid[row][col] = value;
_errorCells = _getErrorCells();
});
}
void _showNumberPicker(int row, int col) {
showModalBottomSheet(
context: context,
builder: (context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(_size, (i) {
final num = i + 1;
return SizedBox(
width: 60,
height: 60,
child: ElevatedButton(
onPressed: () {
_updateGrid(row, col, num);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: kPrimaryColor.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'$num',
style: const TextStyle(fontSize: 20, color: kPrimaryColor),
),
),
);
}),
);
},
);
}
void _giveHint() {
final emptyCells = <List<int>>[];
for (int i = 0; i < _size; i++) {
for (int j = 0; j < _size; j++) {
if (_currentGrid[i][j] == 0) {
emptyCells.add([i, j]);
}
}
}
if (emptyCells.isNotEmpty) {
final cell = emptyCells[Random().nextInt(emptyCells.length)];
final correctValue = _solution[cell[0]][cell[1]];
_updateGrid(cell[0], cell[1], correctValue);
}
}
void _resetPuzzle() {
setState(() {
for (int i = 0; i < _size; i++) {
for (int j = 0; j < _size; j++) {
if (_originalPuzzle[i][j] == 0) {
_currentGrid[i][j] = 0;
}
}
}
_errorCells.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('简易数独'),
actions: [
PopupMenuButton<int>(
onSelected: (value) {
setState(() {
_size = value;
});
_newGame();
},
itemBuilder: (context) => [
const PopupMenuItem(value: 4, child: Text('4×4')),
const PopupMenuItem(value: 6, child: Text('6×6')),
],
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Expanded(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _size,
crossAxisSpacing: 2,
mainAxisSpacing: 2,
),
itemCount: _size * _size,
itemBuilder: (context, index) {
final row = index ~/ _size;
final col = index % _size;
final value = _currentGrid[row][col];
final original = _originalPuzzle[row][col] != 0;
final hasError = _errorCells.any((e) => e[0] == row && e[1] == col);
return GestureDetector(
onTap: original ? null : () => _showNumberPicker(row, col),
child: Container(
decoration: BoxDecoration(
color: _getBoxColor(row, col),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: hasError ? Colors.red : Colors.transparent,
width: hasError ? 2 : 0,
),
),
child: Center(
child: Text(
value == 0 ? '' : '$value',
style: TextStyle(
fontSize: 20,
fontWeight: original ? FontWeight.w400 : FontWeight.bold,
color: original ? Colors.grey[600] : kPrimaryColor,
),
),
),
),
);
},
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _giveHint,
icon: const Icon(Icons.lightbulb_outlined),
label: const Text('提示'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _resetPuzzle,
icon: const Icon(Icons.refresh),
label: const Text('重置'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _newGame,
icon: const Icon(Icons.casino),
label: const Text('新游戏'),
),
),
],
),
],
),
),
);
}
}
运行界面



结语
这款简易数独生成器,不仅是一次对经典益智游戏的现代化演绎,更是 Flutter 与 OpenHarmony 设计哲学 的完美结合。通过精巧的算法、清晰的反馈与优雅的界面,我们让"动脑"变得轻松而愉悦。
无论是通勤路上的几分钟,还是睡前放松的片刻,打开这个小工具,让逻辑之光照亮你的日常。正如鸿蒙所倡导的:"科技,应服务于人的专注与平静。"