在这里插入图片描述

在上一篇文章中,我们完成了项目的初始化和架构设计,搭建好了应用的基本框架。现在是时候开始实现具体的功能了。游戏大厅是整个应用的核心页面,用户打开应用后首先看到的就是这个页面。一个设计精美、功能完善的游戏大厅,能够给用户留下良好的第一印象,也能有效地引导用户探索和体验各种游戏。

游戏大厅的设计思路

在开始编码之前,我们需要先明确游戏大厅要实现哪些功能。作为应用的首页,游戏大厅需要承担多个职责:展示所有可玩的游戏、提供游戏分类导航、支持游戏搜索、推荐精选游戏等。同时,页面的视觉设计也要足够吸引人,让用户产生继续探索的欲望。

我们的设计方案是这样的:页面顶部是一个欢迎Banner,使用渐变色背景和大号emoji图标,营造出欢快的氛围。Banner下方是横向滚动的分类标签,用户可以快速筛选不同类型的游戏。主体部分是游戏网格,以卡片形式展示每个游戏的图标、名称、分类和评分。页面右上角有搜索和精选按钮,方便用户快速找到想玩的游戏。

这种布局方式在游戏类应用中很常见,用户接受度高,操作也很直观。通过合理的间距和圆角设计,整个页面看起来既整洁又有层次感。

页面结构的搭建

让我们从GameHallPage的基本结构开始。这是一个有状态组件,因为它需要管理游戏列表数据:

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

  
  State<GameHallPage> createState() => _GameHallPageState();
}

这里我们选择使用StatefulWidget而不是StatelessWidget,主要是考虑到未来可能需要添加数据刷新、筛选等功能。虽然目前游戏列表是静态的,但使用StatefulWidget可以为后续的功能扩展预留空间。StatefulWidget由两部分组成:Widget本身和对应的State类,State类负责管理可变的状态数据。

在State类中,我们首先定义游戏列表数据:

class _GameHallPageState extends State<GameHallPage> {
  final List<GameModel> games = [
    GameModel(id: '1', name: '拼图游戏', icon: '🧩', category: '益智', 
              playCount: 1234, rating: 4.5, description: '经典拼图挑战'),
    GameModel(id: '2', name: '记忆翻牌', icon: '🎴', category: '记忆', 
              playCount: 2341, rating: 4.7, description: '考验记忆力'),
    GameModel(id: '3', name: '知识问答', icon: '❓', category: '知识', 
              playCount: 3456, rating: 4.6, description: '趣味问答'),

这里我们创建了8个游戏的数据。每个游戏都使用GameModel来表示,包含了id、名称、图标、分类、游玩次数、评分和描述等信息。使用emoji作为图标是一个巧妙的设计,不仅节省了图片资源,而且在不同平台上都能保持一致的显示效果。emoji是Unicode字符,不需要额外的图片文件,渲染速度快,也不占用存储空间。

游戏的分类涵盖了益智、记忆、反应、知识、文字、冒险等多种类型,这样可以满足不同用户的喜好。playCount和rating这两个数据虽然目前是硬编码的,但在实际应用中可以从服务器获取,实现动态更新。每个游戏的description字段提供了简短的介绍,让用户能快速了解游戏的玩法。

继续添加剩余的游戏数据:

    GameModel(id: '4', name: '数字消除', icon: '🔢', category: '益智', 
              playCount: 4567, rating: 4.8, description: '数字配对消除'),
    GameModel(id: '5', name: '单词接龙', icon: '📝', category: '文字', 
              playCount: 1890, rating: 4.4, description: '单词挑战'),
    GameModel(id: '6', name: '颜色匹配', icon: '🎨', category: '反应', 
              playCount: 2789, rating: 4.5, description: '快速匹配颜色'),
    GameModel(id: '7', name: '反应测试', icon: '⚡', category: '反应', 
              playCount: 3210, rating: 4.6, description: '测试反应速度'),
    GameModel(id: '8', name: '迷宫探险', icon: '🗺️', category: '冒险', 
              playCount: 2567, rating: 4.7, description: '走出迷宫'),
  ];

这8个游戏涵盖了不同的游戏类型和难度等级。从简单的拼图游戏到需要快速反应的颜色匹配,从考验记忆力的翻牌游戏到需要策略的迷宫探险,每个游戏都有其独特的玩法和乐趣。这样的游戏组合可以满足不同年龄段和不同喜好的用户需求。final关键字表示这个列表在创建后不会被重新赋值,但列表内的元素可以修改。

AppBar的设计与实现

页面的AppBar不仅要显示标题,还要提供搜索和精选功能的入口:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('游戏大厅', 
                       style: TextStyle(fontWeight: FontWeight.bold)),
      backgroundColor: const Color(0xFF16213e),
      actions: [
        IconButton(
          icon: const Icon(Icons.search),
          onPressed: () => Get.to(() => const GameSearchPage()),
        ),

AppBar的背景色设置为深蓝色(#16213e),与整体主题保持一致。标题使用粗体字,让页面名称更加醒目。actions数组中添加了两个IconButton,分别对应搜索和精选功能。点击这些按钮会使用GetX的路由功能跳转到对应的页面。GetX的路由导航非常简洁,Get.to方法接收一个返回Widget的函数,自动处理页面跳转的动画和栈管理。

继续添加精选按钮和body部分:

        IconButton(
          icon: const Icon(Icons.star),
          onPressed: () => Get.to(() => const FeaturedGamesPage()),
        ),
      ],
    ),
    body: SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _buildBanner(),
          _buildCategories(),
          _buildSectionTitle('热门游戏'),
          _buildGameGrid(),
        ],
      ),
    ),
  );
}

body部分使用SingleChildScrollView包裹,这样当内容超出屏幕高度时可以滚动查看。Column组件按垂直方向排列各个子组件,crossAxisAlignment设置为start表示子组件左对齐。我们将页面内容拆分成了四个独立的方法:_buildBanner构建欢迎横幅、_buildCategories构建分类导航、_buildSectionTitle构建章节标题、_buildGameGrid构建游戏网格。这种将UI拆分成小方法的做法让代码更加清晰,每个方法只负责构建一个特定的UI部分,职责明确。

欢迎Banner的实现

Banner是页面的视觉焦点,需要设计得足够吸引人:

Widget _buildBanner() {
  return Container(
    height: 180.h,
    margin: EdgeInsets.all(16.w),
    decoration: BoxDecoration(
      gradient: const LinearGradient(
        colors: [Color(0xFF6a11cb), Color(0xFF2575fc)],
      ),
      borderRadius: BorderRadius.circular(16.r),
    ),

Banner使用Container组件,高度设置为180个设计稿单位。margin设置为16个单位的外边距,让Banner与屏幕边缘保持适当的距离。decoration使用BoxDecoration来定义视觉效果,gradient属性创建了一个从紫色到蓝色的线性渐变。这种渐变色背景比纯色更有视觉冲击力,也更符合现代UI设计的趋势。borderRadius设置圆角为16个单位,让Banner看起来更加柔和。

Banner的内容部分:

    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('🎮', style: TextStyle(fontSize: 48.sp)),
          SizedBox(height: 8.h),
          Text('欢迎来到游戏中心', 
               style: TextStyle(fontSize: 24.sp, 
                              fontWeight: FontWeight.bold, 
                              color: Colors.white)),
          SizedBox(height: 4.h),
          Text('30+精品小游戏等你来玩', 
               style: TextStyle(fontSize: 14.sp, 
                              color: Colors.white70)),
        ],
      ),
    ),
  );
}

Banner内容使用Center和Column组件居中排列。最上面是一个大号的游戏手柄emoji,字体大小为48个单位,非常醒目。下面是欢迎文字,使用24号粗体白色字体。最下面是副标题,使用14号半透明白色字体。SizedBox用于在元素之间添加间距,让布局更加舒适。这种层次分明的文字排列,配合渐变色背景,营造出欢快活泼的氛围。

分类导航的实现

分类导航让用户可以快速筛选不同类型的游戏:

Widget _buildCategories() {
  final categories = ['全部', '益智', '记忆', '反应', '知识', '文字', '冒险'];
  return SizedBox(
    height: 50.h,
    child: ListView.builder(
      scrollDirection: Axis.horizontal,
      padding: EdgeInsets.symmetric(horizontal: 16.w),
      itemCount: categories.length,

首先定义了一个分类列表,包含"全部"和各种游戏类型。使用SizedBox限制高度为50个单位,ListView.builder设置为横向滚动。scrollDirection设置为Axis.horizontal表示水平滚动,padding设置左右各16个单位的内边距。ListView.builder是一个高效的列表组件,只会渲染可见区域的内容,即使分类很多也不会影响性能。

分类标签的构建:

      itemBuilder: (context, index) {
        return GestureDetector(
          onTap: () => Get.to(() => GameCategoryPage(category: categories[index])),
          child: Container(
            margin: EdgeInsets.only(right: 12.w),
            padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
            decoration: BoxDecoration(
              color: index == 0 ? Colors.purpleAccent : const Color(0xFF16213e),
              borderRadius: BorderRadius.circular(25.r),
            ),
            child: Center(child: Text(categories[index])),
          ),
        );
      },
    ),
  );
}

每个分类标签使用GestureDetector包裹,点击时跳转到对应的分类页面。Container设置右边距12个单位,让标签之间有适当的间隔。padding设置水平20个单位、垂直10个单位的内边距,让文字与边框保持距离。第一个标签(全部)使用紫色背景作为强调色,其他标签使用深蓝色背景。borderRadius设置为25个单位,创建出胶囊形状的标签。这种设计既美观又实用,用户可以一眼看出当前选中的分类。

章节标题的实现

章节标题用于分隔不同的内容区域:

Widget _buildSectionTitle(String title) {
  return Padding(
    padding: EdgeInsets.fromLTRB(16.w, 20.h, 16.w, 12.h),
    child: Text(title, 
                style: TextStyle(fontSize: 20.sp, 
                               fontWeight: FontWeight.bold)),
  );
}

这是一个简单但重要的组件。Padding设置上边距20个单位、下边距12个单位、左右各16个单位,让标题与上下内容保持适当的距离。Text使用20号粗体字,让标题足够醒目。这个方法接收一个title参数,可以在不同地方复用,比如"热门游戏"、"最新游戏"等。虽然代码很简单,但这种将可复用的UI封装成方法的做法,可以大大提高代码的可维护性。

游戏网格的实现

游戏网格是页面的核心部分,展示所有可玩的游戏:

Widget _buildGameGrid() {
  return GridView.builder(
    shrinkWrap: true,
    physics: const NeverScrollableScrollPhysics(),
    padding: EdgeInsets.symmetric(horizontal: 16.w),
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 2,
      childAspectRatio: 0.85,
      crossAxisSpacing: 12.w,
      mainAxisSpacing: 12.h,
    ),

GridView.builder用于构建网格布局。shrinkWrap设置为true表示GridView的高度由内容决定,而不是占满整个屏幕。physics设置为NeverScrollableScrollPhysics禁用GridView自身的滚动,因为外层已经有SingleChildScrollView了。gridDelegate定义网格的布局规则:crossAxisCount设置为2表示每行显示2个游戏,childAspectRatio设置为0.85表示宽高比,crossAxisSpacing和mainAxisSpacing分别设置横向和纵向的间距。

游戏卡片的构建:

    itemCount: games.length,
    itemBuilder: (context, index) {
      final game = games[index];
      return GestureDetector(
        onTap: () => _navigateToGame(game),
        child: Container(
          decoration: BoxDecoration(
            color: const Color(0xFF16213e),
            borderRadius: BorderRadius.circular(16.r),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(game.icon, style: TextStyle(fontSize: 48.sp)),
              SizedBox(height: 8.h),

itemCount设置为游戏列表的长度,itemBuilder负责构建每个游戏卡片。首先获取当前索引对应的游戏对象,然后用GestureDetector包裹整个卡片,点击时调用_navigateToGame方法跳转到游戏详情页。Container使用深蓝色背景和16个单位的圆角。Column垂直排列卡片内容,mainAxisAlignment设置为center让内容居中。最上面显示游戏的emoji图标,字体大小为48个单位。

游戏信息的显示:

              Text(game.name, 
                   style: TextStyle(fontSize: 16.sp, 
                                  fontWeight: FontWeight.bold)),
              SizedBox(height: 4.h),
              Text(game.category, 
                   style: TextStyle(fontSize: 12.sp, 
                                  color: Colors.white60)),
              SizedBox(height: 8.h),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.star, color: Colors.amber, size: 16),
                  SizedBox(width: 4.w),
                  Text('${game.rating}', style: TextStyle(fontSize: 12.sp)),
                ],
              ),
            ],
          ),
        ),
      );
    },
  );
}

游戏名称使用16号粗体字显示,分类使用12号半透明白色字显示。最下面是评分信息,使用Row横向排列星星图标和评分数字。星星图标使用琥珀色,大小为16像素。评分数字使用12号字体。这种卡片式的设计简洁明了,用户可以快速浏览所有游戏,点击感兴趣的游戏查看详情或直接开始游戏。

页面跳转的处理

最后是处理游戏卡片点击事件的方法:

void _navigateToGame(GameModel game) {
  Get.to(() => GameDetailPage(game: game));
}

这个方法接收一个GameModel对象作为参数,使用GetX的路由功能跳转到游戏详情页。Get.to方法会自动处理页面跳转的动画效果,默认是从右向左的滑动动画。GameDetailPage的构造函数接收game参数,这样详情页就能显示对应游戏的信息。这种通过构造函数传递数据的方式简单直接,不需要使用全局状态或其他复杂的数据传递机制。

屏幕适配的应用

在整个页面的实现中,我们大量使用了flutter_screenutil提供的适配单位。所有的尺寸都使用.w、.h、.sp等扩展方法,确保在不同屏幕尺寸的设备上都能保持一致的视觉效果。

比如Banner的高度180.h,会根据实际屏幕高度自动缩放。游戏图标的字体大小48.sp,会根据屏幕尺寸自动调整。这种适配方式的好处是开发时不需要考虑具体的设备尺寸,只需要按照设计稿的标注来写代码,运行时会自动适配。

需要注意的是,并不是所有尺寸都需要适配。比如分割线的宽度通常固定为1像素,图标的大小如果使用了矢量图标也不需要适配。适配的原则是:影响布局和视觉效果的尺寸需要适配,而那些需要保持固定大小的元素则不需要适配。

性能优化的考虑

虽然游戏大厅页面的内容不算多,但我们在实现时还是考虑了性能优化。首先是使用ListView.builder和GridView.builder而不是ListView和GridView,这样可以实现懒加载,只渲染可见区域的内容。

其次是将UI拆分成小方法,每个方法只负责构建一个特定的部分。这样当某个部分需要更新时,Flutter可以精确地重建对应的Widget,而不是重建整个页面。

再次是使用const构造函数创建不会改变的Widget,比如const Text、const Icon等。const Widget在编译时就确定了,运行时不需要重新创建,可以提高性能。

最后是合理使用final关键字。对于不会改变的数据,使用final修饰可以让Dart编译器进行优化。虽然这些优化在小型应用中可能感觉不明显,但养成良好的编码习惯,在大型项目中会有显著的效果。

用户体验的细节

在实现游戏大厅时,我们注重了很多用户体验的细节。比如Banner使用渐变色背景和大号emoji,让页面一打开就能吸引用户的注意力。分类标签使用横向滚动,即使分类很多也不会显得拥挤。

游戏卡片使用圆角和阴影效果,让每个游戏都像一个独立的实体,点击时有明确的反馈。评分信息使用星星图标和琥珀色,让用户一眼就能看出游戏的质量。

页面的配色也经过精心设计。深色背景配合紫色和蓝色的强调色,既符合游戏应用的调性,也不会让用户感到视觉疲劳。所有的文字都使用了合适的字号和颜色,确保在深色背景上清晰可读。

这些细节虽然看起来不起眼,但正是这些细节的积累,才能打造出优秀的用户体验。在实际开发中,我们应该时刻站在用户的角度思考,每一个设计决策都要考虑用户的感受。

代码组织的思考

游戏大厅页面的代码组织体现了良好的编程实践。我们将页面拆分成多个小方法,每个方法只负责一个特定的功能。这种做法的好处是代码结构清晰,易于理解和维护。

如果将来需要修改Banner的样式,只需要修改_buildBanner方法。如果需要调整游戏卡片的布局,只需要修改_buildGameGrid方法。这种模块化的设计让代码的可维护性大大提高。

同时,我们也注意到了代码的复用性。比如_buildSectionTitle方法接收一个title参数,可以在不同地方使用。如果将来需要添加更多的章节,只需要调用这个方法并传入不同的标题即可。

在命名方面,我们使用了清晰的方法名,比如_buildBanner、_buildCategories等。这些名字一看就知道方法的作用,不需要额外的注释。私有方法使用下划线开头,这是Dart的命名约定。

扩展功能的预留

虽然目前游戏大厅的功能已经比较完善,但我们在实现时预留了扩展的空间。比如游戏列表目前是硬编码的,但可以很容易地改成从服务器获取。只需要添加一个网络请求方法,获取数据后调用setState更新games列表即可。

分类导航目前点击后跳转到分类页面,但也可以改成在当前页面筛选。只需要添加一个selectedCategory状态变量,根据这个变量过滤games列表,然后重新构建GridView即可。

搜索功能目前跳转到独立的搜索页面,但也可以在游戏大厅页面添加一个搜索框。用户输入关键词后,实时过滤游戏列表,提供更流畅的搜索体验。

这些扩展功能的实现都不需要大幅修改现有代码,这得益于我们良好的代码组织和架构设计。在实际项目中,需求总是在不断变化的,预留扩展空间可以让我们更从容地应对变化。

总结

本文详细介绍了游戏大厅主页的实现。我们从页面结构开始,逐步实现了AppBar、欢迎Banner、分类导航、章节标题和游戏网格等各个部分。每个部分都有详细的代码和讲解,帮助你理解实现的原理。

游戏大厅作为应用的首页,承担着展示游戏、引导用户的重要职责。我们通过精心的设计和实现,打造出了一个既美观又实用的页面。渐变色Banner营造出欢快的氛围,横向滚动的分类导航提供了便捷的筛选功能,网格布局的游戏卡片让用户可以快速浏览所有游戏。

在实现过程中,我们注重了代码的组织和性能优化。将UI拆分成小方法,使用懒加载的列表组件,合理使用const和final关键字,这些都是良好的编程实践。同时,我们也考虑了用户体验的细节,从配色到布局,从动画到反馈,每个细节都经过精心设计。

接下来的文章中,我们会继续实现游戏详情页、各个小游戏、排行榜等功能。每个功能都会有完整的代码和详细的讲解,帮助你掌握Flutter开发的各种技巧。通过这个完整的项目,你不仅能学到Flutter的开发知识,还能了解如何设计和实现一个完整的应用。


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

Logo

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

更多推荐