在这里插入图片描述

游戏历史记录是游戏中心应用中非常实用的功能,它让玩家可以回顾自己的游戏轨迹,查看每次游戏的得分和时间。这不仅能帮助玩家追踪自己的进步,还能激发他们挑战更高分数的动力。在本文中,我们将实现一个完整的游戏历史记录页面,包括历史数据的展示、时间格式化、列表优化等功能。

历史记录的设计思路

游戏历史记录页面需要展示玩家过往的游戏数据,每条记录应该包含游戏名称、得分、游戏时间等关键信息。设计时我们要考虑几个要点:数据的展示要清晰易读,让玩家一眼就能看到重要信息;列表要支持滚动,因为历史记录可能会很多;每条记录的样式要统一,保持视觉的一致性。

我们采用卡片式的设计,每个游戏记录显示为一个独立的卡片。卡片左侧显示游戏图标,中间显示游戏名称、得分和时间,整体布局简洁明了。使用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

Logo

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

更多推荐