Flutter for OpenHarmony数独游戏App实战:暂停与继续
本文介绍了数独游戏暂停功能的实现方法。通过状态管理控制游戏暂停/继续状态,暂停时隐藏棋盘并停止计时器。设计考虑了自动暂停(应用进入后台时触发)、界面显示(防止玩家继续思考)和状态保存(防止数据丢失)。代码展示了如何使用Flutter实现暂停界面、计时器控制和生命周期监听,确保游戏体验的完整性。
暂停功能是数独游戏的基本功能之一。当玩家需要暂时离开游戏时,可以暂停游戏,计时器停止,棋盘隐藏。这样既保护了游戏进度,也防止玩家在暂停时继续思考。今天我们来详细实现数独游戏的暂停与继续功能。
设计考虑
在设计暂停功能之前,我们需要考虑几个关键问题:暂停时的界面显示(应该隐藏棋盘防止玩家继续思考)、计时器的处理(暂停时应该停止计时)、自动暂停(当应用进入后台时应该自动暂停)。
暂停状态管理
class GameController extends GetxController {
bool isPaused = false;
void pauseGame() {
isPaused = true;
update();
}
void resumeGame() {
isPaused = false;
update();
}
isPaused标记游戏是否处于暂停状态。pauseGame和resumeGame方法分别设置暂停和恢复状态。update()通知UI更新,显示相应的界面。
计时器与暂停配合
void incrementTimer() {
if (!isPaused && !isComplete) {
elapsedSeconds++;
update();
}
}
incrementTimer只在游戏进行中才增加时间。当isPaused为true时,即使Timer继续触发回调,时间也不会增加。这确保了暂停时计时器真正停止。
暂停界面
Widget _buildPausedScreen() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pause_circle_outline, size: 80.sp, color: Colors.grey),
SizedBox(height: 20.h),
Text('游戏已暂停', style: TextStyle(fontSize: 24.sp)),
SizedBox(height: 20.h),
ElevatedButton(
onPressed: controller.resumeGame,
child: const Text('继续游戏'),
),
],
),
);
}
暂停界面显示一个大的暂停图标、提示文字和继续按钮。Center和Column组合实现垂直居中布局。这个界面完全覆盖棋盘,防止玩家在暂停时看到棋盘继续思考。
根据状态显示界面
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('数独'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: _showNewGameDialog,
),
],
),
body: GetBuilder<GameController>(
builder: (ctrl) {
if (ctrl.isComplete) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showVictoryDialog();
});
}
return ctrl.isPaused
? _buildPausedScreen()
: _buildGameScreen();
},
),
);
}
GetBuilder监听controller状态变化。根据isPaused决定显示暂停界面还是游戏界面。游戏完成时使用addPostFrameCallback延迟显示胜利对话框,避免在build过程中显示对话框。
信息栏与暂停按钮
Widget _buildInfoBar() {
return GetBuilder<GameController>(
builder: (ctrl) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
ctrl.difficulty,
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
),
),
信息栏显示难度、计时器和暂停按钮。难度使用圆角标签样式,蓝色背景突出显示。GetBuilder确保状态变化时自动更新。
计时器和暂停按钮
Row(
children: [
Icon(Icons.timer, size: 20.sp),
SizedBox(width: 4.w),
Text(
ctrl.formattedTime,
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
],
),
IconButton(
icon: const Icon(Icons.pause),
onPressed: ctrl.pauseGame,
),
],
),
);
}
计时器显示图标和格式化的时间。暂停按钮使用暂停图标,点击后调用pauseGame方法。按钮位于信息栏右侧,方便单手操作。
自动暂停功能
class _GamePageState extends State<GamePage> with WidgetsBindingObserver {
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_startTimer();
}
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_timer?.cancel();
super.dispose();
}
WidgetsBindingObserver监听应用生命周期变化。initState中注册观察者,dispose中移除观察者并取消计时器。这是Flutter中监听应用生命周期的标准方式。
生命周期回调
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
controller.pauseGame();
}
}
当应用进入后台(paused状态)时,自动暂停游戏。这确保了玩家切换应用时游戏会自动暂停,计时器停止。不需要在resumed时自动恢复,让玩家手动点击继续。
暂停时保存状态
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
controller.pauseGame();
_saveGameState();
}
}
Future<void> _saveGameState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('savedGame', jsonEncode(controller.toJson()));
}
应用进入后台时,除了暂停游戏,还保存当前状态。这样即使应用被系统杀死,下次打开时也能恢复游戏进度。使用SharedPreferences存储JSON格式的游戏状态。
动画暂停界面组件
class AnimatedPauseScreen extends StatefulWidget {
final VoidCallback onResume;
const AnimatedPauseScreen({super.key, required this.onResume});
State<AnimatedPauseScreen> createState() => _AnimatedPauseScreenState();
}
class _AnimatedPauseScreenState extends State<AnimatedPauseScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
AnimatedPauseScreen使用组合动画让暂停界面出现更加平滑。需要AnimationController控制动画,两个Animation分别控制淡入和缩放效果。
动画初始化
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_controller.forward();
}
动画时长300毫秒。淡入动画从完全透明到完全不透明,缩放动画从0.8放大到1.0。initState中启动动画,让暂停界面平滑出现。
暂停界面构建
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Container(
color: Colors.white.withOpacity(0.95),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pause_circle_outline, size: 80.sp, color: Colors.grey),
SizedBox(height: 20.h),
Text('游戏已暂停', style: TextStyle(fontSize: 24.sp)),
SizedBox(height: 20.h),
ElevatedButton(
onPressed: widget.onResume,
child: const Text('继续游戏'),
),
],
),
),
),
),
);
}
FadeTransition和ScaleTransition组合产生淡入缩放效果。半透明白色背景让暂停界面有一种覆盖层的感觉,同时隐藏下方的棋盘。
暂停菜单选项
Widget _buildPausedScreen() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pause_circle_outline, size: 80.sp, color: Colors.grey),
SizedBox(height: 20.h),
Text('游戏已暂停', style: TextStyle(fontSize: 24.sp)),
SizedBox(height: 8.h),
Text(
'用时: ${controller.formattedTime}',
style: TextStyle(fontSize: 16.sp, color: Colors.grey),
),
SizedBox(height: 30.h),
ElevatedButton(
onPressed: controller.resumeGame,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 40.w, vertical: 12.h),
),
child: const Text('继续游戏'),
),
SizedBox(height: 12.h),
TextButton(
onPressed: _showNewGameDialog,
child: const Text('新游戏'),
),
TextButton(
onPressed: () {
// 返回主菜单
},
child: const Text('返回主菜单'),
),
],
),
);
}
暂停界面除了继续按钮,还提供新游戏和返回主菜单的选项。显示当前用时让玩家知道自己的进度。这些选项让暂停界面更加实用。
暂停状态持久化
class PauseStateManager {
static Future<void> savePauseState(bool isPaused, int elapsedSeconds) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isPaused', isPaused);
await prefs.setInt('pausedAt', DateTime.now().millisecondsSinceEpoch);
await prefs.setInt('elapsedSeconds', elapsedSeconds);
}
static Future<Map<String, dynamic>> loadPauseState() async {
final prefs = await SharedPreferences.getInstance();
return {
'isPaused': prefs.getBool('isPaused') ?? false,
'pausedAt': prefs.getInt('pausedAt') ?? 0,
'elapsedSeconds': prefs.getInt('elapsedSeconds') ?? 0,
};
}
PauseStateManager保存暂停状态到本地存储。当应用被系统杀死后重新打开时,可以恢复暂停状态。pausedAt记录暂停的时间戳,用于计算离开的时长。
清除暂停状态
static Future<void> clearPauseState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('isPaused');
await prefs.remove('pausedAt');
}
}
游戏结束或开始新游戏时清除暂停状态。remove方法删除指定的键值对。这确保了即使应用被强制关闭,游戏状态也不会丢失。
模糊效果暂停界面
Widget _buildBlurredBoard() {
return Stack(
children: [
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: _buildGameBoard(),
),
Container(
color: Colors.white.withOpacity(0.7),
),
Center(
child: _buildPauseContent(),
),
],
);
}
使用ImageFiltered对棋盘应用模糊效果,让暂停界面更有层次感。模糊的棋盘在背景中若隐若现,但玩家无法看清具体数字。半透明白色遮罩进一步降低棋盘的可见度。
暂停内容
Widget _buildPauseContent() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pause_circle_filled, size: 80.sp, color: Colors.blue),
SizedBox(height: 20.h),
Text(
'游戏已暂停',
style: TextStyle(fontSize: 28.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 8.h),
Text(
'用时: ${controller.formattedTime}',
style: TextStyle(fontSize: 18.sp, color: Colors.grey.shade600),
),
SizedBox(height: 30.h),
ElevatedButton.icon(
onPressed: controller.resumeGame,
icon: const Icon(Icons.play_arrow),
label: const Text('继续游戏'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 16.h),
),
),
],
);
}
暂停内容使用蓝色填充的暂停图标,更加醒目。继续按钮使用ElevatedButton.icon,包含播放图标和文字。这种设计既美观又实用。
暂停菜单卡片
Widget _buildPauseMenu() {
return Container(
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
spreadRadius: 5,
),
],
),
暂停菜单使用卡片式设计,白色背景、圆角和阴影让菜单更加突出。boxShadow添加较大的模糊半径,产生柔和的阴影效果。
菜单内容
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.pause_circle_outline, size: 60.sp, color: Colors.blue),
SizedBox(height: 16.h),
Text('游戏已暂停', style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 8.h),
Text('用时: ${controller.formattedTime}', style: TextStyle(fontSize: 16.sp, color: Colors.grey)),
SizedBox(height: 24.h),
_buildMenuButton(Icons.play_arrow, '继续游戏', controller.resumeGame, Colors.green),
SizedBox(height: 12.h),
_buildMenuButton(Icons.refresh, '重新开始', _restartGame, Colors.orange),
SizedBox(height: 12.h),
_buildMenuButton(Icons.add, '新游戏', _showNewGameDialog, Colors.blue),
SizedBox(height: 12.h),
_buildMenuButton(Icons.home, '返回主页', _goToHome, Colors.grey),
],
),
);
}
菜单提供多个选项:继续游戏、重新开始、新游戏、返回主页。每个按钮使用不同颜色区分功能。mainAxisSize.min让卡片高度自适应内容。
菜单按钮
Widget _buildMenuButton(IconData icon, String label, VoidCallback onTap, Color color) {
return SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: onTap,
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 14.h),
),
),
);
}
_buildMenuButton创建统一样式的菜单按钮。SizedBox设置宽度为父容器宽度,让按钮占满整行。每个按钮使用对应的颜色,白色文字和图标。
暂停统计信息
Widget _buildPauseStats() {
return Container(
margin: EdgeInsets.only(top: 20.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
Text('本局统计', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 12.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('难度', controller.difficulty),
_buildStatItem('用时', controller.formattedTime),
_buildStatItem('提示', '${controller.hintsUsed}次'),
],
),
],
),
);
}
暂停界面显示当前游戏的统计信息,包括难度、用时和提示使用次数。这让玩家在暂停时可以回顾自己的进度。灰色背景区分统计区域。
统计项
Widget _buildStatItem(String label, String value) {
return Column(
children: [
Text(value, style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.blue)),
SizedBox(height: 4.h),
Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
],
);
}
每个统计项显示数值和标签。数值使用蓝色加粗,标签使用灰色小字。这种布局清晰展示统计信息,让信息层次更清晰。
恢复倒计时组件
class PauseResumeCountdown extends StatefulWidget {
final VoidCallback onComplete;
const PauseResumeCountdown({super.key, required this.onComplete});
State<PauseResumeCountdown> createState() => _PauseResumeCountdownState();
}
class _PauseResumeCountdownState extends State<PauseResumeCountdown> {
int _countdown = 3;
Timer? _timer;
PauseResumeCountdown在恢复游戏前显示3秒倒计时。这给玩家一个准备时间,不会因为突然恢复而措手不及。使用Timer实现倒计时。
倒计时逻辑
void initState() {
super.initState();
_startCountdown();
}
void _startCountdown() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
_countdown--;
if (_countdown <= 0) {
timer.cancel();
widget.onComplete();
}
});
});
}
void dispose() {
_timer?.cancel();
super.dispose();
}
Timer.periodic每秒触发一次,减少倒计时数字。倒计时结束时取消Timer并调用onComplete回调。dispose中确保Timer被取消,避免内存泄漏。
倒计时显示
Widget build(BuildContext context) {
return Center(
child: Text(
'$_countdown',
style: TextStyle(
fontSize: 80.sp,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
);
}
}
倒计时数字大而醒目,让玩家清楚知道还有多久恢复。使用蓝色加粗文字,80sp的字号确保在任何屏幕上都清晰可见。
使用倒计时恢复
void _resumeWithCountdown() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
backgroundColor: Colors.transparent,
child: PauseResumeCountdown(
onComplete: () {
Navigator.pop(context);
controller.resumeGame();
},
),
),
);
}
点击继续按钮后,先显示倒计时对话框,倒计时结束后自动关闭对话框并恢复游戏。barrierDismissible设为false防止玩家点击外部关闭对话框。透明背景让倒计时数字更加突出。
双击暂停手势
class PauseGestureDetector extends StatelessWidget {
final Widget child;
final VoidCallback onPause;
const PauseGestureDetector({
super.key,
required this.child,
required this.onPause,
});
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: onPause,
child: child,
);
}
}
PauseGestureDetector允许玩家通过双击棋盘快速暂停游戏。这是一个便捷的交互方式,不需要点击暂停按钮。双击是一个不容易误触的手势,适合用于暂停这种重要操作。
暂停原因记录
enum PauseReason {
manual,
appBackground,
phoneCall,
notification,
}
class PauseRecord {
PauseReason reason;
DateTime pausedAt;
DateTime? resumedAt;
PauseRecord({
required this.reason,
required this.pausedAt,
this.resumedAt,
});
Duration get pauseDuration {
DateTime endTime = resumedAt ?? DateTime.now();
return endTime.difference(pausedAt);
}
}
PauseRecord记录每次暂停的原因和时长。PauseReason枚举区分手动暂停、应用后台、电话、通知等原因。pauseDuration计算暂停时长,这些数据可以用于分析玩家的游戏习惯。
总结
暂停与继续功能的关键设计要点:状态管理(使用isPaused标记暂停状态)、界面切换(暂停时显示覆盖界面隐藏棋盘)、计时器配合(暂停时停止计时)、自动暂停(应用进入后台时自动暂停)、状态持久化(确保暂停状态不会因应用关闭而丢失)、丰富的菜单选项(让玩家在暂停时有更多选择)。
暂停功能是数独游戏的基本功能,它让玩家可以随时中断游戏而不丢失进度。良好的暂停功能设计可以提升用户体验,让游戏更加人性化。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)