Flutter for OpenHarmony 游戏中心App实战:游戏历史记录实现
本文介绍了游戏历史记录页面的设计与实现,采用Flutter框架构建。主要内容包括: 设计思路:采用卡片式布局展示游戏名称、得分、时间等信息,使用ListView优化性能 组件结构:定义GameHistoryPage无状态组件,分离UI与数据逻辑 数据模拟:使用List.generate创建10条测试记录,包含游戏图标、得分和时间 页面框架:Scaffold+AppBar标准布局,统一深蓝色主题风格

游戏历史记录是游戏中心应用中非常实用的功能,它让玩家可以回顾自己的游戏轨迹,查看每次游戏的得分和时间。这不仅能帮助玩家追踪自己的进步,还能激发他们挑战更高分数的动力。在本文中,我们将实现一个完整的游戏历史记录页面,包括历史数据的展示、时间格式化、列表优化等功能。
历史记录的设计思路
游戏历史记录页面需要展示玩家过往的游戏数据,每条记录应该包含游戏名称、得分、游戏时间等关键信息。设计时我们要考虑几个要点:数据的展示要清晰易读,让玩家一眼就能看到重要信息;列表要支持滚动,因为历史记录可能会很多;每条记录的样式要统一,保持视觉的一致性。
我们采用卡片式的设计,每个游戏记录显示为一个独立的卡片。卡片左侧显示游戏图标,中间显示游戏名称、得分和时间,整体布局简洁明了。使用ListView来展示历史记录列表,这样可以高效地渲染大量数据,只有可见区域的内容会被实际构建。
时间的显示格式也很重要。我们使用intl包来格式化日期时间,将DateTime对象转换为易读的字符串格式,比如"2024-01-23 14:30"。这种格式既包含了日期又包含了时间,让玩家能够准确地回忆起每次游戏的时刻。
页面组件的定义
GameHistoryPage是一个无状态组件,因为历史数据的管理可以通过状态管理方案来处理,页面本身只负责展示。
class GameHistoryPage extends StatelessWidget {
const GameHistoryPage({super.key});
Widget build(BuildContext context) {
使用StatelessWidget让组件更加简单高效。虽然历史记录是动态数据,但我们可以通过外部的状态管理来更新数据,页面本身不需要维护内部状态。这种设计符合Flutter的最佳实践,将数据逻辑和UI展示分离。
const构造函数表示这个Widget是编译时常量,可以提高性能。super.key传递给父类,用于Widget的标识和优化。这些看似简单的细节,实际上对应用的性能有着积极的影响。
模拟历史数据
在实际应用中,历史数据应该从数据库或服务器获取。这里我们先使用模拟数据来展示页面效果。
final history = List.generate(10, (index) {
return {
'game': '游戏 ${index + 1}',
'score': (index + 1) * 100,
'time': DateTime.now().subtract(Duration(days: index)),
'icon': ['🧩', '🎴', '❓', '🔢', '📝'][index % 5],
};
});
List.generate是一个非常实用的方法,它可以快速生成一个列表。这里我们生成10条历史记录,每条记录包含游戏名称、得分、时间和图标。游戏名称使用字符串插值生成,得分按照索引递增,时间使用DateTime.now()减去相应的天数,模拟不同时间的游戏记录。
图标使用emoji数组,通过index % 5取模运算循环使用。这种方式让不同的游戏显示不同的图标,增加了视觉的丰富性。Map结构让数据的组织非常清晰,每个字段都有明确的含义。
在实际开发中,这部分代码应该替换为从数据库读取数据的逻辑。可以使用sqflite来存储历史记录,每次游戏结束时保存数据,页面打开时查询数据。
页面框架的构建
页面使用Scaffold作为基本框架,AppBar显示标题,body部分使用ListView展示历史记录列表。
return Scaffold(
appBar: AppBar(
title: const Text('游戏历史'),
backgroundColor: const Color(0xFF16213e),
),
Scaffold是Flutter中最常用的页面框架组件,它提供了标准的Material Design页面结构。AppBar显示页面标题"游戏历史",让用户清楚地知道当前浏览的内容。
backgroundColor设置为深蓝色(0xFF16213e),这是应用的主题色。保持AppBar的颜色与整体主题一致,可以让应用看起来更加统一和专业。这个颜色在整个应用中反复使用,形成了一致的视觉语言。
const关键字用于Text,因为标题文本是固定的。使用const可以让Flutter在编译时创建这个对象,而不是在运行时,从而提高性能。这些小的优化累积起来,可以让应用运行得更加流畅。
列表的构建
历史记录列表使用ListView.builder来构建,这是展示大量数据的最佳方式。
body: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: history.length,
itemBuilder: (context, index) {
final item = history[index];
ListView.builder是一个懒加载的列表组件,它只会构建可见区域的列表项。这种方式比一次性构建所有列表项要高效得多,特别是当历史记录很多时,可以显著提高性能。
padding设置为EdgeInsets.all(16.w),在列表四周添加16个设计稿单位的内边距。这样列表内容不会紧贴屏幕边缘,看起来更加舒适。使用flutter_screenutil的适配单位,确保在不同设备上显示一致。
itemCount设置为history.length,表示列表项的数量。itemBuilder是一个回调函数,接收context和index两个参数,返回每个列表项的Widget。在回调函数中,我们首先获取当前索引对应的历史记录数据。
历史记录卡片的设计
每条历史记录显示为一个卡片,包含游戏图标、名称、得分和时间。
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: const Color(0xFF16213e),
borderRadius: BorderRadius.circular(12.r),
),
Container是卡片的容器,margin设置了底部间距12.h,让相邻的卡片之间有一定的间隔。这种间隔让列表看起来不会太拥挤,每个卡片都有自己的空间。
padding设置了内边距16.w,让卡片内的内容不会紧贴边缘。适当的内边距可以让内容更加舒适,提升视觉体验。
decoration定义了容器的装饰样式。color设置为深蓝色,与AppBar的颜色一致,形成了统一的视觉风格。borderRadius设置为12.r,创建了圆角效果。圆角让卡片看起来更加柔和,符合现代UI设计的趋势。
BoxDecoration是Flutter中非常强大的装饰类,除了颜色和圆角,还可以设置边框、阴影、渐变等效果。这里我们只使用了基本的颜色和圆角,保持了简洁的设计风格。
卡片内容的布局
卡片内容使用Row水平排列,左侧是游戏图标,右侧是游戏信息。
child: Row(
children: [
Text(item['icon'] as String, style: TextStyle(fontSize: 40.sp)),
SizedBox(width: 16.w),
Row组件水平排列子Widget。第一个子元素是游戏图标,使用Text显示emoji。fontSize设置为40.sp,这是一个比较大的尺寸,让图标清晰可见。
使用emoji作为游戏图标是一个巧妙的设计。emoji不需要准备图片资源,在所有平台上都有统一的显示效果,而且非常直观。不同的游戏使用不同的emoji,让用户可以快速识别游戏类型。
SizedBox添加了16.w的水平间距,将图标和文字信息分开。适当的间距让布局更加清晰,不会显得拥挤。使用SizedBox创建间距是Flutter中的标准做法,比使用Padding更加简洁。
游戏信息的展示
游戏信息包括名称、得分和时间,使用Column垂直排列。Expanded组件让这部分内容占据剩余的水平空间。
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item['game'] as String, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 4.h),
Expanded是一个非常有用的布局组件,它会让子Widget占据父Widget中剩余的空间。在Row中使用Expanded,可以让某个子Widget自动填充剩余的水平空间。这样无论屏幕宽度如何,游戏信息都会占据图标右侧的所有空间。
Column垂直排列游戏名称、得分和时间。crossAxisAlignment设置为start,让文本左对齐。这是文本内容的标准对齐方式,符合阅读习惯。
第一个Text显示游戏名称,fontSize设置为16.sp,fontWeight设置为bold。较大的字号和粗体让游戏名称非常醒目,这是最重要的信息,应该最先被用户注意到。
SizedBox添加了4.h的垂直间距,将游戏名称和得分分开。这个间距比较小,因为这些信息是紧密相关的,不需要太大的分隔。
得分和时间的显示
得分使用琥珀色显示,突出重要性。时间使用半透明的白色,表明这是次要信息。
Text('得分: ${item['score']}', style: TextStyle(fontSize: 14.sp, color: Colors.amber)),
SizedBox(height: 2.h),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(item['time'] as DateTime),
style: TextStyle(fontSize: 12.sp, color: Colors.white60),
),
得分文本使用字符串插值将分数嵌入到文本中。fontSize设置为14.sp,比游戏名称小一些。color设置为Colors.amber琥珀色,这是一个暖色调,给人积极、成功的感觉。使用特殊的颜色来显示得分,可以让这个信息更加突出。
又一个SizedBox添加2.h的间距,将得分和时间分开。这个间距更小,因为得分和时间都是次要信息,关联性更强。
时间的显示使用了intl包的DateFormat类。DateFormat(‘yyyy-MM-dd HH:mm’)定义了日期时间的格式,yyyy表示四位年份,MM表示两位月份,dd表示两位日期,HH表示24小时制的小时,mm表示分钟。format方法将DateTime对象转换为这种格式的字符串。
时间文本的fontSize设置为12.sp,是最小的字号,表明这是最次要的信息。color设置为Colors.white60,这是一个半透明的白色,看起来比纯白色更柔和,适合用于次要文本。
时间格式化的重要性
时间格式化是历史记录功能的关键部分,它让时间信息变得易读易懂。
DateFormat是intl包提供的强大工具,支持多种日期时间格式。我们选择的’yyyy-MM-dd HH:mm’格式是一个常用的格式,既包含了完整的日期信息,又包含了精确到分钟的时间信息。这种格式在中文环境中非常直观,用户可以快速理解。
如果需要显示相对时间,比如"3天前"、“1小时前”,可以自己实现一个相对时间的计算函数。比较当前时间和游戏时间的差值,根据差值的大小返回不同的字符串。这种相对时间的显示方式在社交应用中很常见,让时间信息更加人性化。
intl包还支持国际化,可以根据用户的语言设置显示不同语言的日期时间。比如英文环境下显示"Jan 23, 2024",中文环境下显示"2024年1月23日"。这种国际化的支持让应用可以服务全球用户。
列表性能的优化
虽然ListView.builder已经很高效了,但当历史记录非常多时,还可以进一步优化。
一个优化方法是实现分页加载。不是一次性加载所有历史记录,而是每次只加载一部分,比如20条。当用户滚动到列表底部时,再加载下一页数据。这种无限滚动的方式可以显著减少初始加载时间,提升用户体验。
另一个优化是使用缓存。将已经加载的历史记录缓存在内存中,下次打开页面时直接使用缓存数据,同时在后台刷新数据。这样用户可以立即看到内容,不需要等待数据加载。
还可以添加下拉刷新功能,让用户可以手动刷新历史记录。使用RefreshIndicator组件包裹ListView,用户下拉时会触发刷新回调。这种交互方式在移动应用中非常常见,用户已经习惯了这种操作。
空状态的处理
当用户还没有游戏历史时,应该显示一个友好的空状态提示。
if (history.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 80.sp, color: Colors.white30),
SizedBox(height: 20.h),
Text('还没有游戏历史', style: TextStyle(fontSize: 16.sp, color: Colors.white60)),
SizedBox(height: 10.h),
Text('快去玩游戏吧!', style: TextStyle(fontSize: 14.sp, color: Colors.white60)),
],
),
);
}
这段代码检查历史记录是否为空,如果为空则显示空状态提示。使用Center组件将内容居中显示,Column垂直排列图标和文字。
图标使用Material Icons中的history图标,size设置为80.sp,color设置为半透明的白色。大号的图标作为视觉焦点,让空状态不会显得太单调。
文字提示分为两行,第一行说明当前状态"还没有游戏历史",第二行鼓励用户"快去玩游戏吧!"。这种友好的提示比简单的"暂无数据"要好得多,给用户明确的指引。
空状态的设计是用户体验的重要组成部分。一个好的空状态不仅要告诉用户当前的情况,还要引导用户采取行动。在这个例子中,我们鼓励用户去玩游戏,这样就能产生历史记录了。
历史记录的筛选
随着历史记录的增多,用户可能需要筛选功能来快速找到特定的记录。
可以添加一个筛选按钮,点击后弹出筛选选项。比如按游戏类型筛选,只显示某个特定游戏的历史记录。或者按时间范围筛选,只显示最近一周或一个月的记录。
筛选功能的实现可以使用where方法。比如筛选特定游戏的记录:
final filteredHistory = history.where((item) => item['game'] == selectedGame).toList();
这行代码使用where方法筛选历史记录列表,只保留游戏名称匹配的记录。toList方法将筛选结果转换为列表。这种函数式的写法简洁明了,一行代码就完成了筛选逻辑。
筛选条件可以保存在State中,当用户选择不同的筛选条件时,调用setState更新筛选结果,触发UI重建。这样用户可以实时看到筛选效果。
历史记录的排序
除了筛选,排序也是一个实用的功能。用户可能想按时间排序,看最近的游戏记录;或者按得分排序,看自己的最高分。
排序功能可以使用sort方法。比如按时间降序排序(最新的在前):
history.sort((a, b) => (b['time'] as DateTime).compareTo(a['time'] as DateTime));
这行代码使用sort方法对历史记录列表进行排序。sort接收一个比较函数,返回负数表示a在b前面,返回正数表示b在a前面。这里我们比较时间,b的时间与a的时间比较,实现降序排序。
按得分排序也类似:
history.sort((a, b) => (b['score'] as int).compareTo(a['score'] as int));
可以在AppBar中添加一个排序按钮,点击时弹出排序选项菜单。用户选择排序方式后,更新列表的排序,让用户可以从不同角度查看历史记录。
历史记录的详情
点击历史记录卡片时,可以跳转到详情页面,显示更多信息。
详情页面可以显示游戏的完整信息,比如游戏时长、使用的道具、达成的成就等。还可以显示游戏过程的统计数据,比如每一步的操作、得分的变化曲线等。
跳转到详情页面使用GetX的路由功能:
onTap: () => Get.to(() => GameHistoryDetailPage(record: item))
这行代码在卡片的onTap回调中调用Get.to跳转到详情页面,同时传入历史记录数据。详情页面可以根据这些数据展示完整的游戏信息。
详情页面还可以提供一些操作,比如删除这条记录、分享到社交媒体、重玩这个游戏等。这些功能可以增加历史记录的实用性,让它不仅仅是一个查看工具,还是一个交互中心。
数据持久化的实现
历史记录需要持久化存储,这样即使应用关闭,数据也不会丢失。
可以使用sqflite来存储历史记录。首先定义一个数据库表:
CREATE TABLE game_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_id TEXT NOT NULL,
game_name TEXT NOT NULL,
score INTEGER NOT NULL,
play_time TEXT NOT NULL,
duration INTEGER NOT NULL
)
这个表包含了历史记录的所有字段。id是自增主键,game_id是游戏的唯一标识,game_name是游戏名称,score是得分,play_time是游戏时间,duration是游戏时长。
每次游戏结束时,将数据插入到数据库:
await db.insert('game_history', {
'game_id': gameId,
'game_name': gameName,
'score': score,
'play_time': DateTime.now().toIso8601String(),
'duration': duration,
});
insert方法将数据插入到指定的表中。第一个参数是表名,第二个参数是一个Map,包含要插入的数据。play_time使用toIso8601String方法将DateTime转换为ISO 8601格式的字符串,这是一个标准的日期时间格式,便于存储和解析。
页面打开时,从数据库查询数据:
final List<Map<String, dynamic>> maps = await db.query('game_history', orderBy: 'play_time DESC');
这行代码查询game_history表的所有记录,按play_time降序排序。查询结果是一个Map列表,可以直接用于构建UI。orderBy参数指定排序字段和顺序,DESC表示降序,这样最新的记录会排在最前面。
历史记录的删除功能
用户可能想要删除某些历史记录,我们需要提供删除功能。
可以在卡片上添加一个删除按钮,或者使用滑动删除的交互方式。滑动删除是移动应用中常见的交互模式,用户向左滑动列表项,会显示删除按钮。
使用Dismissible组件可以轻松实现滑动删除:
return Dismissible(
key: Key(item['id'].toString()),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20.w),
color: Colors.red,
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
_deleteHistory(item['id']);
},
child: Container(
// 原来的卡片内容
),
);
Dismissible组件包裹列表项,使其可以滑动删除。key参数必须是唯一的,这里使用记录的id。direction设置为endToStart,表示只能从右向左滑动。
background定义了滑动时显示的背景,这里使用红色背景和删除图标,让用户清楚地知道这是删除操作。alignment设置为centerRight,让图标显示在右侧。
onDismissed回调在滑动完成后触发,这里调用_deleteHistory方法删除记录。删除操作应该同时更新数据库和UI状态,确保数据的一致性。
统计信息的展示
在历史记录页面顶部,可以显示一些统计信息,让用户了解自己的游戏概况。
统计信息可以包括总游戏次数、总得分、平均得分、最高得分等。这些数据可以从历史记录列表中计算得出:
Widget _buildStatistics() {
final totalGames = history.length;
final totalScore = history.fold<int>(0, (sum, item) => sum + (item['score'] as int));
final avgScore = totalGames > 0 ? totalScore ~/ totalGames : 0;
final maxScore = history.isEmpty ? 0 : history.map((item) => item['score'] as int).reduce((a, b) => a > b ? a : b);
return Container(
margin: EdgeInsets.all(16.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6a11cb), Color(0xFF2575fc)],
),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('总场次', totalGames.toString()),
_buildStatItem('总得分', totalScore.toString()),
_buildStatItem('平均分', avgScore.toString()),
_buildStatItem('最高分', maxScore.toString()),
],
),
);
}
这段代码计算各种统计数据并展示在一个渐变色的卡片中。fold方法累加所有得分,map和reduce方法找出最高分。这些统计信息让用户对自己的游戏表现有一个整体的了解。
totalGames直接使用列表长度,totalScore使用fold方法遍历列表累加所有得分。avgScore是平均分,使用整除运算符~/得到整数结果。maxScore使用map将列表转换为得分列表,然后用reduce找出最大值。
搜索功能的实现
随着历史记录的增多,搜索功能变得很有必要。
可以在AppBar中添加一个搜索按钮,点击后显示搜索框。用户输入关键词,实时过滤历史记录列表。
搜索状态的管理:
bool _isSearching = false;
String _searchQuery = '';
List<Map<String, dynamic>> get filteredHistory {
if (_searchQuery.isEmpty) {
return history;
}
return history.where((item) {
final gameName = item['game'] as String;
return gameName.toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
}
_isSearching标记是否处于搜索状态,_searchQuery保存搜索关键词。filteredHistory是一个计算属性,根据搜索关键词过滤历史记录。
where方法筛选列表,只保留游戏名称包含搜索关键词的记录。toLowerCase方法将字符串转换为小写,实现不区分大小写的搜索。这种实时搜索的方式让用户可以快速找到想要的记录。
历史记录的分享功能
用户可能想要分享自己的游戏成绩,我们可以提供分享功能。
可以使用share_plus插件来实现分享。点击分享按钮时,生成一段文字,包含游戏名称、得分等信息,然后调用系统的分享功能:
void _shareRecord(Map<String, dynamic> item) {
final game = item['game'];
final score = item['score'];
final time = DateFormat('yyyy-MM-dd').format(item['time'] as DateTime);
final text = '我在游戏中心玩了$game,得分$score分!($time)';
Share.share(text);
}
这个方法构建分享文本,然后调用Share.share方法打开系统分享面板。用户可以选择分享到微信、QQ、微博等社交平台,或者复制到剪贴板。
分享功能可以增加应用的传播性,用户分享自己的成绩,可以吸引更多人来玩游戏。同时也增强了用户的成就感,让他们愿意展示自己的游戏表现。
总结
本文详细介绍了游戏历史记录页面的实现。我们从设计思路开始,确定了卡片式的展示方式和清晰的信息层次。然后实现了GameHistoryPage页面,包括列表构建、卡片设计、时间格式化等核心功能。
我们还讨论了列表性能优化、空状态处理、筛选排序、详情展示、数据持久化等扩展功能。这些功能可以让历史记录系统更加完善,为用户提供更好的体验。
历史记录功能看似简单,但其中包含了很多细节。时间的格式化、列表的优化、空状态的处理,每一个细节都影响着用户体验。通过本文的学习,你掌握了历史记录功能的实现方法,这些知识可以应用到各种需要记录用户行为的场景中。
在下一篇文章中,我们将实现成就系统,让玩家可以解锁各种成就徽章。成就系统会涉及到条件判断、状态管理、动画效果等内容,敬请期待。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)