Flutter for OpenHarmony数独游戏App实战:游戏控制按钮
本文介绍了数独游戏辅助功能的实现方法,包括撤销、擦除、笔记和提示四种核心功能。通过Dart代码展示了如何构建控制按钮UI组件,并详细说明了各功能在GameController中的逻辑实现。撤销功能通过记录操作历史实现回退,擦除功能清空单元格内容,笔记功能允许标记候选数字,提示功能在玩家卡住时提供帮助。文章采用GetX状态管理方案,确保UI与游戏状态的同步更新,同时注重用户体验设计,如通过颜色区分按
数独游戏除了数字输入,还需要一系列辅助功能来提升游戏体验。撤销、擦除、笔记、提示这四个功能是数独游戏的标配。今天我们来详细实现这些游戏控制按钮,让玩家能够更轻松地享受解题的乐趣。
在设计控制按钮之前,我们需要理解每个功能的使用场景。撤销功能让玩家可以回退错误的操作,这在填错数字时非常有用。擦除功能用于清空当前选中的单元格。笔记功能让玩家可以在单元格中记录候选数字,这是解决困难数独的必备技巧。提示功能在玩家卡住时给出正确答案,但通常会有使用次数的限制。
让我们从创建GameControls组件开始。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../controllers/game_controller.dart';
class GameControls extends StatelessWidget {
const GameControls({super.key});
这是控制按钮组件的基础结构。我们导入了Flutter的Material库、GetX状态管理库、ScreenUtil屏幕适配库,以及我们自定义的GameController。使用StatelessWidget是因为按钮本身不需要管理状态,所有状态都由GameController提供。
接下来实现build方法,构建控制按钮的整体布局。
Widget build(BuildContext context) {
return GetBuilder<GameController>(
builder: (controller) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildControlButton(
icon: Icons.undo,
label: '撤销',
onTap: controller.undoMove,
),
GetBuilder监听GameController的状态变化,当笔记模式或提示次数等状态改变时自动重建UI。Row组件水平排列四个控制按钮,mainAxisAlignment设置为spaceEvenly让按钮均匀分布。第一个按钮是撤销功能,使用undo图标,点击时调用controller的undoMove方法。
继续添加其他控制按钮。
_buildControlButton(
icon: Icons.backspace_outlined,
label: '擦除',
onTap: controller.eraseCell,
),
_buildControlButton(
icon: controller.notesMode ? Icons.edit : Icons.edit_outlined,
label: '笔记',
onTap: controller.toggleNotesMode,
isActive: controller.notesMode,
),
_buildControlButton(
icon: Icons.lightbulb_outline,
label: '提示(${controller.hintsUsed})',
onTap: controller.useHint,
),
],
),
);
}
擦除按钮使用backspace图标,直观地表示删除操作。笔记按钮根据当前模式显示不同的图标,激活时使用实心图标,未激活时使用空心图标。提示按钮的标签中显示了已使用的提示次数,让玩家知道自己用了多少次提示。isActive参数用于标记笔记按钮的激活状态,会影响按钮的视觉样式。
现在来实现单个控制按钮的构建方法。
Widget _buildControlButton({
required IconData icon,
required String label,
required VoidCallback onTap,
bool isActive = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: isActive ? Colors.blue.shade100 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8.r),
),
_buildControlButton是一个通用的按钮构建方法,接收图标、标签、点击回调和激活状态作为参数。GestureDetector处理点击事件,Container定义按钮的样式。内边距设置为水平16、垂直8,让按钮有足够的点击区域。激活状态使用蓝色背景,非激活状态使用灰色背景,通过颜色区分当前状态。
按钮内部的图标和文字布局。
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 24.sp, color: isActive ? Colors.blue : Colors.grey.shade700),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isActive ? Colors.blue : Colors.grey.shade700,
),
),
],
),
),
);
}
}
Column垂直排列图标和文字,mainAxisSize设置为min让Column只占用必要的空间。图标大小为24,文字大小为12,两者之间有4像素的间距。激活状态下图标和文字都使用蓝色,非激活状态使用深灰色。这种一致的颜色方案让按钮状态一目了然。
现在让我们深入理解每个控制功能在GameController中的实现。首先是撤销功能。
List<GameMove> moveHistory = [];
void undoMove() {
if (moveHistory.isEmpty) return;
GameMove lastMove = moveHistory.removeLast();
if (lastMove.previousValue != null) {
board[lastMove.row][lastMove.col] = lastMove.previousValue!;
}
if (lastMove.previousNotes != null) {
notes[lastMove.row][lastMove.col] = lastMove.previousNotes!;
}
update();
}
moveHistory是一个列表,记录了玩家的所有操作。每次输入数字或修改笔记时,都会向这个列表添加一个GameMove对象。undoMove方法从列表末尾取出最后一次操作,然后恢复该单元格的旧值和旧笔记。如果列表为空则直接返回,避免出错。这种设计让撤销功能可以无限次使用,玩家可以一直撤销到游戏开始的状态。
GameMove数据模型的定义。
class GameMove {
final int row;
final int col;
final int? previousValue;
final int? newValue;
final Set<int>? previousNotes;
final Set<int>? newNotes;
final DateTime timestamp;
GameMove({
required this.row,
required this.col,
this.previousValue,
this.newValue,
this.previousNotes,
this.newNotes,
DateTime? timestamp,
}) : timestamp = timestamp ?? DateTime.now();
}
GameMove记录了一次操作的完整信息:位置、旧值、新值、旧笔记、新笔记、时间戳。使用可空类型是因为有些操作只涉及数值变化,有些只涉及笔记变化。timestamp记录操作时间,可以用于统计分析或回放功能。这个数据模型的设计让撤销功能能够精确地恢复任何类型的操作。
擦除功能的实现。
void eraseCell() {
if (selectedRow < 0 || selectedCol < 0) return;
if (isFixed[selectedRow][selectedCol]) return;
int row = selectedRow;
int col = selectedCol;
moveHistory.add(GameMove(
row: row,
col: col,
previousValue: board[row][col],
newValue: 0,
previousNotes: Set.from(notes[row][col]),
newNotes: {},
));
board[row][col] = 0;
notes[row][col] = {};
update();
}
擦除功能首先检查是否有选中的单元格,以及该单元格是否可以修改。然后记录当前状态到历史中,将单元格的值设为0,清空笔记。这样擦除操作也可以被撤销。注意我们使用Set.from创建笔记的副本,避免直接引用导致的问题。
笔记模式的切换。
bool notesMode = false;
void toggleNotesMode() {
notesMode = !notesMode;
update();
}
笔记模式是一个简单的布尔值,toggleNotesMode方法切换这个值并更新UI。当笔记模式激活时,点击数字键盘会添加笔记而不是填入数字。这种模式切换的设计让玩家可以方便地在填数和记笔记之间切换。
提示功能的实现。
int hintsUsed = 0;
void useHint() {
if (selectedRow < 0 || selectedCol < 0) return;
if (isFixed[selectedRow][selectedCol]) return;
if (board[selectedRow][selectedCol] ==
solution[selectedRow][selectedCol]) return;
int row = selectedRow;
int col = selectedCol;
int correctValue = solution[row][col];
提示功能首先进行多项检查:是否有选中单元格、是否是固定数字、当前值是否已经正确。如果当前值已经是正确答案,就不需要提示了。然后从solution数组中获取正确答案。这些检查确保提示功能只在真正需要时才生效。
继续完成提示功能的实现。
moveHistory.add(GameMove(
row: row,
col: col,
previousValue: board[row][col],
newValue: correctValue,
previousNotes: Set.from(notes[row][col]),
newNotes: {},
));
board[row][col] = correctValue;
notes[row][col] = {};
hintsUsed++;
update();
_checkCompletion();
}
提示操作也会被记录到历史中,这样玩家可以撤销提示。填入正确答案后清空笔记,增加提示使用次数,然后检查游戏是否完成。hintsUsed的值会显示在提示按钮上,让玩家知道自己用了多少次提示。有些数独应用会限制提示次数,这里我们只是记录次数,不做限制。
让我们来优化控制按钮的视觉效果。添加按压动画可以提升交互体验。
class AnimatedControlButton extends StatefulWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
final bool isActive;
final bool isEnabled;
const AnimatedControlButton({
super.key,
required this.icon,
required this.label,
required this.onTap,
this.isActive = false,
this.isEnabled = true,
});
State<AnimatedControlButton> createState() => _AnimatedControlButtonState();
}
将控制按钮提取为独立的StatefulWidget,可以为每个按钮添加独立的动画效果。新增了isEnabled参数,用于控制按钮是否可用,比如当没有操作可撤销时,撤销按钮应该显示为禁用状态。
按钮的动画状态管理。
class _AnimatedControlButtonState extends State<AnimatedControlButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
AnimationController控制按压动画,时长100毫秒。缩放动画从1.0到0.95,比数字按钮的缩放幅度小一些,因为控制按钮本身就比较小。使用easeInOut曲线让动画更自然流畅。
处理按压事件和构建UI。
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: widget.isEnabled ? (_) => _controller.forward() : null,
onTapUp: widget.isEnabled ? (_) {
_controller.reverse();
widget.onTap();
} : null,
onTapCancel: () => _controller.reverse(),
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) => Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: widget.isEnabled ? 1.0 : 0.5,
child: child,
),
),
child: _buildButtonContent(),
),
);
}
当按钮禁用时,不响应点击事件,并且透明度降低到0.5。AnimatedBuilder监听动画值变化,Transform.scale实现缩放效果,Opacity控制透明度。这种组合让按钮的状态变化更加直观。
按钮内容的构建。
Widget _buildButtonContent() {
Color color = widget.isActive
? Colors.blue
: (widget.isEnabled ? Colors.grey.shade700 : Colors.grey.shade400);
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: widget.isActive ? Colors.blue.shade100 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8.r),
border: widget.isActive
? Border.all(color: Colors.blue, width: 1)
: null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon, size: 24.sp, color: color),
SizedBox(height: 4.h),
Text(
widget.label,
style: TextStyle(fontSize: 12.sp, color: color),
),
],
),
);
}
颜色根据激活状态和启用状态动态计算。激活状态的按钮添加了蓝色边框,让它更加突出。禁用状态使用更浅的灰色,视觉上表示不可用。这种细致的状态区分让用户界面更加清晰易懂。
资源释放。
void dispose() {
_controller.dispose();
super.dispose();
}
}
在组件销毁时释放AnimationController,这是Flutter动画开发的基本规范。忘记释放会导致内存泄漏,特别是在频繁创建销毁组件的场景下。
现在让我们考虑控制按钮的增强功能。撤销按钮可以显示可撤销的步数。
class EnhancedGameControls extends StatelessWidget {
const EnhancedGameControls({super.key});
Widget build(BuildContext context) {
return GetBuilder<GameController>(
builder: (controller) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildUndoButton(controller),
_buildEraseButton(controller),
_buildNotesButton(controller),
_buildHintButton(controller),
],
),
);
}
将每个按钮的构建逻辑分离到独立的方法中,让代码更清晰。这种结构也方便后续添加更多的控制按钮,比如重新开始、暂停等。
撤销按钮的增强实现。
Widget _buildUndoButton(GameController controller) {
bool canUndo = controller.moveHistory.isNotEmpty;
int undoCount = controller.moveHistory.length;
return GestureDetector(
onTap: canUndo ? controller.undoMove : null,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: canUndo ? Colors.grey.shade100 : Colors.grey.shade50,
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
Icon(
Icons.undo,
size: 24.sp,
color: canUndo ? Colors.grey.shade700 : Colors.grey.shade400,
),
if (canUndo && undoCount > 0)
Positioned(
right: -4,
top: -4,
child: Container(
padding: EdgeInsets.all(4.w),
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: Text(
undoCount > 99 ? '99+' : undoCount.toString(),
style: TextStyle(
fontSize: 8.sp,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
SizedBox(height: 4.h),
Text(
'撤销',
style: TextStyle(
fontSize: 12.sp,
color: canUndo ? Colors.grey.shade700 : Colors.grey.shade400,
),
),
],
),
),
);
}
撤销按钮在图标右上角显示一个小徽章,显示可撤销的步数。当步数超过99时显示"99+",避免徽章过大。当没有可撤销的操作时,按钮变灰且不可点击。这种设计让玩家清楚地知道自己还能撤销多少步。
擦除按钮的增强实现。
Widget _buildEraseButton(GameController controller) {
bool canErase = controller.selectedRow >= 0 &&
controller.selectedCol >= 0 &&
!controller.isFixed[controller.selectedRow][controller.selectedCol] &&
(controller.board[controller.selectedRow][controller.selectedCol] != 0 ||
controller.notes[controller.selectedRow][controller.selectedCol].isNotEmpty);
return GestureDetector(
onTap: canErase ? controller.eraseCell : null,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: canErase ? Colors.grey.shade100 : Colors.grey.shade50,
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.backspace_outlined,
size: 24.sp,
color: canErase ? Colors.grey.shade700 : Colors.grey.shade400,
),
SizedBox(height: 4.h),
Text(
'擦除',
style: TextStyle(
fontSize: 12.sp,
color: canErase ? Colors.grey.shade700 : Colors.grey.shade400,
),
),
],
),
),
);
}
擦除按钮只有在选中了非固定单元格,且该单元格有内容时才可用。这个判断逻辑比较复杂,需要检查选中状态、固定状态、数值和笔记。当按钮不可用时显示为灰色,给玩家明确的反馈。
笔记按钮的增强实现。
Widget _buildNotesButton(GameController controller) {
return GestureDetector(
onTap: controller.toggleNotesMode,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: controller.notesMode ? Colors.blue.shade100 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8.r),
border: controller.notesMode
? Border.all(color: Colors.blue, width: 2)
: null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
controller.notesMode ? Icons.edit : Icons.edit_outlined,
size: 24.sp,
color: controller.notesMode ? Colors.blue : Colors.grey.shade700,
),
SizedBox(height: 4.h),
Text(
controller.notesMode ? '笔记开' : '笔记',
style: TextStyle(
fontSize: 12.sp,
color: controller.notesMode ? Colors.blue : Colors.grey.shade700,
),
),
],
),
),
);
}
笔记按钮在激活时有明显的视觉变化:蓝色背景、蓝色边框、实心图标、文字变为"笔记开"。这些变化让玩家清楚地知道当前处于笔记模式,避免误操作。边框宽度为2像素,比普通状态更粗,增强视觉区分度。
提示按钮的增强实现。
Widget _buildHintButton(GameController controller) {
bool canUseHint = controller.selectedRow >= 0 &&
controller.selectedCol >= 0 &&
!controller.isFixed[controller.selectedRow][controller.selectedCol] &&
controller.board[controller.selectedRow][controller.selectedCol] !=
controller.solution[controller.selectedRow][controller.selectedCol];
return GestureDetector(
onTap: canUseHint ? controller.useHint : null,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: canUseHint ? Colors.amber.shade50 : Colors.grey.shade50,
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lightbulb_outline,
size: 24.sp,
color: canUseHint ? Colors.amber.shade700 : Colors.grey.shade400,
),
SizedBox(height: 4.h),
Text(
'提示(${controller.hintsUsed})',
style: TextStyle(
fontSize: 12.sp,
color: canUseHint ? Colors.amber.shade700 : Colors.grey.shade400,
),
),
],
),
),
);
}
}
提示按钮使用琥珀色调,与其他按钮的灰色调形成对比,暗示这是一个特殊功能。只有当选中的单元格需要提示时按钮才可用。括号中显示已使用的提示次数,让玩家有节制地使用提示功能。
控制按钮的触觉反馈。
import 'package:flutter/services.dart';
GestureDetector(
onTap: () {
HapticFeedback.mediumImpact();
controller.undoMove();
},
child: Container(
// 按钮内容
),
)
为控制按钮添加触觉反馈,使用mediumImpact比数字按钮的lightImpact稍强,因为控制操作通常更重要。不同的操作可以使用不同强度的反馈,比如擦除可以用heavyImpact,提示可以用selectionClick。
控制按钮的无障碍支持。
Semantics(
button: true,
label: '撤销,可撤销${controller.moveHistory.length}步',
enabled: controller.moveHistory.isNotEmpty,
child: GestureDetector(
onTap: controller.undoMove,
child: Container(
// 按钮内容
),
),
)
Semantics组件为屏幕阅读器提供按钮的描述信息。撤销按钮的描述包括可撤销的步数,让视障用户也能获得完整的信息。enabled属性告诉辅助技术按钮是否可用。
控制按钮的主题定制。
class GameControlsTheme {
final Color activeBackgroundColor;
final Color inactiveBackgroundColor;
final Color activeIconColor;
final Color inactiveIconColor;
final Color activeTextColor;
final Color inactiveTextColor;
final Color hintButtonColor;
final double iconSize;
final double fontSize;
final double borderRadius;
const GameControlsTheme({
this.activeBackgroundColor = const Color(0xFFE3F2FD),
this.inactiveBackgroundColor = const Color(0xFFF5F5F5),
this.activeIconColor = Colors.blue,
this.inactiveIconColor = const Color(0xFF616161),
this.activeTextColor = Colors.blue,
this.inactiveTextColor = const Color(0xFF616161),
this.hintButtonColor = const Color(0xFFFFA000),
this.iconSize = 24,
this.fontSize = 12,
this.borderRadius = 8,
});
}
通过主题类可以轻松定制控制按钮的外观。所有的颜色、尺寸都可以配置,让控制按钮能够适应不同的应用主题。提示按钮有单独的颜色配置,因为它通常需要特殊的视觉处理。
总结一下游戏控制按钮的关键设计要点。首先是清晰的功能划分,每个按钮负责一个明确的功能。其次是状态反馈,通过颜色、图标、文字的变化让玩家知道当前状态。然后是可用性判断,根据游戏状态动态启用或禁用按钮。最后是操作记录,所有操作都记录到历史中支持撤销。
这四个控制按钮虽然简单,但它们极大地提升了数独游戏的可玩性。撤销功能让玩家敢于尝试,擦除功能方便修改,笔记功能辅助思考,提示功能在卡住时提供帮助。这些功能的组合让数独游戏变得更加友好和有趣。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)