Flutter for OpenHarmony 剧本杀组队App实战02:首页Banner与快捷入口实现
本文介绍了剧本杀App首页的实现方案,采用垂直滚动布局设计,包含Banner、快捷入口、热门剧本和附近店铺四大模块。Banner采用紫色渐变背景设计,添加阴影效果增强视觉吸引力;快捷入口采用网格布局提供核心功能导航;热门剧本实现横向滚动列表展示精选内容;附近店铺使用卡片式布局显示位置信息。文章详细讲解了Flutter中的布局技巧、组件封装和数据管理方法,包括SingleChildScrollVie
引言
首页是用户进入App后最常访问的页面,承担着内容展示和功能导航的重要职责。一个设计良好的首页应该能够快速传达App的核心价值,提供便捷的功能入口,展示精选内容吸引用户,并引导用户进行下一步操作。本篇将实现首页的核心组件,包括渐变背景Banner设计、快捷入口网格布局、热门剧本横向滚动列表以及附近店铺卡片展示。通过本篇的学习,你将掌握Flutter中常用的布局技巧和组件使用方法,为后续开发打下坚实基础。
功能设计
首页采用垂直滚动布局,从上到下依次为AppBar标题栏(包含搜索入口)、渐变背景的推荐Banner、4-5个常用功能的快捷入口、横向滚动的热门剧本卡片以及垂直排列的附近店铺列表。设计规范方面,主题色采用0xFF6B4EFF紫色,渐变色从0xFF6B4EFF过渡到0xFF9D4EDD,卡片圆角为12px,大组件圆角为16px,间距分为12px紧凑、16px标准和20px宽松三档。
核心代码实现
第一部分:导入依赖与类定义
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../script/script_list_page.dart';
import '../script/script_detail_page.dart';
import '../store/store_list_page.dart';
import '../search/search_page.dart';
import '../ranking/ranking_page.dart';
import '../activity/activity_page.dart';
import '../team/quick_match_page.dart';
class HomePage extends StatelessWidget {
HomePage({super.key});
上述代码首先导入了Flutter的Material组件库和GetX状态管理库。Material库提供了丰富的UI组件,是Flutter开发的基础。GetX是一个轻量级的状态管理和路由管理库,使用Get.to()方法可以方便地进行页面跳转。接下来导入了各个功能页面的引用,包括剧本列表、剧本详情、店铺列表、搜索页面、排行榜、活动中心和快速匹配页面。HomePage类继承自StatelessWidget,因为首页不需要管理内部状态,所有数据都是静态展示的。
第二部分:热门剧本数据定义
final List<Map<String, dynamic>> _hotScripts = [
{
'id': '1',
'name': '年轮',
'type': '情感本',
'players': '6人',
'duration': '4-5h',
'rating': 9.2,
'price': 88,
},
{
'id': '2',
'name': '古木吟',
'type': '恐怖本',
'players': '7人',
'duration': '5-6h',
'rating': 9.5,
'price': 98,
},
这里定义了热门剧本的数据列表,使用List<Map<String, dynamic>>类型存储。每个剧本包含id唯一标识、name剧本名称、type剧本类型(情感本、恐怖本、机制本等)、players适合人数、duration游戏时长、rating评分和price价格等字段。在实际项目中,这些数据应该从后端API获取,这里使用静态数据进行演示。Map的dynamic类型允许存储不同类型的值,如字符串、数字等,提供了灵活性。
{
'id': '3',
'name': '你好',
'type': '情感本',
'players': '5人',
'duration': '3-4h',
'rating': 9.0,
'price': 78,
},
{
'id': '4',
'name': '云使',
'type': '机制本',
'players': '8人',
'duration': '6-7h',
'rating': 9.3,
'price': 108,
},
];
继续定义更多剧本数据,包括"你好"和"云使"两个剧本。"你好"是一个5人情感本,时长较短适合新手;"云使"是8人机制本,时长较长适合资深玩家。通过不同类型和人数的剧本,可以满足不同用户群体的需求。数据结构的设计要考虑到后续的展示需求,确保包含所有需要显示的字段。
第三部分:附近店铺数据定义
final List<Map<String, dynamic>> _nearbyStores = [
{
'id': '1',
'name': '迷雾剧本杀',
'distance': '1.2km',
'rating': 4.8,
'address': '朝阳区三里屯',
},
{
'id': '2',
'name': '探案馆',
'distance': '2.5km',
'rating': 4.6,
'address': '海淀区中关村',
},
];
附近店铺数据同样使用List
第四部分:build方法与页面结构
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('剧本杀组队'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => Get.to(() => SearchPage()),
),
],
),
build方法是Widget的核心方法,返回页面的UI结构。Scaffold是Material Design的基础页面结构,提供了AppBar、body、floatingActionButton等标准布局槽位。AppBar设置了页面标题"剧本杀组队",并在右侧actions区域添加了搜索图标按钮。点击搜索按钮时,使用Get.to()方法跳转到SearchPage搜索页面。const关键字用于编译时常量,可以提升性能。
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildBanner(),
_buildQuickEntry(),
_buildSectionTitle('热门剧本', () => Get.to(() => ScriptListPage())),
_buildHotScripts(),
_buildSectionTitle('附近店铺', () => Get.to(() => StoreListPage())),
_buildNearbyStores(),
const SizedBox(height: 20),
],
),
),
);
}
body部分使用SingleChildScrollView包裹Column实现垂直滚动。SingleChildScrollView适合内容不多但需要滚动的场景,它会一次性渲染所有子组件。Column的crossAxisAlignment设置为start,使子组件左对齐。页面内容从上到下依次是Banner、快捷入口、热门剧本标题和列表、附近店铺标题和列表,最后添加20像素的底部间距避免内容贴边。每个区块都封装成独立的方法,提高代码可读性和复用性。
第五部分:Banner区域实现
Widget _buildBanner() {
return Container(
height: 160,
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF6B4EFF), Color(0xFF9D4EDD)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0xFF6B4EFF).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
Banner区域使用Container组件,设置固定高度160像素和12像素的外边距。decoration属性使用BoxDecoration进行装饰,其中gradient属性创建从左上到右下的线性渐变,颜色从主题紫色过渡到浅紫色。borderRadius设置12像素的圆角,boxShadow添加阴影效果增强立体感。阴影颜色使用主题色的30%透明度,模糊半径10像素,向下偏移4像素,营造出卡片悬浮的视觉效果。
child: Stack(
children: [
Positioned(
right: -20,
bottom: -20,
child: Icon(
Icons.theater_comedy,
size: 120,
color: Colors.white.withOpacity(0.1),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
Banner内部使用Stack组件实现层叠布局。底层使用Positioned定位一个半透明的剧院图标作为背景装饰,right和bottom设为负值使图标部分超出容器边界,营造出延伸感。上层是实际内容,使用Padding添加20像素内边距,Column垂直排列内容。crossAxisAlignment设为start左对齐,mainAxisAlignment设为center垂直居中,确保内容在Banner中央显示。
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'今日推荐',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
const SizedBox(height: 12),
const Text(
'年轮',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Banner内容区域首先显示一个"今日推荐"标签,使用半透明白色背景和小圆角。接着是剧本名称"年轮",使用24像素的白色粗体字,作为Banner的主标题突出显示。SizedBox用于添加12像素的垂直间距,这是Flutter中常用的间距添加方式,比使用margin更加灵活。文字颜色统一使用白色系,与渐变背景形成对比,确保可读性。
const SizedBox(height: 4),
const Text(
'情感沉浸式体验 · 6人本 · 4-5小时',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'立即组队',
style: TextStyle(
color: Color(0xFF6B4EFF),
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
),
);
}
副标题使用Colors.white70(70%透明度的白色)显示剧本的类型、人数和时长信息,与主标题形成层次对比。最后是"立即组队"按钮,使用白色背景和20像素圆角打造胶囊形状,文字使用主题紫色与背景呼应。这种反色设计使按钮在渐变背景上非常醒目,引导用户点击。整个Banner设计遵循了视觉层次原则:标签→标题→副标题→行动按钮。
第六部分:快捷入口实现
Widget _buildQuickEntry() {
final entries = [
{'icon': Icons.menu_book, 'name': '剧本库', 'page': ScriptListPage()},
{'icon': Icons.store, 'name': '找店铺', 'page': StoreListPage()},
{'icon': Icons.groups, 'name': '快速组队', 'page': QuickMatchPage()},
{'icon': Icons.event, 'name': '活动', 'page': ActivityPage()},
{'icon': Icons.leaderboard, 'name': '排行榜', 'page': RankingPage()},
];
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: entries.map((e) => _buildEntryItem(e)).toList(),
),
);
}
快捷入口区域定义了5个功能入口的数据,每个入口包含icon图标、name名称和page目标页面。使用Row横向排列,mainAxisAlignment设为spaceEvenly使各入口均匀分布。entries.map()方法遍历数据列表,为每个入口调用_buildEntryItem方法生成Widget,最后toList()转换为List供Row使用。这种数据驱动的方式使得添加或修改入口非常方便,只需修改entries数组即可。
Widget _buildEntryItem(Map<String, dynamic> entry) {
return GestureDetector(
onTap: () {
if (entry['page'] != null) {
Get.to(() => entry['page'] as Widget);
}
},
child: Column(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: const Color(0xFF6B4EFF).withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(
entry['icon'] as IconData,
color: const Color(0xFF6B4EFF),
size: 28,
),
),
const SizedBox(height: 8),
Text(entry['name'] as String, style: const TextStyle(fontSize: 12)),
],
),
);
}
单个入口项使用GestureDetector包裹实现点击事件,点击时跳转到对应页面。Column垂直排列图标和文字,图标使用56x56像素的Container,背景色为主题色的10%透明度,圆角16像素,图标大小28像素。文字使用12像素字号,与图标间距8像素。这种设计使入口区域整洁美观,图标的浅色背景与主题色呼应,形成统一的视觉风格。
第七部分:区块标题实现
Widget _buildSectionTitle(String title, VoidCallback onMore) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
GestureDetector(
onTap: onMore,
child: Text('更多 >', style: TextStyle(color: Colors.grey[600])),
),
],
),
);
}
区块标题是一个可复用的组件,接收title标题文字和onMore点击回调两个参数。使用Row横向排列标题和"更多"按钮,spaceBetween使两者分布在两端。标题使用18像素粗体字,"更多"按钮使用灰色文字。VoidCallback是Flutter中无参数无返回值的函数类型,适合用于点击回调。这种封装方式使得在不同区块复用标题组件时只需传入不同参数即可。
第八部分:热门剧本列表实现
Widget _buildHotScripts() {
return SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _hotScripts.length,
itemBuilder: (context, index) {
final script = _hotScripts[index];
return _buildScriptCard(script);
},
),
);
}
热门剧本使用横向滚动的ListView.builder实现。SizedBox设置固定高度180像素,这是横向ListView必须的,否则会报错。scrollDirection设为Axis.horizontal启用横向滚动,padding添加水平内边距。ListView.builder是懒加载列表,只渲染可见区域的项目,性能优于直接使用ListView。itemBuilder回调为每个索引构建对应的剧本卡片Widget。
Widget _buildScriptCard(Map<String, dynamic> script) {
return GestureDetector(
onTap: () => Get.to(() => ScriptDetailPage(scriptId: script['id'])),
child: Container(
width: 140,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
),
],
),
剧本卡片使用GestureDetector包裹,点击时跳转到剧本详情页并传递scriptId参数。卡片宽度固定140像素,水平间距4像素。使用白色背景、12像素圆角和轻微阴影,营造出卡片悬浮效果。阴影使用5%透明度的黑色,模糊半径10像素,效果柔和不突兀。这种卡片设计是移动端常见的内容展示方式。
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 90,
decoration: BoxDecoration(
color: Colors.purple[100],
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
),
child: Center(
child: Icon(Icons.auto_stories, size: 40, color: Colors.purple[400]),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
script['name'],
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
卡片内容分为上下两部分:上部是90像素高的封面区域,使用浅紫色背景和书本图标作为占位;下部是信息区域,包含剧本名称、类型人数和评分。封面区域只有顶部有圆角,与卡片整体圆角衔接。剧本名称使用14像素粗体,maxLines和overflow处理文字过长的情况,超出部分显示省略号。这种处理方式确保了卡片布局的稳定性。
const SizedBox(height: 4),
Text(
'${script['type']} · ${script['players']}',
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.star, size: 14, color: Colors.amber),
Text(' ${script['rating']}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
],
),
],
),
),
],
),
),
);
}
类型和人数信息使用灰色小字显示,用中点符号分隔。评分使用Row横向排列星星图标和分数,星星使用琥珀色(amber)突出显示。字符串插值使用${}语法,可以方便地将变量嵌入字符串中。整个卡片的信息层次清晰:封面图→名称→类型人数→评分,用户可以快速获取剧本的关键信息。
第九部分:附近店铺列表实现
Widget _buildNearbyStores() {
return Column(
children: _nearbyStores.map((store) => _buildStoreCard(store)).toList(),
);
}
Widget _buildStoreCard(Map<String, dynamic> store) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
附近店铺使用Column垂直排列,因为店铺数量较少不需要滚动。店铺卡片使用水平12像素、垂直4像素的外边距,内边距12像素,白色背景和12像素圆角。与剧本卡片不同,店铺卡片采用横向布局,左侧是店铺图片,右侧是店铺信息,这种布局更适合展示详细信息。
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.purple[100],
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.store, color: Colors.purple[400], size: 30),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(store['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
const SizedBox(height: 4),
Text(store['address'], style: TextStyle(color: Colors.grey[600], fontSize: 12)),
店铺卡片使用Row横向布局,左侧是60x60像素的店铺图片占位,使用浅紫色背景和店铺图标。右侧使用Expanded占据剩余空间,内部Column垂直排列店铺名称、地址和评分距离信息。店铺名称使用15像素粗体,地址使用12像素灰色字。Expanded组件确保右侧内容自适应宽度,避免溢出问题。
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.star, size: 14, color: Colors.amber),
Text(' ${store['rating']}', style: const TextStyle(fontSize: 12)),
const Spacer(),
Text(store['distance'], style: TextStyle(color: Colors.grey[600], fontSize: 12)),
],
),
],
),
),
],
),
);
}
}
最后一行显示评分和距离,使用Row横向排列,Spacer组件将评分和距离分隔到两端。评分同样使用星星图标和数字组合,距离显示在右侧。这种布局使用户可以快速比较不同店铺的评分和距离,做出选择。整个首页的实现遵循了组件化、数据驱动的原则,代码结构清晰,易于维护和扩展。
技术要点总结
本篇实现了首页的核心组件,涉及的主要技术点包括:SingleChildScrollView实现页面滚动、LinearGradient创建渐变背景、Stack实现层叠布局、ListView.builder实现横向滚动列表、GestureDetector处理点击事件、GetX进行页面路由跳转。通过合理的组件封装和数据驱动设计,代码具有良好的可读性和可维护性。
小结
本篇详细讲解了首页Banner与快捷入口的实现过程,每段代码都配有详细的文字说明,帮助读者理解Flutter布局和组件的使用方法。下一篇将实现组队大厅列表功能,敬请期待!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)