Flutter for OpenHarmony数独游戏App实战:设置主界面
本文介绍了数独游戏设置页面的实现,包含游戏设置、外观设置和关于信息三个主要部分。游戏设置提供计时器显示、自动查错和音效开关功能;外观设置包含深色模式和主题颜色选择;关于信息显示版本号和隐私政策。通过StatefulWidget管理设置状态,使用SwitchListTile实现开关选项,并采用分组设计使界面结构清晰。代码示例展示了如何使用Flutter构建美观实用的设置页面,包括状态管理、UI组件封
设置页面是应用的重要组成部分,它让用户可以根据自己的偏好定制应用的行为和外观。对于数独游戏来说,设置页面通常包括游戏设置、外观设置、关于信息等内容。今天我们来详细实现数独游戏的设置主界面。
创建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
更多推荐

所有评论(0)