MV播放是音乐播放器中一个重要的视频功能模块。用户可以观看歌曲的MV,享受视听结合的体验。本篇将详细介绍如何实现一个功能完善的MV播放页面,包括视频控制、全屏切换、手势操作和相关MV推荐。

功能分析

MV播放页面需要实现以下功能:视频播放控制(播放/暂停、快进快退)、进度条拖动、全屏切换、手势调节音量亮度、清晰度和倍速选择、点赞收藏分享、相关MV推荐列表。

核心技术点

本篇涉及的核心技术包括:StatefulWidget状态管理、GestureDetector手势识别、SystemChrome系统UI控制、Stack层叠布局、Get.bottomSheet底部弹窗。

对应代码文件

lib/pages/mv/mv_player_page.dart

完整代码实现

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';

/// MV播放页面
/// 提供MV视频播放、全屏切换、相关MV推荐等功能
class MVPlayerPage extends StatefulWidget {
  final int id;
  const MVPlayerPage({super.key, required this.id});

  
  State<MVPlayerPage> createState() => _MVPlayerPageState();
}

这段代码导入了Flutter核心库、系统服务库和GetX状态管理库。services库提供SystemChrome用于控制系统UI和屏幕方向。MVPlayerPage继承StatefulWidget,通过构造函数接收MV ID,用于加载对应的MV数据。

class _MVPlayerPageState extends State<MVPlayerPage>
    with SingleTickerProviderStateMixin {
  // 播放状态
  bool _isPlaying = false;
  // 播放进度(0.0-1.0)
  double _progress = 0.3;
  // 当前播放时间
  Duration _currentPosition = const Duration(minutes: 1, seconds: 23);
  // 总时长
  final Duration _totalDuration = const Duration(minutes: 4, seconds: 35);
  // 是否全屏
  bool _isFullScreen = false;
  // 是否显示控制栏
  bool _showControls = true;

混入SingleTickerProviderStateMixin为动画提供vsync。定义了播放状态、进度、当前时间、总时长、全屏状态、控制栏显示等状态变量。_progress使用0到1的范围表示播放进度,方便与Slider组件配合使用。

  // 音量(0.0-1.0)
  double _volume = 0.8;
  // 亮度(0.0-1.0)
  double _brightness = 0.5;
  // 播放速度
  double _playbackSpeed = 1.0;
  // 是否点赞
  bool _isLiked = false;
  // 是否收藏
  bool _isCollected = false;
  // 清晰度
  String _quality = '1080P';

  // MV信息
  late Map<String, dynamic> _mvInfo;
  // 相关MV列表
  late List<Map<String, dynamic>> _relatedMVs;

继续定义音量、亮度、播放速度、点赞收藏状态和清晰度选择等状态变量。_mvInfo存储MV信息,_relatedMVs存储相关MV列表。这些状态会随用户操作而改变,触发UI更新。

  
  void initState() {
    super.initState();
    _initData();
    // 自动隐藏控制栏
    _startHideControlsTimer();
  }

  void _initData() {
    _mvInfo = {
      'name': 'MV ${widget.id + 1}',
      'artist': '歌手名称',
      'playCount': 1000000 + widget.id * 50000,
      'likeCount': 50000 + widget.id * 1000,
      'commentCount': 8000 + widget.id * 200,
      'shareCount': 3000 + widget.id * 100,
      'publishTime': '2024-01-${(widget.id % 28 + 1).toString().padLeft(2, '0')}',
      'description': '这是一首非常好听的歌曲MV,画面精美,值得一看!',
    };

initState中初始化MV数据和启动自动隐藏控制栏定时器。_initData方法初始化MV信息,包括名称、歌手、播放量、点赞数等。实际项目中这些数据应该从服务器获取,这里使用模拟数据演示。

    _relatedMVs = List.generate(10, (index) => {
      return {
        'id': index + 100,
        'name': '相关MV ${index + 1}',
        'artist': '歌手 ${index % 5 + 1}',
        'playCount': 500000 + index * 30000,
        'duration': '${3 + index % 3}:${(index * 17 % 60).toString().padLeft(2, '0')}',
      };
    });
  }

  
  void dispose() {
    // 恢复状态栏和屏幕方向
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
    SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
    super.dispose();
  }

_relatedMVs使用List.generate生成10个相关MV数据。dispose中恢复系统UI和屏幕方向,避免影响其他页面。这是使用SystemChrome时的重要清理工作,确保退出页面后系统状态正常。

  void _startHideControlsTimer() {
    Future.delayed(const Duration(seconds: 3), () {
      if (mounted && _isPlaying) {
        setState(() => _showControls = false);
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: _isFullScreen
            ? _buildVideoPlayer()
            : _buildNormalPlayer(),
      ),
    );
  }

_startHideControlsTimer方法在播放3秒后自动隐藏控制栏,让用户专注于视频内容。mounted检查组件是否还在树中,避免在组件销毁后调用setState。build方法根据全屏状态显示不同布局。

  /// 构建普通模式播放器
  Widget _buildNormalPlayer() {
    return Column(
      children: [
        // 视频播放区域(16:9比例)
        AspectRatio(
          aspectRatio: 16 / 9,
          child: _buildVideoPlayer(),
        ),
        // 内容区域
        Expanded(
          child: Container(
            color: const Color(0xFF121212),
            child: SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _buildMVInfo(),
                  _buildActionBar(),
                  const Divider(color: Colors.grey, height: 1),
                  _buildArtistInfo(),
                  const Divider(color: Colors.grey, height: 1),
                  _buildRelatedMVs(),
                ],
              ),
            ),
          ),
        ),
      ],
    );
  }

普通模式下视频在上方使用16:9宽高比,下方是可滚动的信息区域。AspectRatio确保视频保持正确的宽高比。SingleChildScrollView让内容可以滚动,包含MV信息、操作栏、歌手信息和相关MV列表。

  /// 构建视频播放器
  Widget _buildVideoPlayer() {
    return GestureDetector(
      onTap: () {
        setState(() => _showControls = !_showControls);
        if (_showControls) _startHideControlsTimer();
      },
      onDoubleTap: () => setState(() => _isPlaying = !_isPlaying),
      onHorizontalDragUpdate: (details) {
        // 水平滑动调整进度
        setState(() {
          _progress += details.delta.dx / MediaQuery.of(context).size.width;
          _progress = _progress.clamp(0.0, 1.0);
          _currentPosition = Duration(
            milliseconds: (_totalDuration.inMilliseconds * _progress).toInt(),
          );
        });
      },

_buildVideoPlayer方法构建视频播放器。GestureDetector处理多种手势:单击切换控制栏显示,双击切换播放暂停,水平滑动调整进度。clamp方法限制进度值在0到1之间,防止越界。

      onVerticalDragUpdate: (details) {
        // 垂直滑动调整音量/亮度
        final dx = details.localPosition.dx;
        final screenWidth = MediaQuery.of(context).size.width;
        if (dx < screenWidth / 2) {
          // 左侧调整亮度
          setState(() {
            _brightness -= details.delta.dy / 200;
            _brightness = _brightness.clamp(0.0, 1.0);
          });
        } else {
          // 右侧调整音量
          setState(() {
            _volume -= details.delta.dy / 200;
            _volume = _volume.clamp(0.0, 1.0);
          });
        }
      },

垂直滑动根据触摸位置判断是调整亮度还是音量。左侧滑动调整亮度,右侧滑动调整音量。向上滑动增加,向下滑动减少。这是视频播放器的常见交互方式,用户可以快速调节而不需要点击按钮。

      child: Stack(
        children: [
          // 视频画面(模拟)
          Container(
            color: Colors.primaries[widget.id % Colors.primaries.length]
                .withOpacity(0.3),
            child: const Center(
              child: Icon(Icons.videocam, size: 80, color: Colors.white30),
            ),
          ),
          // 控制层
          if (_showControls) ...[
            _buildTopControls(),
            _buildCenterControls(),
            _buildBottomControls(),
          ],
        ],
      ),
    );
  }

Stack叠加视频画面和控制层。控制层只在_showControls为true时显示,包含顶部控制栏、中间播放按钮和底部控制栏。实际项目中Container应该替换为真正的视频播放组件,如video_player插件。

  /// 构建顶部控制栏
  Widget _buildTopControls() {
    return Positioned(
      top: 0,
      left: 0,
      right: 0,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Colors.black.withOpacity(0.7), Colors.transparent],
          ),
        ),

_buildTopControls方法构建顶部控制栏。使用Positioned定位在顶部,渐变背景让控制栏与视频画面融合。从上到下的渐变让控制栏有一个自然的过渡效果,不会显得突兀。

        child: Row(
          children: [
            IconButton(
              icon: const Icon(Icons.arrow_back, color: Colors.white),
              onPressed: () {
                if (_isFullScreen) {
                  _toggleFullScreen();
                } else {
                  Get.back();
                }
              },
            ),
            Expanded(
              child: Text(
                _mvInfo['name'],
                style: const TextStyle(color: Colors.white, fontSize: 16),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
            ),
            IconButton(
              icon: const Icon(Icons.share, color: Colors.white),
              onPressed: () => _shareMV(),
            ),
          ],
        ),
      ),
    );
  }

顶部控制栏包含返回按钮、MV标题和分享按钮。全屏模式下返回按钮退出全屏,普通模式下返回上一页。Expanded让标题占据剩余空间,maxLines和overflow处理长标题的溢出情况。

  /// 构建中间控制按钮
  Widget _buildCenterControls() {
    return Center(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 快退10秒
          IconButton(
            icon: const Icon(Icons.replay_10, color: Colors.white, size: 36),
            onPressed: () {
              setState(() {
                _progress = (_progress - 0.05).clamp(0.0, 1.0);
              });
            },
          ),
          const SizedBox(width: 32),

_buildCenterControls方法构建中间播放控制。快退按钮每次减少5%的进度,使用clamp确保不会小于0。SizedBox添加按钮之间的间距,让布局更加舒适。

          // 播放/暂停按钮
          GestureDetector(
            onTap: () => setState(() => _isPlaying = !_isPlaying),
            child: Container(
              width: 64,
              height: 64,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: Colors.white.withOpacity(0.3),
              ),
              child: Icon(
                _isPlaying ? Icons.pause : Icons.play_arrow,
                color: Colors.white,
                size: 40,
              ),
            ),
          ),
          const SizedBox(width: 32),
          // 快进10秒
          IconButton(
            icon: const Icon(Icons.forward_10, color: Colors.white, size: 36),
            onPressed: () {
              setState(() {
                _progress = (_progress + 0.05).clamp(0.0, 1.0);
              });
            },
          ),
        ],
      ),
    );
  }

播放按钮使用半透明圆形背景突出显示,根据_isPlaying状态显示播放或暂停图标。快进按钮每次增加5%的进度。这种布局是视频播放器的标准设计,用户可以快速控制播放。

  /// 构建底部控制栏
  Widget _buildBottomControls() {
    return Positioned(
      bottom: 0,
      left: 0,
      right: 0,
      child: Container(
        padding: const EdgeInsets.all(8),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.bottomCenter,
            end: Alignment.topCenter,
            colors: [Colors.black.withOpacity(0.8), Colors.transparent],
          ),
        ),

_buildBottomControls方法构建底部控制栏。渐变背景从底部向上渐变,让控制栏与视频画面自然融合。padding设置内边距,让内容不紧贴边缘。

        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 进度条
            Row(
              children: [
                Text(
                  _formatDuration(_currentPosition),
                  style: const TextStyle(color: Colors.white, fontSize: 12),
                ),
                Expanded(
                  child: Slider(
                    value: _progress,
                    activeColor: const Color(0xFFE91E63),
                    inactiveColor: Colors.white24,
                    onChanged: (v) {
                      setState(() {
                        _progress = v;
                        _currentPosition = Duration(
                          milliseconds: (_totalDuration.inMilliseconds * v).toInt(),
                        );
                      });
                    },
                  ),
                ),
                Text(
                  _formatDuration(_totalDuration),
                  style: const TextStyle(color: Colors.white, fontSize: 12),
                ),
              ],
            ),

进度条两侧显示当前时间和总时长,Slider组件实现进度条拖动。activeColor使用粉色主题色,inactiveColor使用白色24%透明度。拖动进度条时同步更新_currentPosition。

            // 底部按钮
            Row(
              children: [
                IconButton(
                  icon: Icon(
                    _isPlaying ? Icons.pause : Icons.play_arrow,
                    color: Colors.white,
                  ),
                  onPressed: () => setState(() => _isPlaying = !_isPlaying),
                ),
                const Spacer(),
                // 清晰度选择
                GestureDetector(
                  onTap: () => _showQualityOptions(),
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.white54),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Text(
                      _quality,
                      style: const TextStyle(color: Colors.white, fontSize: 12),
                    ),
                  ),
                ),

底部按钮区域包含播放按钮、清晰度选择、倍速选择和全屏按钮。清晰度使用边框按钮,点击后弹出选择菜单。Spacer让按钮分布在两端,布局更加合理。

                const SizedBox(width: 8),
                // 倍速选择
                GestureDetector(
                  onTap: () => _showSpeedOptions(),
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                    decoration: BoxDecoration(
                      border: Border.all(color: Colors.white54),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: Text(
                      '${_playbackSpeed}x',
                      style: const TextStyle(color: Colors.white, fontSize: 12),
                    ),
                  ),
                ),
                // 全屏按钮
                IconButton(
                  icon: Icon(
                    _isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
                    color: Colors.white,
                  ),
                  onPressed: () => _toggleFullScreen(),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

倍速选择显示当前播放速度,全屏按钮根据当前状态显示不同图标。这些功能让用户可以自定义观看体验,是视频播放器的标准功能。

  /// 切换全屏
  void _toggleFullScreen() {
    setState(() => _isFullScreen = !_isFullScreen);
    if (_isFullScreen) {
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
      SystemChrome.setPreferredOrientations([
        DeviceOrientation.landscapeLeft,
        DeviceOrientation.landscapeRight,
      ]);
    } else {
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
      SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
    }
  }

  /// 格式化时长
  String _formatDuration(Duration duration) {
    final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }

_toggleFullScreen方法切换全屏模式。全屏时隐藏状态栏并切换到横屏,退出全屏时恢复状态栏和竖屏。_formatDuration方法格式化时间显示,使用padLeft补零保证格式统一。

GestureDetector手势识别

GestureDetector是Flutter中处理手势的核心组件,支持多种手势类型:

GestureDetector(
  onTap: () {},              // 单击
  onDoubleTap: () {},        // 双击
  onLongPress: () {},        // 长按
  onHorizontalDragUpdate: (details) {},  // 水平拖动
  onVerticalDragUpdate: (details) {},    // 垂直拖动
  child: // 子组件
)

在MV播放器中,我们使用单击切换控制栏、双击切换播放、水平拖动调整进度、垂直拖动调整音量亮度。

SystemChrome系统UI控制

SystemChrome用于控制系统UI和屏幕方向:

// 隐藏状态栏(沉浸式)
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);

// 显示状态栏
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

// 设置屏幕方向
SystemChrome.setPreferredOrientations([
  DeviceOrientation.landscapeLeft,
  DeviceOrientation.landscapeRight,
]);

immersiveSticky模式隐藏状态栏,用户从边缘滑动可以临时显示。setPreferredOrientations控制屏幕方向,可以设置为横屏或竖屏。

小结

本篇实现了音乐播放器的MV播放页面。通过GestureDetector实现丰富的手势操作,SystemChrome控制全屏切换,Stack层叠视频画面和控制层。手势操作让用户可以方便地调整进度、音量和亮度,全屏模式提供沉浸式的观看体验。这些技术是实现视频播放器的核心,掌握后可以应对各种视频播放需求。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐