如何在Flutter中实现类似YouTube的视频迷你播放器并保活状态?
Flutter 跨页面持久化迷你播放器实现方案
核心思路
要实现YouTube式的跨页面迷你播放器,关键是让播放器实例脱离页面路由层级,通过全局状态管理持有播放器控制器,同时将迷你播放器组件固定在全局布局的顶层,不受页面切换影响。
步骤1:全局播放器状态管理
创建单例状态管理器,统一持有播放器控制器、播放状态、迷你播放器显示状态等:
import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; class VideoPlayerManager extends ChangeNotifier { VideoPlayerController? _controller; bool _isMiniPlayerVisible = false; double _progress = 0.0; bool _isPlaying = false; String? _currentVideoUrl; // 对外暴露状态和控制器 VideoPlayerController? get controller => _controller; bool get isMiniPlayerVisible => _isMiniPlayerVisible; double get progress => _progress; bool get isPlaying => _isPlaying; String? get currentVideoUrl => _currentVideoUrl; // 初始化播放器(支持切换视频) void initializePlayer(String videoUrl) async { // 同一视频直接切换播放状态 if (_currentVideoUrl == videoUrl && _controller != null) { togglePlay(); return; } // 释放旧控制器资源 _controller?.dispose(); _currentVideoUrl = videoUrl; _controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl)); await _controller!.initialize(); _controller!.addListener(_updateState); _isPlaying = true; _controller!.play(); _isMiniPlayerVisible = true; notifyListeners(); } // 播放/暂停切换 void togglePlay() { if (_controller == null) return; _isPlaying ? _controller!.pause() : _controller!.play(); _isPlaying = !_isPlaying; notifyListeners(); } // 更新播放进度 void updateProgress() { if (_controller == null) return; _progress = _controller!.value.position.inMilliseconds / _controller!.value.duration.inMilliseconds; notifyListeners(); } // 隐藏迷你播放器 void hideMiniPlayer() { _isMiniPlayerVisible = false; _controller?.pause(); notifyListeners(); } // 监听播放器状态变化 void _updateState() { _isPlaying = _controller!.value.isPlaying; updateProgress(); notifyListeners(); } // 页面销毁时释放资源 @override void dispose() { _controller?.dispose(); super.dispose(); } }
步骤2:全局布局配置
将迷你播放器放在Navigator上层的Stack中,确保页面切换时播放器不被销毁:
void main() { runApp( ChangeNotifierProvider( create: (context) => VideoPlayerManager(), child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Stack( children: [ // 主页面路由容器 const Navigator( initialRoute: '/home', onGenerateRoute: (settings) { switch (settings.name) { case '/home': return MaterialPageRoute(builder: (_) => const HomePage()); case '/library': return MaterialPageRoute(builder: (_) => const LibraryPage()); case '/subscriptions': return MaterialPageRoute(builder: (_) => const SubscriptionsPage()); default: return MaterialPageRoute(builder: (_) => const HomePage()); } }, ), // 迷你播放器组件(固定在底部) Positioned( bottom: 20, left: 20, right: 20, child: Consumer<VideoPlayerManager>( builder: (context, manager, child) { if (!manager.isMiniPlayerVisible || manager.controller == null) { return const SizedBox.shrink(); } return GestureDetector( // 点击跳转到全屏播放页面 onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => FullScreenPlayerPage(manager: manager)), ), child: Container( height: 80, decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ // 视频预览区域 SizedBox( width: 120, child: VideoPlayer(manager.controller!), ), // 视频信息和进度条 Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '当前播放视频', style: TextStyle(color: Colors.white, fontSize: 14), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), LinearProgressIndicator( value: manager.progress, backgroundColor: Colors.grey[700], valueColor: const AlwaysStoppedAnimation<Color>(Colors.red), ), ], ), ), ), // 播放/暂停按钮 IconButton( icon: Icon(manager.isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white), onPressed: manager.togglePlay, ), // 关闭按钮 IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: manager.hideMiniPlayer, ), ], ), ), ); }, ), ), ], ), ), ); } }
步骤3:页面中调用播放器
在任意页面(如首页、库、订阅页)中,通过Provider获取管理器实例,触发播放操作:
class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('首页'), actions: [ IconButton(icon: const Icon(Icons.video_library), onPressed: () => Navigator.pushNamed(context, '/library')), IconButton(icon: const Icon(Icons.subscriptions), onPressed: () => Navigator.pushNamed(context, '/subscriptions')), ], ), body: ListView.builder( itemCount: 10, itemBuilder: (context, index) { return ListTile( title: Text('视频 $index'), subtitle: const Text('点击播放'), onTap: () { // 调用管理器初始化播放器 Provider.of<VideoPlayerManager>(context, listen: false) .initializePlayer('https://example.com/video$index.mp4'); }, ); }, ), ); } } // 全屏播放页面示例 class FullScreenPlayerPage extends StatelessWidget { final VideoPlayerManager manager; const FullScreenPlayerPage({super.key, required this.manager}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Center( child: AspectRatio( aspectRatio: manager.controller!.value.aspectRatio, child: VideoPlayer(manager.controller!), ), ), floatingActionButton: FloatingActionButton( onPressed: manager.togglePlay, child: Icon(manager.isPlaying ? Icons.pause : Icons.play_arrow), ), ); } }
关键注意事项
- 资源管理:播放器控制器由
VideoPlayerManager统一持有和释放,避免页面销毁时重复释放资源 - 状态同步:通过
ChangeNotifier通知所有监听组件更新UI,确保迷你播放器和页面中的播放状态一致 - 路由隔离:迷你播放器脱离页面路由层级,放在全局
Stack中,不受页面push/pop影响
内容的提问来源于stack exchange,提问作者Noob Developer
相关产品推荐
相关产品推荐

