设置页面是应用的重要组成部分,它让用户可以根据自己的偏好定制应用的行为和外观。对于数独游戏来说,设置页面通常包括游戏设置、外观设置、关于信息等内容。今天我们来详细实现数独游戏的设置主界面。
请添加图片描述

创建SettingsPage组件

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});

  
  State<SettingsPage> createState() => _SettingsPageState();
}

SettingsPage使用StatefulWidget是因为需要管理各种设置的状态。当用户切换开关时,需要更新状态并保存到本地存储。ScreenUtil用于屏幕适配。

状态变量定义

class _SettingsPageState extends State<SettingsPage> {
  bool _showTimer = true;
  bool _autoCheckErrors = true;
  bool _soundEnabled = true;
  bool _darkMode = false;
  String _selectedTheme = 'classic';

定义各种设置的状态变量。_showTimer控制是否显示计时器,_autoCheckErrors控制是否自动高亮错误,_soundEnabled控制是否开启音效。这些默认值是大多数用户期望的行为。

主题列表定义

  final List<Map<String, dynamic>> _themes = [
    {'name': 'classic', 'label': '经典', 'color': Colors.blue},
    {'name': 'ocean', 'label': '海洋', 'color': Colors.cyan},
    {'name': 'forest', 'label': '森林', 'color': Colors.green},
    {'name': 'sunset', 'label': '日落', 'color': Colors.orange},
    {'name': 'minimal', 'label': '简约', 'color': Colors.grey},
  ];

主题列表定义了可选的主题,每个主题有名称、显示标签和代表颜色。使用List和Map的组合让主题数据易于管理和扩展。

页面结构

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('设置')),
      body: ListView(
        padding: EdgeInsets.all(16.w),
        children: [
          _buildSectionTitle('游戏设置'),
          _buildSwitchTile(
            title: '显示计时器',
            subtitle: '在游戏中显示用时',
            value: _showTimer,
            onChanged: (v) => setState(() => _showTimer = v),
          ),

Scaffold提供基本页面结构,AppBar显示页面标题。ListView让内容可以滚动。_buildSectionTitle构建分组标题,_buildSwitchTile构建开关设置项。

更多游戏设置

          _buildSwitchTile(
            title: '自动检查错误',
            subtitle: '实时高亮显示冲突',
            value: _autoCheckErrors,
            onChanged: (v) => setState(() => _autoCheckErrors = v),
          ),
          _buildSwitchTile(
            title: '音效',
            subtitle: '开启游戏音效',
            value: _soundEnabled,
            onChanged: (v) => setState(() => _soundEnabled = v),
          ),
          SizedBox(height: 24.h),

每个设置项都有标题和副标题,副标题解释设置的作用。onChanged回调在用户切换开关时触发,使用setState更新状态。24像素的间距将游戏设置与外观设置分开。

外观设置

          _buildSectionTitle('外观'),
          _buildSwitchTile(
            title: '深色模式',
            subtitle: '使用深色主题',
            value: _darkMode,
            onChanged: (v) => setState(() => _darkMode = v),
          ),
          SizedBox(height: 16.h),
          Text('主题颜色', style: TextStyle(fontSize: 16.sp)),
          SizedBox(height: 12.h),
          _buildThemeSelector(),
          SizedBox(height: 24.h),

深色模式使用开关控制。主题颜色使用自定义的选择器组件,让用户可以从多个预设主题中选择。这种分组设计让设置页面结构清晰。

关于信息

          _buildSectionTitle('关于'),
          ListTile(
            title: const Text('版本'),
            trailing: const Text('1.0.0'),
          ),
          ListTile(
            title: const Text('开源协议'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              // 显示开源协议
            },
          ),

关于部分显示应用版本和法律信息。ListTile是Material Design的标准列表项组件,trailing显示右侧内容。版本号直接显示文本,开源协议显示箭头图标表示可以点击。

隐私政策

          ListTile(
            title: const Text('隐私政策'),
            subtitle: const Text('所有数据仅存储在本地'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              // 显示隐私政策
            },
          ),
        ],
      ),
    );
  }

隐私政策说明数据存储方式,让用户放心。subtitle提供简短说明,点击可以查看详细内容。

分组标题实现

  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: EdgeInsets.only(bottom: 8.h),
      child: Text(
        title,
        style: TextStyle(
          fontSize: 18.sp,
          fontWeight: FontWeight.bold,
          color: Colors.blue,
        ),
      ),
    );
  }

分组标题使用蓝色粗体字,与普通文本区分开来。底部有8像素的内边距,与下方内容保持适当距离。这种设计让设置页面的结构层次分明。

开关设置项实现

  Widget _buildSwitchTile({
    required String title,
    required String subtitle,
    required bool value,
    required ValueChanged<bool> onChanged,
  }) {
    return SwitchListTile(
      title: Text(title),
      subtitle: Text(subtitle),
      value: value,
      onChanged: onChanged,
      activeColor: Colors.blue,
    );
  }

SwitchListTile是Material Design的开关列表项组件,集成了标题、副标题和开关。activeColor设置开关激活时的颜色为蓝色。这个方法封装了通用的开关设置项。

主题选择器

  Widget _buildThemeSelector() {
    return Wrap(
      spacing: 12.w,
      runSpacing: 12.h,
      children: _themes.map((theme) {
        bool isSelected = _selectedTheme == theme['name'];
        return GestureDetector(
          onTap: () => setState(() => _selectedTheme = theme['name']),
          child: Container(
            width: 60.w,
            height: 80.h,
            decoration: BoxDecoration(
              color: theme['color'],
              borderRadius: BorderRadius.circular(8.r),
              border: isSelected
                  ? Border.all(color: Colors.black, width: 3)
                  : null,
            ),

Wrap组件让主题选项自动换行,适应不同屏幕宽度。每个主题显示为一个彩色方块,选中的主题有黑色边框。GestureDetector处理点击事件。

主题标签

            child: Column(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                Container(
                  width: double.infinity,
                  padding: EdgeInsets.symmetric(vertical: 4.h),
                  decoration: BoxDecoration(
                    color: Colors.black.withOpacity(0.3),
                    borderRadius: BorderRadius.only(
                      bottomLeft: Radius.circular(8.r),
                      bottomRight: Radius.circular(8.r),
                    ),
                  ),
                  child: Text(
                    theme['label'],
                    textAlign: TextAlign.center,
                    style: TextStyle(fontSize: 12.sp, color: Colors.white),
                  ),
                ),
              ],
            ),
          ),
        );
      }).toList(),
    );
  }
}

主题标签显示在方块底部,使用半透明黑色背景让白色文字在任何颜色上都清晰可见。只有底部两个角有圆角,与方块的整体圆角一致。

数据持久化

增强设置页面,添加数据持久化功能。

class _SettingsPageState extends State<SettingsPage> {
  bool _showTimer = true;
  bool _autoCheckErrors = true;
  bool _soundEnabled = true;
  bool _darkMode = false;
  String _selectedTheme = 'classic';

  
  void initState() {
    super.initState();
    _loadSettings();
  }

在initState中加载保存的设置。这确保页面显示时使用用户之前保存的偏好设置。

加载设置

  Future<void> _loadSettings() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _showTimer = prefs.getBool('showTimer') ?? true;
      _autoCheckErrors = prefs.getBool('autoCheckErrors') ?? true;
      _soundEnabled = prefs.getBool('soundEnabled') ?? true;
      _darkMode = prefs.getBool('darkMode') ?? false;
      _selectedTheme = prefs.getString('theme') ?? 'classic';
    });
  }

SharedPreferences是Flutter的本地存储方案,适合存储简单的键值对数据。使用??操作符提供默认值,处理首次运行没有保存数据的情况。

保存设置

  Future<void> _saveSetting(String key, dynamic value) async {
    final prefs = await SharedPreferences.getInstance();
    if (value is bool) {
      await prefs.setBool(key, value);
    } else if (value is String) {
      await prefs.setString(key, value);
    }
  }

_saveSetting方法根据值的类型调用不同的保存方法。这种设计减少了重复代码,每个设置项的更新都可以使用同一个方法。

更新设置方法

  void _updateShowTimer(bool value) {
    setState(() => _showTimer = value);
    _saveSetting('showTimer', value);
  }

  void _updateAutoCheckErrors(bool value) {
    setState(() => _autoCheckErrors = value);
    _saveSetting('autoCheckErrors', value);
  }

  void _updateSoundEnabled(bool value) {
    setState(() => _soundEnabled = value);
    _saveSetting('soundEnabled', value);
  }

每个设置项都有对应的更新方法,同时更新状态和保存到本地存储。这种设计确保设置的更改立即生效并持久化。

深色模式切换

  void _updateDarkMode(bool value) {
    setState(() => _darkMode = value);
    _saveSetting('darkMode', value);
    Get.find<ThemeController>().setDarkMode(value);
  }

  void _updateTheme(String theme) {
    setState(() => _selectedTheme = theme);
    _saveSetting('theme', theme);
    Get.find<ThemeController>().setTheme(theme);
  }

深色模式和主题的更新还需要通知ThemeController,让整个应用的主题跟着变化。Get.find获取已注册的控制器实例。

重置设置对话框

  void _showResetDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('重置设置'),
        content: const Text('确定要将所有设置恢复为默认值吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              _resetSettings();
            },
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }

重置功能让用户可以恢复默认设置。显示确认对话框防止误操作。两个按钮分别用于取消和确认。

执行重置

  Future<void> _resetSettings() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
    await _loadSettings();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('设置已重置')),
    );
  }

prefs.clear()清除所有保存的设置,然后重新加载(会使用默认值)。SnackBar提示用户操作已完成。

震动设置

  bool _vibrationEnabled = true;

  _buildSwitchTile(
    title: '震动反馈',
    subtitle: '操作时提供触觉反馈',
    value: _vibrationEnabled,
    onChanged: (v) {
      setState(() => _vibrationEnabled = v);
      _saveSetting('vibrationEnabled', v);
      if (v) {
        HapticFeedback.mediumImpact();
      }
    },
  ),

震动设置控制是否在操作时提供触觉反馈。当用户开启震动时,立即触发一次震动让用户体验效果。

语言设置数据

  String _language = 'zh';

  final List<Map<String, String>> _languages = [
    {'code': 'zh', 'label': '简体中文'},
    {'code': 'en', 'label': 'English'},
  ];

语言设置支持多语言切换。_languages列表定义可选的语言,每个语言有代码和显示标签。

语言选择器

  Widget _buildLanguageSelector() {
    return ListTile(
      title: const Text('语言'),
      subtitle: Text(_languages.firstWhere((l) => l['code'] == _language)['label']!),
      trailing: const Icon(Icons.chevron_right),
      onTap: () => _showLanguageDialog(),
    );
  }

语言选择器显示当前选中的语言,点击打开选择对话框。firstWhere找到当前语言的标签显示在副标题。

语言选择对话框

  void _showLanguageDialog() {
    showDialog(
      context: context,
      builder: (context) => SimpleDialog(
        title: const Text('选择语言'),
        children: _languages.map((lang) {
          return SimpleDialogOption(
            onPressed: () {
              Navigator.pop(context);
              _updateLanguage(lang['code']!);
            },
            child: Row(
              children: [
                if (_language == lang['code'])
                  const Icon(Icons.check, color: Colors.blue)
                else
                  const SizedBox(width: 24),
                SizedBox(width: 12),
                Text(lang['label']!),
              ],
            ),
          );
        }).toList(),
      ),
    );
  }

SimpleDialog和SimpleDialogOption是Material Design的简单对话框组件。当前选中的语言显示勾选图标,让用户清楚知道当前设置。

清除数据对话框

  void _showClearDataDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('清除数据'),
        content: const Text('这将清除所有游戏记录和统计数据,此操作不可撤销。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              _clearAllData();
            },
            style: TextButton.styleFrom(foregroundColor: Colors.red),
            child: const Text('清除'),
          ),
        ],
      ),
    );
  }

清除数据是一个危险操作,使用红色按钮警示用户。对话框明确说明操作不可撤销。

执行清除

  Future<void> _clearAllData() async {
    await Get.find<StatsController>().clearStats();
    await Get.find<DailyController>().clearDailyData();
    
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('数据已清除')),
    );
  }

清除操作调用各个controller的清除方法,确保所有相关数据都被删除。SnackBar确认操作完成。

评分和反馈入口

  ListTile(
    title: const Text('给我们评分'),
    subtitle: const Text('如果喜欢这个应用,请给我们好评'),
    trailing: const Icon(Icons.star, color: Colors.amber),
    onTap: () {
      // 打开应用商店评分页面
    },
  ),
  ListTile(
    title: const Text('反馈问题'),
    subtitle: const Text('报告bug或提出建议'),
    trailing: const Icon(Icons.feedback),
    onTap: () {
      // 打开反馈页面或发送邮件
    },
  ),

评分和反馈入口帮助开发者获取用户反馈。评分使用金色星星图标,反馈使用反馈图标。点击后可以跳转到应用商店或发送邮件。

SettingsController集中管理

class SettingsController extends GetxController {
  bool showTimer = true;
  bool autoCheckErrors = true;
  bool soundEnabled = true;
  bool vibrationEnabled = true;
  bool darkMode = false;
  String theme = 'classic';
  String language = 'zh';
  
  
  void onInit() {
    super.onInit();
    loadSettings();
  }

SettingsController集中管理所有设置项。在onInit中加载保存的设置,确保应用启动时使用用户的偏好。

加载所有设置

  Future<void> loadSettings() async {
    final prefs = await SharedPreferences.getInstance();
    showTimer = prefs.getBool('showTimer') ?? true;
    autoCheckErrors = prefs.getBool('autoCheckErrors') ?? true;
    soundEnabled = prefs.getBool('soundEnabled') ?? true;
    vibrationEnabled = prefs.getBool('vibrationEnabled') ?? true;
    darkMode = prefs.getBool('darkMode') ?? false;
    theme = prefs.getString('theme') ?? 'classic';
    language = prefs.getString('language') ?? 'zh';
    update();
  }

loadSettings从SharedPreferences读取所有设置。使用??运算符提供默认值。最后调用update()通知UI更新。

通用保存方法

  Future<void> saveSetting(String key, dynamic value) async {
    final prefs = await SharedPreferences.getInstance();
    if (value is bool) {
      await prefs.setBool(key, value);
    } else if (value is String) {
      await prefs.setString(key, value);
    } else if (value is int) {
      await prefs.setInt(key, value);
    }
  }

saveSetting是一个通用的保存方法,根据值的类型调用不同的SharedPreferences方法。这种设计减少了重复代码。

设置项更新方法

  void setShowTimer(bool value) {
    showTimer = value;
    saveSetting('showTimer', value);
    update();
  }
  
  void setAutoCheckErrors(bool value) {
    autoCheckErrors = value;
    saveSetting('autoCheckErrors', value);
    update();
  }
  
  void setSoundEnabled(bool value) {
    soundEnabled = value;
    saveSetting('soundEnabled', value);
    update();
  }

每个设置项都有对应的setter方法,更新内存中的值、保存到本地、通知UI更新。这种模式确保设置的更改立即生效并持久化。

深色模式切换

  void setDarkMode(bool value) {
    darkMode = value;
    saveSetting('darkMode', value);
    Get.changeThemeMode(value ? ThemeMode.dark : ThemeMode.light);
    update();
  }

setDarkMode除了保存设置外,还调用Get.changeThemeMode切换应用的主题模式。GetX提供了便捷的主题切换API。

主题颜色切换

  void setTheme(String value) {
    theme = value;
    saveSetting('theme', value);
    _applyTheme(value);
    update();
  }
  
  void _applyTheme(String themeName) {
    Color primaryColor;
    switch (themeName) {
      case 'classic':
        primaryColor = Colors.blue;
        break;
      case 'ocean':
        primaryColor = Colors.cyan;
        break;
      case 'forest':
        primaryColor = Colors.green;
        break;
      case 'sunset':
        primaryColor = Colors.orange;
        break;
      case 'minimal':
        primaryColor = Colors.grey;
        break;
      default:
        primaryColor = Colors.blue;
    }

_applyTheme根据主题名称设置对应的主色调。switch语句映射主题名称到颜色值。

应用主题

    Get.changeTheme(ThemeData(
      primarySwatch: _createMaterialColor(primaryColor),
      brightness: darkMode ? Brightness.dark : Brightness.light,
    ));
  }

Get.changeTheme可以动态更改应用的主题,包括主色调和亮度。这让用户可以实时预览主题效果。

创建MaterialColor

  MaterialColor _createMaterialColor(Color color) {
    List<double> strengths = [.05, .1, .2, .3, .4, .5, .6, .7, .8, .9];
    Map<int, Color> swatch = {};
    
    for (int i = 0; i < strengths.length; i++) {
      double strength = strengths[i];
      swatch[(strength * 1000).round()] = Color.fromRGBO(
        color.red,
        color.green,
        color.blue,
        1,
      );
    }
    
    return MaterialColor(color.value, swatch);
  }
}

MaterialColor需要一个包含不同深浅度的色板。_createMaterialColor从单一颜色生成完整的色板。这个方法让我们可以使用任意颜色作为主题色。

在游戏中应用设置

class GamePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return GetBuilder<SettingsController>(
      builder: (settings) => Scaffold(
        appBar: AppBar(
          title: const Text('数独'),
          actions: [
            if (settings.showTimer)
              Padding(
                padding: EdgeInsets.only(right: 16.w),
                child: Center(
                  child: GetBuilder<GameController>(
                    builder: (game) => Text(game.formattedTime),
                  ),
                ),
              ),
          ],
        ),
        body: _buildGameBody(settings),
      ),
    );
  }
}

游戏页面使用GetBuilder监听SettingsController。根据showTimer设置决定是否显示计时器。这种响应式设计让设置的更改立即反映在游戏界面上。

错误检查设置应用

Widget _buildCell(GameController controller, SettingsController settings, 
    int row, int col) {
  bool hasConflict = settings.autoCheckErrors 
      ? controller.hasConflict(row, col) 
      : false;
  
  return Container(
    decoration: BoxDecoration(
      color: _getCellColor(isSelected, isHighlighted, isSameNumber, hasConflict),
    ),
    child: _buildCellContent(controller, row, col),
  );
}

只有当autoCheckErrors为true时才检查冲突。这让玩家可以选择是否需要实时错误提示。

音效设置应用

void enterNumber(int number) {
  final settings = Get.find<SettingsController>();
  
  if (settings.soundEnabled) {
    GameSoundManager.playTap();
  }
  if (settings.vibrationEnabled) {
    HapticFeedback.lightImpact();
  }
  
  board[selectedRow][selectedCol] = number;
  update();
}

在操作中检查音效和震动设置,只有启用时才播放音效或触发震动。Get.find获取设置控制器实例。

设置导出

Future<String> exportSettings() async {
  return jsonEncode({
    'showTimer': showTimer,
    'autoCheckErrors': autoCheckErrors,
    'soundEnabled': soundEnabled,
    'vibrationEnabled': vibrationEnabled,
    'darkMode': darkMode,
    'theme': theme,
    'language': language,
  });
}

exportSettings将所有设置导出为JSON字符串。这个功能可以用于在多设备间同步设置,或者备份恢复。

设置导入

Future<void> importSettings(String json) async {
  try {
    Map<String, dynamic> data = jsonDecode(json);
    showTimer = data['showTimer'] ?? true;
    autoCheckErrors = data['autoCheckErrors'] ?? true;
    soundEnabled = data['soundEnabled'] ?? true;
    vibrationEnabled = data['vibrationEnabled'] ?? true;
    darkMode = data['darkMode'] ?? false;
    theme = data['theme'] ?? 'classic';
    language = data['language'] ?? 'zh';
    
    await _saveAllSettings();
    _applyTheme(theme);
    update();
  } catch (e) {
    Get.snackbar('错误', '导入设置失败');
  }
}

importSettings从JSON恢复设置。使用try-catch处理解析错误。导入后保存到本地并应用主题。

总结

设置主界面的关键设计要点:分组组织(将相关设置项分组显示,使用标题区分)、即时反馈(设置更改立即生效并保存)、危险操作确认(重置和清除数据需要用户确认)、完整信息(包含版本、协议、隐私政策等必要信息)、集中管理(使用SettingsController统一管理设置)。

设置页面虽然不是应用的核心功能,但它直接影响用户体验。通过合理的设置选项和清晰的界面设计,我们可以让用户根据自己的偏好定制应用,获得更好的使用体验。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐