Flutter for OpenHarmony音乐播放器App实战:MV列表实现

MV是音乐App中非常受欢迎的内容形式,用户可以通过MV更直观地感受音乐的魅力。今天我们来实现MV列表页面,展示各种MV供用户浏览和选择播放。
功能分析
MV列表页面需要实现以下功能:
- 网格布局展示MV缩略图
- 显示MV时长标签
- 显示MV标题和歌手名称
- 点击MV进入播放页面
MV列表和歌单列表类似,但MV的缩略图通常是横向的比例,需要调整网格的宽高比来适应视频内容的展示。
对应代码文件
lib/pages/mv/mv_list_page.dart
页面基础结构
首先看页面的基础结构和导入部分。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'mv_player_page.dart';
class MVListPage extends StatelessWidget {
const MVListPage({super.key});
页面导入了Flutter的Material组件库和GetX路由管理库。同时导入了MV播放页面,用于点击MV时进行页面跳转。
MV列表页面使用StatelessWidget,因为列表数据通常是从服务器获取的固定数据,不需要在页面内部管理复杂的状态变化。
Scaffold脚手架
页面使用Scaffold作为基础框架。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MV')),
body: GridView.builder(
AppBar设置了简洁的标题"MV",body部分使用GridView.builder来构建网格列表。GridView.builder是一个懒加载的网格组件,只会构建当前可见的项目,对于长列表来说性能更好。
GridView网格配置
GridView的核心配置在gridDelegate参数中。
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 12,
mainAxisSpacing: 12
),
itemCount: 20,
padding设置网格四周的内边距为16像素,让内容不会紧贴屏幕边缘。
SliverGridDelegateWithFixedCrossAxisCount是一个固定列数的网格代理,参数含义如下:
crossAxisCount: 2表示每行显示2个MV项目childAspectRatio: 1.2表示子项的宽高比,1.2意味着宽度比高度大,适合展示横向的视频缩略图crossAxisSpacing: 12表示列之间的水平间距mainAxisSpacing: 12表示行之间的垂直间距
itemCount: 20设置列表项的总数,实际项目中这个值应该来自服务器返回的数据长度。
MV项目构建
每个MV项目使用itemBuilder回调来构建。
itemBuilder: (context, index) => GestureDetector(
onTap: () => Get.to(() => MVPlayerPage(id: index)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector包裹整个MV项目,用于处理点击事件。点击时使用GetX的Get.to方法导航到MV播放页面,并传递当前MV的索引作为ID参数。
Column组件垂直排列缩略图和文字信息,crossAxisAlignment: CrossAxisAlignment.start让子组件左对齐。
缩略图区域
缩略图区域使用Expanded占据剩余空间。
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3)
),
Expanded让缩略图区域自动填充Column中除了文字之外的所有空间。
Container的decoration设置了圆角和背景色。borderRadius: BorderRadius.circular(12)创建12像素的圆角,让缩略图看起来更柔和。
背景色使用Colors.primaries[index % Colors.primaries.length]从Material的主色列表中循环取色,withOpacity(0.3)设置30%的透明度,让颜色不会太刺眼。这种方式在没有真实图片时可以产生丰富的视觉效果。
Stack叠加布局
缩略图内部使用Stack实现叠加效果。
child: Stack(
children: [
const Center(
child: Icon(Icons.play_circle_filled, size: 50, color: Colors.white70)
),
Stack允许子组件叠加在一起,后面的子组件会覆盖前面的。
第一个子组件是居中的播放图标,使用Center组件让图标在缩略图中央显示。Icons.play_circle_filled是一个实心的播放按钮图标,大小设置为50像素。Colors.white70是70%透明度的白色,不会太抢眼但又能清晰可见。
时长标签定位
时长标签使用Positioned精确定位在右下角。
Positioned(
right: 8,
bottom: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4)
),
Positioned组件只能在Stack内部使用,用于精确控制子组件的位置。right: 8表示距离右边8像素,bottom: 8表示距离底部8像素,这样时长标签就会显示在缩略图的右下角。
时长标签的Container设置了水平6像素、垂直2像素的内边距,让文字不会紧贴边框。背景色使用Colors.black54,即54%透明度的黑色,既能遮挡背景又不会太突兀。圆角设置为4像素,让标签看起来更精致。
时长文字
时长标签内显示视频时长。
child: const Text(
'04:32',
style: TextStyle(color: Colors.white, fontSize: 10)
)
)
),
],
),
),
),
时长文字使用白色,字号设置为10像素,在小标签中显示清晰又不会占用太多空间。实际项目中,这个时长值应该来自MV的真实数据。
标题和歌手信息
缩略图下方显示MV标题和歌手名称。
const SizedBox(height: 8),
Text(
'MV ${index + 1}',
style: const TextStyle(fontSize: 14),
maxLines: 1,
overflow: TextOverflow.ellipsis
),
const Text(
'歌手名称',
style: TextStyle(color: Colors.grey, fontSize: 12)
),
],
),
),
),
);
}
}
SizedBox(height: 8)在缩略图和标题之间添加8像素的间距。
MV标题使用14像素的字号,maxLines: 1限制只显示一行,overflow: TextOverflow.ellipsis在文本溢出时显示省略号,避免标题过长破坏布局。
歌手名称使用灰色和12像素的较小字号,作为次要信息展示,与标题形成视觉层次。
完整代码
下面是MV列表页面的完整代码。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'mv_player_page.dart';
class MVListPage extends StatelessWidget {
const MVListPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MV')),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 12,
mainAxisSpacing: 12
),
itemCount: 20,
itemBuilder: (context, index) => GestureDetector(
onTap: () => Get.to(() => MVPlayerPage(id: index)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3)
),
child: Stack(
children: [
const Center(
child: Icon(Icons.play_circle_filled, size: 50, color: Colors.white70)
),
Positioned(
right: 8,
bottom: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4)
),
child: const Text(
'04:32',
style: TextStyle(color: Colors.white, fontSize: 10)
)
)
),
],
),
),
),
const SizedBox(height: 8),
Text(
'MV ${index + 1}',
style: const TextStyle(fontSize: 14),
maxLines: 1,
overflow: TextOverflow.ellipsis
),
const Text(
'歌手名称',
style: TextStyle(color: Colors.grey, fontSize: 12)
),
],
),
),
),
);
}
}
功能扩展:分类筛选
实际的MV列表通常需要分类筛选功能,下面是扩展实现。
class MVListPage extends StatefulWidget {
const MVListPage({super.key});
State<MVListPage> createState() => _MVListPageState();
}
class _MVListPageState extends State<MVListPage> {
String _selectedCategory = '全部';
final List<String> _categories = ['全部', '官方版', '现场版', '舞蹈版', '剧情版'];
要实现分类筛选,需要将页面改为StatefulWidget,添加选中分类的状态变量和分类列表。
分类标签栏
分类标签栏使用水平滚动的ListView。
Widget _buildCategoryBar() {
return SizedBox(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: _categories.length,
itemBuilder: (context, index) {
final category = _categories[index];
final isSelected = category == _selectedCategory;
return GestureDetector(
onTap: () => setState(() => _selectedCategory = category),
child: Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFE91E63) : const Color(0xFF2A2A2A),
borderRadius: BorderRadius.circular(20),
),
child: Text(
category,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey,
fontSize: 14,
),
),
),
);
},
),
);
}
scrollDirection: Axis.horizontal让ListView水平滚动。每个分类标签使用圆角容器,选中状态使用主题色背景,未选中使用深灰色背景。点击标签时调用setState更新选中状态。
下拉刷新
可以添加下拉刷新功能来更新MV列表。
Future<void> _refreshMVList() async {
await Future.delayed(const Duration(seconds: 1));
setState(() {
// 刷新数据
});
}
Widget _buildMVGrid() {
return RefreshIndicator(
onRefresh: _refreshMVList,
color: const Color(0xFFE91E63),
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: 20,
itemBuilder: (context, index) => _buildMVItem(index),
),
);
}
RefreshIndicator包裹GridView实现下拉刷新,onRefresh回调返回一个Future,刷新指示器会在Future完成后自动隐藏。color参数设置刷新指示器的颜色。
加载更多
滚动到底部时加载更多数据。
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
Future<void> _loadMore() async {
if (_isLoadingMore) return;
setState(() => _isLoadingMore = true);
await Future.delayed(const Duration(seconds: 1));
setState(() => _isLoadingMore = false);
}
通过监听ScrollController的滚动位置,当距离底部不足200像素时触发加载更多。使用_isLoadingMore标志防止重复加载。
空状态处理
当没有MV数据时显示空状态。
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.videocam_off, size: 64, color: Colors.grey[600]),
const SizedBox(height: 16),
Text(
'暂无MV',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
const SizedBox(height: 8),
Text(
'换个分类试试吧',
style: TextStyle(color: Colors.grey[700], fontSize: 14),
),
],
),
);
}
空状态页面显示一个图标和提示文字,引导用户进行下一步操作。
技术要点总结
MV列表页面虽然代码量不大,但涉及到几个重要的Flutter布局技术:
GridView.builder适合展示大量同类型数据,通过gridDelegate可以灵活配置网格的列数、间距和子项比例。
Stack和Positioned组合可以实现复杂的叠加布局,常用于在图片上添加标签、按钮等元素。
文本溢出处理是列表页面的常见需求,maxLines和overflow属性可以优雅地处理长文本。
GetX的路由管理让页面跳转变得简单,通过构造函数传参可以方便地在页面间传递数据。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)