在这里插入图片描述

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐