Flutter for OpenHarmony 游戏中心App实战:排行榜主页实现
本文介绍了游戏排行榜的设计与实现方法。排行榜采用领奖台形式突出前三名玩家,使用emoji奖牌、渐变色背景和层次化布局增强视觉效果。页面分为三大模块:前三名展示区采用金银铜牌和高度差设计,中间为排行榜分类入口,底部显示个人排名。通过颜色层次、合理间距和模块化组件设计,打造出既能激发竞争欲望又清晰易用的排行榜界面。代码实现上采用StatelessWidget和无状态组件,便于维护和扩展。

排行榜是游戏应用中非常重要的社交功能,它通过展示玩家的排名和得分,激发玩家的竞争欲望和成就感。一个设计良好的排行榜不仅能展示数据,还能营造竞争氛围,让玩家有动力提升自己的排名。本文将详细介绍排行榜主页的实现,包括前三名展示、排行榜分类、个人排名等功能。
排行榜的设计理念
排行榜的设计要突出前几名玩家,让他们获得应有的荣誉感。传统的排行榜设计中,前三名通常会有特殊的视觉效果,比如金银铜牌、领奖台、特殊颜色等。这种设计让排名的价值可视化,激励玩家争取更好的名次。
我们的排行榜主页采用多层次的布局。顶部是前三名的领奖台展示,使用渐变色背景和emoji奖牌,营造颁奖典礼的氛围。中间是排行榜分类入口,使用网格布局展示全球排行、好友排行、周排行、月排行四个分类。底部是个人排名卡片,让玩家快速了解自己的位置。
颜色的使用要有层次感。前三名的领奖台使用渐变色背景,非常醒目。排行榜分类使用深蓝色卡片,与页面主题协调。个人排名使用紫色边框,突出显示。这种层次分明的设计让页面既丰富又不杂乱。
页面组件的定义
LeaderboardPage是一个无状态组件,负责展示排行榜主页的各个模块。
class LeaderboardPage extends StatelessWidget {
const LeaderboardPage({super.key});
Widget build(BuildContext context) {
使用StatelessWidget让组件保持简单。排行榜数据的管理可以通过状态管理方案来处理,页面本身只负责展示。这种设计符合单一职责原则,让代码更容易理解和维护。
const构造函数表示这个Widget是编译时常量,可以提高性能。super.key传递给父类,用于Widget的标识。虽然这些都是基础知识,但正确使用它们可以让应用运行得更加流畅。
在实际应用中,排行榜数据应该从服务器获取。这里我们先使用模拟数据来展示页面效果,后续可以很容易地替换为真实数据。排行榜通常需要实时更新,所以数据获取应该是异步的。
页面框架的构建
页面使用Scaffold作为基本框架,body部分使用SingleChildScrollView支持滚动。
return Scaffold(
appBar: AppBar(
title: const Text('排行榜', style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: const Color(0xFF16213e),
),
body: SingleChildScrollView(
child: Column(
children: [
_buildTopThree(),
_buildRankCategories(),
_buildMyRank(),
],
),
),
);
}
Scaffold提供了标准的Material Design页面结构。AppBar显示页面标题"排行榜",title使用TextStyle设置粗体,让标题更加醒目。backgroundColor设置为深蓝色,与应用的整体主题保持一致。
body使用SingleChildScrollView包裹Column,让页面内容可以滚动。当排行榜内容很多时,用户可以向下滚动查看所有内容。Column垂直排列三个主要模块:前三名展示、排行榜分类、个人排名。
这种模块化的布局设计让页面结构清晰,每个模块负责展示一类信息。如果需要添加新的模块,只需要在Column的children数组中添加新的Widget即可。模块之间的间距由各个模块自己的margin控制,保持了布局的灵活性。
前三名展示的容器
前三名展示使用一个渐变色背景的容器,营造颁奖典礼的氛围。
Widget _buildTopThree() {
return Container(
margin: EdgeInsets.all(16.w),
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6a11cb), Color(0xFF2575fc)],
),
borderRadius: BorderRadius.circular(16.r),
),
Container是前三名展示的容器,margin设置为EdgeInsets.all(16.w),在容器四周添加外边距。padding设置为20.w,让内容有足够的呼吸空间。
decoration使用BoxDecoration定义装饰样式。gradient使用LinearGradient创建渐变色背景,从紫色渐变到蓝色。渐变色比纯色更有层次感,视觉效果更加丰富,给人一种高级、隆重的感觉,符合排行榜的主题。
borderRadius设置为16.r,创建圆角效果。圆角让容器看起来更加柔和,符合现代UI设计的趋势。使用flutter_screenutil的适配单位,确保在不同设备上保持一致的视觉效果。
这个渐变色容器是前三名展示的舞台,让前三名玩家获得应有的荣誉感。渐变色的使用让这个区域在视觉上非常突出,用户打开页面时会立即注意到这里。
领奖台的布局
前三名使用领奖台的形式展示,第一名在中间最高,第二名在左边,第三名在右边。
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_buildPodium('🥈', '玩家2', '9520', 2),
_buildPodium('🥇', '玩家1', '12580', 1),
_buildPodium('🥉', '玩家3', '8760', 3),
],
),
);
}
Row水平排列三个领奖台。mainAxisAlignment设置为spaceAround,让三个领奖台均匀分布。crossAxisAlignment设置为end,让领奖台底部对齐,这样不同高度的领奖台就形成了高低错落的效果。
三个领奖台的顺序是第二名、第一名、第三名,这样第一名就在中间。每个领奖台使用_buildPodium方法构建,传入奖牌emoji、玩家名称、得分和排名。
使用emoji作为奖牌是一个巧妙的设计。🥇🥈🥉这三个emoji分别代表金银铜牌,视觉效果非常直观,不需要准备图片资源。emoji在所有平台上都有统一的显示效果,而且尺寸可以随意调整。
这种领奖台的布局是排行榜的经典设计,用户一眼就能看出谁是第一名。第一名在中间最高的位置,获得最多的关注,这种视觉层次让排名的价值非常明显。
领奖台的实现
_buildPodium方法创建一个领奖台,包含奖牌、玩家名称、得分和台阶。
Widget _buildPodium(String medal, String name, String score, int rank) {
final height = rank == 1 ? 120.h : (rank == 2 ? 100.h : 80.h);
return Column(
children: [
Text(medal, style: TextStyle(fontSize: 40.sp)),
SizedBox(height: 8.h),
方法接收四个参数:奖牌emoji、玩家名称、得分和排名。height根据排名计算台阶的高度,第一名120.h,第二名100.h,第三名80.h。这种高度差异形成了领奖台的视觉效果。
Column垂直排列奖牌、玩家信息和台阶。第一个子元素是奖牌emoji,fontSize设置为40.sp,这是一个很大的尺寸,让奖牌非常醒目。奖牌是领奖台最重要的视觉元素,应该最先被用户注意到。
SizedBox添加了8.h的垂直间距,将奖牌和玩家名称分开。适当的间距让布局更加清晰,不会显得拥挤。这个间距比较小,因为奖牌和玩家信息是紧密相关的。
玩家信息的显示
奖牌下方显示玩家名称和得分。
Text(name, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold, color: Colors.white)),
SizedBox(height: 4.h),
Text(score, style: TextStyle(fontSize: 16.sp, color: Colors.amber)),
SizedBox(height: 8.h),
玩家名称使用14.sp的字号和粗体,color设置为白色。在渐变色背景上,白色有很好的对比度,让名称清晰可见。粗体让名称更加醒目,用户可以快速识别前三名玩家。
SizedBox添加了4.h的垂直间距,将名称和得分分开。这个间距很小,因为名称和得分是同一个玩家的信息,应该紧密排列。
得分使用16.sp的字号,比名称稍大一些,因为得分是排名的依据,是更重要的信息。color设置为Colors.amber,这是一个金黄色,给人富贵、成功的感觉,非常适合表示得分。
SizedBox添加了8.h的垂直间距,将玩家信息和台阶分开。这个间距比较大,因为玩家信息和台阶是两个不同的视觉元素,需要明确的分隔。
台阶的绘制
台阶使用Container绘制,高度根据排名不同而不同。
Container(
width: 60.w,
height: height,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.vertical(top: Radius.circular(8.r)),
),
child: Center(
child: Text('#$rank', style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold, color: Colors.white)),
),
),
],
);
}
Container是台阶的容器,width设置为60.w,height使用之前计算的高度。decoration的color使用半透明的白色,让台阶有一种玻璃质感,不会完全遮挡背景的渐变色。
borderRadius使用BorderRadius.vertical只设置顶部圆角,让台阶看起来像一个真实的台阶。top参数设置为Radius.circular(8.r),创建圆角效果。
Center组件确保排名文字在台阶中居中显示。Text显示排名,使用#符号前缀,fontSize设置为24.sp,fontWeight设置为bold,color设置为白色。这个大号的排名数字是台阶的视觉焦点,让用户清楚地知道这是第几名。
整个领奖台的设计使用了奖牌、名称、得分、台阶四个视觉元素,层次分明,信息传达清晰。不同高度的台阶形成了视觉上的高低错落,让排名的差异一目了然。
排行榜分类的数据定义
排行榜分类提供多种排行榜入口,让用户可以查看不同维度的排名。
Widget _buildRankCategories() {
final categories = [
{'title': '全球排行', 'icon': Icons.public, 'page': const GlobalRankPage()},
{'title': '好友排行', 'icon': Icons.people, 'page': const FriendsRankPage()},
{'title': '周排行', 'icon': Icons.calendar_today, 'page': const WeeklyRankPage()},
{'title': '月排行', 'icon': Icons.calendar_month, 'page': const MonthlyRankPage()},
];
categories是一个列表,每个元素是一个Map,包含分类标题、图标和目标页面。这四个分类代表了排行榜的主要类型。
全球排行展示所有玩家的排名,让用户了解自己在全球范围内的位置。好友排行只展示好友的排名,让竞争更加亲密和有趣。周排行和月排行展示特定时间段的排名,让玩家有机会在新的周期重新开始。
每个分类使用Material Icons中的图标,图标的选择要符合分类的含义。public图标代表全球,people图标代表好友,calendar图标代表时间周期。这些图标让分类的含义一目了然,用户不需要仔细阅读文字就能理解。
page字段保存目标页面的Widget,点击分类时会导航到这个页面。使用const构造函数创建页面Widget,可以提高性能。
分类网格的构建
排行榜分类使用GridView展示,形成2x2的网格布局。
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
),
itemCount: categories.length,
itemBuilder: (context, index) {
final cat = categories[index];
GridView.builder是构建网格的高效方式。shrinkWrap设置为true,让GridView只占据实际需要的高度,而不是填充整个父Widget。这在ScrollView中嵌套GridView时非常重要。
physics设置为NeverScrollableScrollPhysics,禁用GridView自己的滚动。因为外层已经有SingleChildScrollView,不需要GridView再滚动,否则会产生滚动冲突。
padding设置了水平内边距16.w,让网格不会紧贴屏幕边缘。gridDelegate定义了网格的布局规则。crossAxisCount设置为2,表示每行2个网格。childAspectRatio设置为1.5,表示宽高比是1.5:1,让网格呈横向矩形。
crossAxisSpacing和mainAxisSpacing分别设置了网格之间的水平和垂直间距。这些间距让网格之间有明确的分隔,不会显得拥挤。
分类卡片的实现
每个分类显示为一个卡片,包含图标和标题。
return GestureDetector(
onTap: () => Get.to(() => cat['page'] as Widget),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF16213e),
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(cat['icon'] as IconData, size: 40.sp, color: Colors.purpleAccent),
SizedBox(height: 8.h),
Text(cat['title'] as String, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
],
),
),
);
},
);
}
GestureDetector包裹Container,处理点击事件。onTap回调使用Get.to导航到目标页面。Get是GetX库提供的导航方法,比Navigator.push更加简洁。
Container是卡片的容器,decoration的color设置为深蓝色,与其他卡片的颜色一致。borderRadius创建圆角效果,让卡片看起来更加柔和。
Column垂直排列图标和标题。mainAxisAlignment设置为center,让内容在垂直方向上居中。这样图标和标题就会出现在卡片的正中央,视觉上非常平衡。
Icon显示分类图标,size设置为40.sp,这是一个比较大的尺寸,让图标清晰可见。color设置为Colors.purpleAccent,这是一个鲜艳的紫色,在深蓝色背景上有很好的对比度。
SizedBox添加了8.h的垂直间距,将图标和标题分开。Text显示分类标题,fontSize设置为16.sp,fontWeight设置为bold。标题简洁明了,让用户清楚地知道点击后会看到什么内容。
个人排名卡片的容器
个人排名卡片展示用户自己的排名和得分,使用紫色边框突出显示。
Widget _buildMyRank() {
return Container(
margin: EdgeInsets.all(16.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: const Color(0xFF16213e),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.purpleAccent, width: 2),
),
Container是个人排名卡片的容器,margin设置为EdgeInsets.all(16.w),在容器四周添加外边距。padding设置为16.w,让内容不会紧贴边缘。
decoration定义了容器的装饰样式。color设置为深蓝色,与其他卡片的颜色一致。borderRadius创建圆角效果。
border是关键的视觉区分。使用Border.all创建边框,color设置为Colors.purpleAccent,width设置为2。这个紫色边框让个人排名卡片非常醒目,用户可以快速找到自己的排名。
紫色边框的使用传达了"这是你的信息"的含义。在排行榜中,用户最关心的就是自己的排名,所以个人排名应该有特殊的视觉标记,让用户一眼就能看到。
个人排名的内容布局
卡片内容使用Row水平排列,从左到右依次是排名圆圈、排名信息和箭头图标。
child: Row(
children: [
Container(
width: 50.w,
height: 50.w,
decoration: BoxDecoration(
color: Colors.purpleAccent.withOpacity(0.3),
borderRadius: BorderRadius.circular(25.r),
),
child: Center(child: Text('#42', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold))),
),
SizedBox(width: 16.w),
Row水平排列子Widget。第一个子元素是排名圆圈,使用Container创建。width和height都设置为50.w,形成一个正方形。
decoration的color使用半透明的紫色,与边框的颜色协调。borderRadius设置为25.r,正好是宽度的一半,创建了一个完美的圆形。
Center组件确保排名文字在圆圈中居中显示。Text显示排名,使用#符号前缀,fontSize设置为18.sp,fontWeight设置为bold。这个圆形的排名标记非常醒目,让用户立即知道自己的排名。
SizedBox添加了16.w的水平间距,将排名圆圈和排名信息分开。适当的间距让布局更加清晰,不会显得拥挤。
排名信息的展示
排名信息包括标题和得分,使用Column垂直排列。Expanded让这部分内容占据剩余的水平空间。
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('我的排名', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 4.h),
Text('得分: 5680', style: TextStyle(fontSize: 14.sp, color: Colors.amber)),
],
),
),
const Icon(Icons.arrow_forward_ios, size: 16),
],
),
);
}
}
Expanded让Column占据Row中剩余的水平空间。crossAxisAlignment设置为start,让文本左对齐。
标题"我的排名"使用16.sp的字号和粗体,让它醒目突出。这个标题明确地告诉用户这是他们自己的排名信息,不是其他玩家的。
SizedBox添加了4.h的垂直间距,将标题和得分分开。得分使用字符串插值显示数值,fontSize设置为14.sp,color设置为Colors.amber。金黄色的得分与前三名展示中的得分颜色一致,形成了统一的视觉语言。
Row的最后一个子元素是箭头图标,使用Material Icons中的arrow_forward_ios。这个图标暗示卡片是可点击的,点击后会进入详细的排名页面。size设置为16,这是一个比较小的尺寸,表明这是次要的视觉元素。
整个个人排名卡片的设计使用了紫色边框、圆形排名标记、金黄色得分、箭头图标等多个视觉元素,让用户的排名信息非常突出,同时暗示了可以点击查看更多详情。
排行榜数据的获取
在实际应用中,排行榜数据应该从服务器获取,可以定义一个排行榜服务类:
class LeaderboardService {
static Future<List<Map<String, dynamic>>> getTopPlayers(int limit) async {
final response = await http.get(
Uri.parse('https://api.example.com/leaderboard/top?limit=$limit'),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return List<Map<String, dynamic>>.from(data['players']);
} else {
throw Exception('Failed to load leaderboard');
}
}
static Future<Map<String, dynamic>> getMyRank(String userId) async {
final response = await http.get(
Uri.parse('https://api.example.com/leaderboard/user/$userId'),
);
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Failed to load user rank');
}
}
}
getTopPlayers方法获取前N名玩家的数据,limit参数指定获取的数量。使用http包发送GET请求,解析JSON响应,返回玩家列表。
getMyRank方法获取指定用户的排名信息,userId参数是用户的唯一标识。返回的数据包含用户的排名、得分等信息。
这些方法都是异步的,使用async和await关键字。网络请求通常比较耗时,使用异步可以避免阻塞UI线程,保持应用的流畅性。
错误处理也很重要。如果请求失败,抛出异常,调用方可以捕获异常并显示错误提示。在实际应用中,还应该处理网络超时、服务器错误等各种异常情况。
排行榜的状态管理
使用状态管理方案(如GetX)来管理排行榜数据:
class LeaderboardController extends GetxController {
final RxList<Map<String, dynamic>> topPlayers = <Map<String, dynamic>>[].obs;
final Rx<Map<String, dynamic>> myRank = <String, dynamic>{}.obs;
final RxBool isLoading = true.obs;
void onInit() {
super.onInit();
loadLeaderboard();
}
Future<void> loadLeaderboard() async {
try {
isLoading.value = true;
final top = await LeaderboardService.getTopPlayers(3);
final my = await LeaderboardService.getMyRank(UserService.currentUserId);
topPlayers.value = top;
myRank.value = my;
} catch (e) {
Get.snackbar('错误', '加载排行榜失败: $e');
} finally {
isLoading.value = false;
}
}
}
这个控制器管理排行榜的所有数据,topPlayers保存前三名玩家,myRank保存个人排名,isLoading标记是否正在加载。使用响应式变量让数据变化时自动更新UI。
onInit方法在控制器初始化时调用loadLeaderboard加载数据。loadLeaderboard方法并发获取前三名和个人排名,然后更新响应式变量。
使用try-catch捕获异常,如果加载失败显示错误提示。finally块确保无论成功还是失败,都会设置isLoading为false,隐藏加载指示器。
这种响应式的数据管理让排行榜页面始终显示最新的数据。当数据加载完成时,页面会自动更新,不需要手动刷新。
加载状态的处理
在页面中使用控制器,根据加载状态显示不同的内容:
class LeaderboardPage extends StatelessWidget {
const LeaderboardPage({super.key});
Widget build(BuildContext context) {
final controller = Get.put(LeaderboardController());
return Scaffold(
appBar: AppBar(
title: const Text('排行榜'),
backgroundColor: const Color(0xFF16213e),
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
// 显示排行榜内容...
);
}),
);
}
}
使用Get.put创建并注册控制器。Obx是GetX提供的响应式Widget,当控制器中的响应式变量改变时,Obx会自动重新构建。
如果正在加载,显示CircularProgressIndicator。加载完成后,显示排行榜内容。这种加载状态的处理让用户体验更好,不会看到空白页面或错误数据。
前三名和个人排名的数据从控制器获取,使用controller.topPlayers和controller.myRank。这些响应式变量的值改变时,使用它们的Widget会自动更新。
下拉刷新功能
排行榜页面可以添加下拉刷新功能,让用户可以手动刷新排行榜:
RefreshIndicator(
onRefresh: () async {
final controller = Get.find<LeaderboardController>();
await controller.loadLeaderboard();
},
child: SingleChildScrollView(
// 页面内容...
),
)
RefreshIndicator包裹SingleChildScrollView,提供下拉刷新功能。用户下拉页面时,会显示一个加载指示器,同时调用onRefresh回调。
onRefresh回调中,我们获取LeaderboardController实例,然后调用loadLeaderboard方法重新加载排行榜数据。这个方法返回Future,RefreshIndicator会等待Future完成后才隐藏加载指示器。
下拉刷新让用户可以主动更新排行榜,确保看到的是最新的排名。排行榜数据经常变化,特别是在游戏高峰期,用户可能需要频繁刷新来查看最新的排名。
排行榜的缓存策略
为了提升性能和减少服务器压力,可以实现排行榜的缓存策略:
class LeaderboardCache {
static final Map<String, CacheEntry> _cache = {};
static const Duration cacheExpiry = Duration(minutes: 5);
static Future<List<Map<String, dynamic>>> getTopPlayers(int limit) async {
final key = 'top_$limit';
final entry = _cache[key];
if (entry != null && DateTime.now().difference(entry.timestamp) < cacheExpiry) {
return entry.data;
}
final data = await LeaderboardService.getTopPlayers(limit);
_cache[key] = CacheEntry(data, DateTime.now());
return data;
}
}
class CacheEntry {
final List<Map<String, dynamic>> data;
final DateTime timestamp;
CacheEntry(this.data, this.timestamp);
}
这个缓存类使用Map保存排行榜数据,键是查询参数,值是CacheEntry对象。CacheEntry包含数据和时间戳。
getTopPlayers方法先检查缓存,如果缓存存在且未过期,直接返回缓存数据。如果缓存不存在或已过期,从服务器获取数据,然后更新缓存。
缓存过期时间设置为5分钟,这是一个平衡性能和实时性的合理值。如果过期时间太短,缓存的作用不大;如果太长,用户可能看到过时的数据。
这种缓存策略可以显著减少网络请求,提升应用的响应速度。特别是当用户频繁切换页面时,缓存可以让排行榜立即显示,而不需要等待网络请求。
总结
本文详细介绍了排行榜主页的实现。我们从设计理念开始,确定了领奖台式的前三名展示和网格式的分类布局。然后实现了LeaderboardPage页面,包括前三名展示、排行榜分类、个人排名等核心功能。
我们使用了渐变色背景、emoji奖牌、不同高度的台阶等视觉元素,营造了颁奖典礼的氛围。紫色边框突出显示个人排名,让用户快速找到自己的位置。网格布局的分类入口清晰明了,用户可以方便地查看不同维度的排行榜。
我们还讨论了排行榜数据的获取、状态管理、加载状态处理、下拉刷新、缓存策略等扩展功能。这些功能让排行榜系统更加完善,为用户提供流畅、实时的排名体验。
排行榜是激发用户竞争欲望的重要工具,好的排行榜设计可以显著提升用户的参与度和活跃度。通过本文的学习,你掌握了排行榜主页的实现方法,这些知识可以应用到各种需要排名功能的应用中。
在下一篇文章中,我们将实现全球排行榜功能,展示完整的排行榜列表和详细的排名信息。全球排行榜会涉及到列表分页、排名变化、玩家详情等内容,敬请期待。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)