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



所有评论(0)