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';classMVListPageextendsStatelessWidget{constMVListPage({super.key});页面导入了Flutter的Material组件库和GetX路由管理库。同时导入了MV播放页面,用于点击MV时进行页面跳转。
MV列表页面使用StatelessWidget,因为列表数据通常是从服务器获取的固定数据,不需要在页面内部管理复杂的状态变化。
Scaffold脚手架
页面使用Scaffold作为基础框架。
@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:constText('MV')),body:GridView.builder(AppBar设置了简洁的标题"MV",body部分使用GridView.builder来构建网格列表。GridView.builder是一个懒加载的网格组件,只会构建当前可见的项目,对于长列表来说性能更好。
GridView网格配置
GridView的核心配置在gridDelegate参数中。
padding:constEdgeInsets.all(16),gridDelegate:constSliverGridDelegateWithFixedCrossAxisCount(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:[constCenter(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:constEdgeInsets.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:constText('04:32',style:TextStyle(color:Colors.white,fontSize:10)))),],),),),时长文字使用白色,字号设置为10像素,在小标签中显示清晰又不会占用太多空间。实际项目中,这个时长值应该来自MV的真实数据。
标题和歌手信息
缩略图下方显示MV标题和歌手名称。
constSizedBox(height:8),Text('MV${index+1}',style:constTextStyle(fontSize:14),maxLines:1,overflow:TextOverflow.ellipsis),constText('歌手名称',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';classMVListPageextendsStatelessWidget{constMVListPage({super.key});@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:constText('MV')),body:GridView.builder(padding:constEdgeInsets.all(16),gridDelegate:constSliverGridDelegateWithFixedCrossAxisCount(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:[constCenter(child:Icon(Icons.play_circle_filled,size:50,color:Colors.white70)),Positioned(right:8,bottom:8,child:Container(padding:constEdgeInsets.symmetric(horizontal:6,vertical:2),decoration:BoxDecoration(color:Colors.black54,borderRadius:BorderRadius.circular(4)),child:constText('04:32',style:TextStyle(color:Colors.white,fontSize:10)))),],),),),constSizedBox(height:8),Text('MV${index+1}',style:constTextStyle(fontSize:14),maxLines:1,overflow:TextOverflow.ellipsis),constText('歌手名称',style:TextStyle(color:Colors.grey,fontSize:12)),],),),),);}}功能扩展:分类筛选
实际的MV列表通常需要分类筛选功能,下面是扩展实现。
classMVListPageextendsStatefulWidget{constMVListPage({super.key});@overrideState<MVListPage>createState()=>_MVListPageState();}class_MVListPageStateextendsState<MVListPage>{String_selectedCategory='全部';finalList<String>_categories=['全部','官方版','现场版','舞蹈版','剧情版'];要实现分类筛选,需要将页面改为StatefulWidget,添加选中分类的状态变量和分类列表。
分类标签栏
分类标签栏使用水平滚动的ListView。
Widget_buildCategoryBar(){returnSizedBox(height:50,child:ListView.builder(scrollDirection:Axis.horizontal,padding:constEdgeInsets.symmetric(horizontal:16,vertical:8),itemCount:_categories.length,itemBuilder:(context,index){finalcategory=_categories[index];finalisSelected=category==_selectedCategory;returnGestureDetector(onTap:()=>setState(()=>_selectedCategory=category),child:Container(margin:constEdgeInsets.only(right:12),padding:constEdgeInsets.symmetric(horizontal:16),alignment:Alignment.center,decoration:BoxDecoration(color:isSelected?constColor(0xFFE91E63):constColor(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{awaitFuture.delayed(constDuration(seconds:1));setState((){// 刷新数据});}Widget_buildMVGrid(){returnRefreshIndicator(onRefresh:_refreshMVList,color:constColor(0xFFE91E63),child:GridView.builder(padding:constEdgeInsets.all(16),gridDelegate:constSliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2,childAspectRatio:1.2,crossAxisSpacing:12,mainAxisSpacing:12,),itemCount:20,itemBuilder:(context,index)=>_buildMVItem(index),),);}RefreshIndicator包裹GridView实现下拉刷新,onRefresh回调返回一个Future,刷新指示器会在Future完成后自动隐藏。color参数设置刷新指示器的颜色。
加载更多
滚动到底部时加载更多数据。
finalScrollController_scrollController=ScrollController();bool _isLoadingMore=false;@overridevoidinitState(){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);awaitFuture.delayed(constDuration(seconds:1));setState(()=>_isLoadingMore=false);}通过监听ScrollController的滚动位置,当距离底部不足200像素时触发加载更多。使用_isLoadingMore标志防止重复加载。
空状态处理
当没有MV数据时显示空状态。
Widget_buildEmptyState(){returnCenter(child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[Icon(Icons.videocam_off,size:64,color:Colors.grey[600]),constSizedBox(height:16),Text('暂无MV',style:TextStyle(color:Colors.grey[600],fontSize:16),),constSizedBox(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