Flutter for OpenHarmony音乐播放器App实战12:歌手详情实现
简介Tab展示歌手的详细信息。'歌手简介',),'这是歌手的详细简介,包含歌手的成长经历、音乐风格、代表作品等信息。'歌手从小就展现出对音乐的热爱,经过多年的努力终于在乐坛崭露头角。'其音乐风格独特,深受广大歌迷喜爱。',),_buildInfoItem('出生日期', '1990年1月1日'),_buildInfoItem('出生地', '北京'),_buildInfoItem('代表作品',
歌手详情页是音乐播放器中展示歌手信息的核心页面。用户可以在这里查看歌手的热门歌曲、专辑、MV以及个人简介。本篇文章将详细介绍如何使用NestedScrollView和SliverPersistentHeader实现一个功能完善的歌手详情页面。
页面基础结构
歌手详情页需要接收歌手ID参数,用于加载对应的歌手数据。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ArtistDetailPage extends StatefulWidget {
final int id;
const ArtistDetailPage({super.key, required this.id});
State<ArtistDetailPage> createState() => _ArtistDetailPageState();
}
页面通过构造函数接收歌手ID,这个ID会在路由跳转时传入。使用StatefulWidget是因为页面有TabController和关注状态需要管理。
状态变量与初始化
页面需要管理TabController和关注状态。
class _ArtistDetailPageState extends State<ArtistDetailPage> with SingleTickerProviderStateMixin {
late TabController _tabController;
bool _isFollowed = false;
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
void dispose() {
_tabController.dispose();
super.dispose();
}
混入SingleTickerProviderStateMixin是使用TabController的必要条件。TabController的length设置为4,对应热门、专辑、MV、简介四个Tab。
NestedScrollView结构
使用NestedScrollView实现头部折叠效果和Tab内容联动。
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
_buildSliverAppBar(),
_buildTabBarHeader(),
],
body: TabBarView(
controller: _tabController,
children: [
_buildHotSongsList(),
_buildAlbumsGrid(),
_buildMVsGrid(),
_buildBioSection(),
],
),
),
);
}
NestedScrollView的headerSliverBuilder返回头部的Sliver组件列表,body是TabBarView。这种结构让头部可以折叠,同时Tab内容可以独立滚动。
SliverAppBar头部设计
SliverAppBar实现可折叠的歌手信息头部。
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 300,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.primaries[widget.id % Colors.primaries.length],
Colors.black,
],
),
),
expandedHeight设置展开高度为300,pinned为true让AppBar收起后固定在顶部。背景使用渐变色,颜色根据歌手ID变化。
歌手信息展示
头部中间展示歌手头像、名称和统计信息。
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 40),
CircleAvatar(
radius: 50,
backgroundColor: Colors.white24,
child: const Icon(Icons.person, size: 50, color: Colors.white70),
),
const SizedBox(height: 16),
Text(
'歌手 ${widget.id + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'粉丝: 100万 · 歌曲: 200首',
style: TextStyle(color: Colors.white70),
),
使用CircleAvatar显示圆形头像,下方是歌手名称和统计信息。SafeArea确保内容不会被状态栏遮挡。
关注按钮
关注按钮根据状态改变样式和文字。
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => setState(() => _isFollowed = !_isFollowed),
style: ElevatedButton.styleFrom(
backgroundColor: _isFollowed ? Colors.grey : const Color(0xFFE91E63),
),
child: Text(_isFollowed ? '已关注' : '+ 关注'),
),
],
),
),
),
),
);
}
已关注时按钮变灰显示"已关注",未关注时显示主题色和"+ 关注"。点击切换关注状态。
TabBar固定头部
使用SliverPersistentHeader让TabBar在滚动时固定。
Widget _buildTabBarHeader() {
return SliverPersistentHeader(
pinned: true,
delegate: _TabBarDelegate(
TabBar(
controller: _tabController,
labelColor: const Color(0xFFE91E63),
unselectedLabelColor: Colors.grey,
indicatorColor: const Color(0xFFE91E63),
tabs: const [
Tab(text: '热门'),
Tab(text: '专辑'),
Tab(text: 'MV'),
Tab(text: '简介'),
],
),
),
);
}
SliverPersistentHeader配合自定义的Delegate实现TabBar固定效果。pinned为true确保TabBar始终可见。
TabBar代理类
自定义SliverPersistentHeaderDelegate实现TabBar的固定效果。
class _TabBarDelegate extends SliverPersistentHeaderDelegate {
final TabBar tabBar;
_TabBarDelegate(this.tabBar);
double get minExtent => tabBar.preferredSize.height;
double get maxExtent => tabBar.preferredSize.height;
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: const Color(0xFF121212),
child: tabBar,
);
}
bool shouldRebuild(covariant _TabBarDelegate oldDelegate) => false;
}
minExtent和maxExtent都设置为TabBar的高度,让TabBar保持固定大小。build方法返回带背景色的TabBar。
热门歌曲列表
热门歌曲Tab展示歌手的热门歌曲。
Widget _buildHotSongsList() {
return ListView.builder(
itemCount: 50,
itemBuilder: (context, i) => ListTile(
leading: Text(
'${i + 1}',
style: TextStyle(
color: i < 3 ? const Color(0xFFE91E63) : Colors.grey,
fontWeight: i < 3 ? FontWeight.bold : FontWeight.normal,
),
),
title: Text('热门歌曲 ${i + 1}'),
subtitle: Text('专辑 ${i % 5 + 1}'),
trailing: const Icon(
Icons.play_circle_outline,
color: Color(0xFFE91E63),
),
onTap: () => _playSong(i),
),
);
}
前三名歌曲的序号使用主题色和粗体突出显示。每首歌曲显示名称、所属专辑和播放按钮。
专辑网格
专辑Tab使用网格布局展示歌手的专辑。
Widget _buildAlbumsGrid() {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.85,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: 10,
itemBuilder: (context, i) => GestureDetector(
onTap: () => _openAlbum(i),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.primaries[i % Colors.primaries.length].withOpacity(0.3),
),
child: const Center(
child: Icon(Icons.album, size: 50, color: Colors.white70),
),
),
),
const SizedBox(height: 8),
Text(
'专辑 ${i + 1}',
style: const TextStyle(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
每行显示2个专辑,childAspectRatio设置为0.85让卡片略高于宽。封面使用不同颜色区分,底部显示专辑名称。
MV网格
MV Tab使用横向比例的网格展示。
Widget _buildMVsGrid() {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: 8,
itemBuilder: (context, i) => GestureDetector(
onTap: () => _playMV(i),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.primaries[i % Colors.primaries.length].withOpacity(0.3),
),
child: Stack(
children: [
const Center(
child: Icon(Icons.play_circle_filled, size: 40, color: Colors.white70),
),
Positioned(
bottom: 8,
left: 8,
right: 8,
child: Text(
'MV ${i + 1}',
style: const TextStyle(color: Colors.white, fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}
MV卡片使用1.5的宽高比,更符合视频的展示比例。中间显示播放图标,底部显示MV名称。
歌手简介
简介Tab展示歌手的详细信息。
Widget _buildBioSection() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'歌手简介',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
const Text(
'这是歌手的详细简介,包含歌手的成长经历、音乐风格、代表作品等信息。'
'歌手从小就展现出对音乐的热爱,经过多年的努力终于在乐坛崭露头角。'
'其音乐风格独特,深受广大歌迷喜爱。',
style: TextStyle(color: Colors.grey, height: 1.8),
),
const SizedBox(height: 24),
_buildInfoItem('出生日期', '1990年1月1日'),
_buildInfoItem('出生地', '北京'),
_buildInfoItem('代表作品', '热门歌曲1、热门歌曲2'),
_buildInfoItem('音乐风格', '流行、摇滚'),
],
),
);
}
简介使用SingleChildScrollView包裹,内容较多时可以滚动。height设置为1.8增加行间距,提高可读性。
信息项组件
简介中的信息项使用统一的样式。
Widget _buildInfoItem(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
label,
style: const TextStyle(color: Colors.grey),
),
),
Expanded(
child: Text(value),
),
],
),
);
}
标签固定宽度80,值使用Expanded自适应剩余空间。这种布局让信息对齐整齐,易于阅读。
播放歌曲方法
点击歌曲后跳转到播放器页面。
void _playSong(int index) {
Get.toNamed('/player', arguments: {
'songId': index,
'artistId': widget.id,
});
}
使用GetX的命名路由跳转,传递歌曲ID和歌手ID作为参数。
打开专辑方法
点击专辑后跳转到专辑详情页。
void _openAlbum(int index) {
Get.toNamed('/album/$index');
}
使用动态路由传递专辑ID。
播放MV方法
点击MV后跳转到MV播放页面。
void _playMV(int index) {
Get.toNamed('/mv/$index');
}
MV播放页面会接收MV ID并加载对应的视频。
关注状态持久化
实际项目中需要将关注状态保存到服务器。
void _toggleFollow() async {
final newState = !_isFollowed;
setState(() => _isFollowed = newState);
try {
// 调用API更新关注状态
await _apiService.followArtist(widget.id, newState);
} catch (e) {
// 失败时恢复状态
setState(() => _isFollowed = !newState);
Get.snackbar('错误', '操作失败,请重试');
}
}
先乐观更新UI,然后调用API。如果API调用失败,恢复原来的状态并提示用户。
下拉刷新
可以添加下拉刷新功能更新歌手数据。
Future<void> _refreshData() async {
// 模拟网络请求
await Future.delayed(const Duration(seconds: 1));
// 更新数据
setState(() {});
}
在NestedScrollView外层包裹RefreshIndicator即可实现下拉刷新。
总结
歌手详情页的实现综合运用了Flutter的多个高级组件:NestedScrollView实现头部折叠和Tab内容联动,SliverAppBar实现可折叠的头部,SliverPersistentHeader实现TabBar固定效果。通过合理的组件组合和状态管理,为用户提供了流畅的浏览体验。在实际项目中,还需要对接后端接口获取真实的歌手数据。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)