歌手详情页是音乐播放器中展示歌手信息的核心页面。用户可以在这里查看歌手的热门歌曲、专辑、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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐