暂停功能是数独游戏的基本功能之一。当玩家需要暂时离开游戏时,可以暂停游戏,计时器停止,棋盘隐藏。这样既保护了游戏进度,也防止玩家在暂停时继续思考。今天我们来详细实现数独游戏的暂停与继续功能。
请添加图片描述

设计考虑

在设计暂停功能之前,我们需要考虑几个关键问题:暂停时的界面显示(应该隐藏棋盘防止玩家继续思考)、计时器的处理(暂停时应该停止计时)、自动暂停(当应用进入后台时应该自动暂停)。

暂停状态管理

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐