news 2026/6/10 19:43:59

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter for OpenHarmony音乐播放器App实战18: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';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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 10:50:39

SQL 中的 WITH ... AS ...

SQL 中的 WITH ... AS ... 是一种非常强大且常用的语法结构&#xff0c;用于定义公用表表达式&#xff08;Common Table Expression&#xff0c;简称 CTE&#xff09;。它可以帮助你将复杂的查询拆解为更清晰、可读性更强的逻辑块。 一、基本语法 WITH cte_name AS (-- 子查询…

作者头像 李华
网站建设 2026/6/2 15:27:03

【程序员必学】GPT模型架构解析:预训练与微调技术详解(建议收藏)

本文详细解析了GPT模型的预训练与微调机制。预训练阶段通过自监督单向语言模型学习通用语义表示&#xff1b;微调阶段利用标注数据适配特定任务&#xff0c;但需解决灾难性遗忘问题。文章提出的混合损失函数方法&#xff0c;通过调节预训练与微调损失的权重平衡&#xff0c;有效…

作者头像 李华
网站建设 2026/5/28 14:18:39

探索AI提示工程国际化与本地化,提示工程架构师的独特视角

探索AI提示工程国际化与本地化:提示工程架构师的独特视角 一、引言:AI出海的“隐形门槛” 1.1 痛点引入:你可能遇到过的“翻译陷阱” 假设你是一家中国 SaaS 公司的产品经理,负责将AI客服系统推向东南亚市场。前期一切顺利:翻译了界面文案,适配了当地支付方式,甚至调…

作者头像 李华
网站建设 2026/5/28 16:57:05

农业供应链AI决策系统:架构师如何实现产销协同?

农业供应链AI决策系统&#xff1a;架构师如何用技术破解“产销错位”的千年难题&#xff1f; 一、开场&#xff1a;你见过凌晨3点的蔬菜批发市场吗&#xff1f; 去年冬天&#xff0c;我在山东寿光的蔬菜批发市场蹲了一周。凌晨2点&#xff0c;菜农王大爷的三轮车刚停稳&#xf…

作者头像 李华
网站建设 2026/5/28 14:19:32

Linux中get命令怎么用?

关于Linux&#xff0c;大家应该都知道它拥有许多的命令&#xff0c;这些命令可以帮助我们完成各种各样的操作。今天这篇文章主要跟大家聊聊get命令&#xff0c;那么Linux中get命令怎么用?以下是具体的内容介绍。get命令用于在Linux中获取和设置系统变量的值。它通过操纵底层的…

作者头像 李华
网站建设 2026/6/10 16:51:45

基于微信小程序的家校沟通管理系统的设计与实现

文章目录 详细视频演示项目介绍技术介绍功能介绍核心代码系统效果图源码获取 详细视频演示 文章底部名片&#xff0c;获取项目的完整演示视频&#xff0c;免费解答技术疑问 项目介绍 基于微信小程序的家校沟通管理系统采用前后端分离架构&#xff0c;前端基于微信小程序开发&…

作者头像 李华