266 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			266 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Dart
		
	
	
| 
 | ||
| import 'package:flutter/material.dart';
 | ||
| import 'package:video_player/video_player.dart';
 | ||
| import 'package:flutter/services.dart';
 | ||
| 
 | ||
| ///弹窗组件:VideoPlayerPopup
 | ||
| class VideoPlayerPopup extends StatefulWidget {
 | ||
|   /// 视频地址
 | ||
|   final String videoUrl;
 | ||
|   const VideoPlayerPopup({Key? key, required this.videoUrl}) : super(key: key);
 | ||
| 
 | ||
|   @override
 | ||
|   State<VideoPlayerPopup> createState() => _VideoPlayerPopupState();
 | ||
| 
 | ||
| }
 | ||
| 
 | ||
| class _VideoPlayerPopupState extends State<VideoPlayerPopup> {
 | ||
|   late VideoPlayerController _controller;
 | ||
|   bool _showControls = true;
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|     _controller = VideoPlayerController.networkUrl(
 | ||
|       Uri.parse(widget.videoUrl),
 | ||
|     )..initialize().then((_) {
 | ||
|         setState(() {});
 | ||
|         _controller.play();
 | ||
|       });
 | ||
|     // 自动隐藏控件
 | ||
|     _controller.addListener(() {
 | ||
|       if (_controller.value.isPlaying && _showControls) {
 | ||
|         Future.delayed(const Duration(seconds: 3), () {
 | ||
|           if (_controller.value.isPlaying && mounted) {
 | ||
|             setState(() => _showControls = false);
 | ||
|           }
 | ||
|         });
 | ||
|       }
 | ||
|     });
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   void dispose() {
 | ||
|     _controller.dispose();
 | ||
|     super.dispose();
 | ||
|   }
 | ||
| 
 | ||
|   Widget _buildControls() {
 | ||
|     final pos = _controller.value.position;
 | ||
|     final dur = _controller.value.duration;
 | ||
|     String fmt(Duration d) =>
 | ||
|         '${d.inMinutes.remainder(60).toString().padLeft(2, '0')}:'
 | ||
|             '${d.inSeconds.remainder(60).toString().padLeft(2, '0')}';
 | ||
| 
 | ||
|     return Positioned.fill(
 | ||
|       child: AnimatedOpacity(
 | ||
|         opacity: _showControls ? 1 : 0,
 | ||
|         duration: const Duration(milliseconds: 300),
 | ||
|         child: GestureDetector(
 | ||
|           onTap: () => setState(() => _showControls = !_showControls),
 | ||
|           child: Container(
 | ||
|             color: Colors.black45,
 | ||
|             child: Column(
 | ||
|               mainAxisAlignment: MainAxisAlignment.end,
 | ||
|               children: [
 | ||
|                 // 进度条
 | ||
|                 Slider(
 | ||
|                   value: pos.inMilliseconds.toDouble().clamp(0, dur.inMilliseconds.toDouble()),
 | ||
|                   max: dur.inMilliseconds.toDouble(),
 | ||
|                   onChanged: (v) {
 | ||
|                     _controller.seekTo(Duration(milliseconds: v.toInt()));
 | ||
|                   },
 | ||
|                 ),
 | ||
|                 // 时间 + 控制按钮
 | ||
|                 Padding(
 | ||
|                   padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
 | ||
|                   child: Row(
 | ||
|                     children: [
 | ||
|                       IconButton(
 | ||
|                         icon: Icon(
 | ||
|                           _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | ||
|                           color: Colors.white,
 | ||
|                         ),
 | ||
|                         onPressed: () {
 | ||
|                           setState(() {
 | ||
|                             _controller.value.isPlaying
 | ||
|                                 ? _controller.pause()
 | ||
|                                 : _controller.play();
 | ||
|                           });
 | ||
|                         },
 | ||
|                       ),
 | ||
|                       Text(
 | ||
|                         '${fmt(pos)} / ${fmt(dur)}',
 | ||
|                         style: const TextStyle(color: Colors.white),
 | ||
|                       ),
 | ||
|                       const Spacer(),
 | ||
|                       IconButton(
 | ||
|                         icon: const Icon(Icons.fullscreen, color: Colors.white),
 | ||
|                         onPressed: () {
 | ||
|                           Navigator.of(context).push(MaterialPageRoute(
 | ||
|                               builder: (_) => FullScreenVideoPage(
 | ||
|                                   controller: _controller)));
 | ||
|                         },
 | ||
|                       ),
 | ||
|                     ],
 | ||
|                   ),
 | ||
|                 ),
 | ||
|               ],
 | ||
|             ),
 | ||
|           ),
 | ||
|         ),
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     return Center(
 | ||
|       child: Material(
 | ||
|         color: Colors.transparent,
 | ||
|         child: Container(
 | ||
|           constraints: BoxConstraints(
 | ||
|             maxWidth: MediaQuery.of(context).size.width * 0.9,
 | ||
|             maxHeight: MediaQuery.of(context).size.height * 0.8,
 | ||
|           ),
 | ||
|           decoration: BoxDecoration(
 | ||
|             color: Colors.white,
 | ||
|             borderRadius: BorderRadius.circular(8),
 | ||
|           ),
 | ||
|           child: Stack(
 | ||
|             children: [
 | ||
|               // 视频内容
 | ||
|               if (_controller.value.isInitialized)
 | ||
|                 AspectRatio(
 | ||
|                   aspectRatio: _controller.value.aspectRatio,
 | ||
|                   child: VideoPlayer(_controller),
 | ||
|                 )
 | ||
|               else
 | ||
|                 const Center(child: CircularProgressIndicator()),
 | ||
| 
 | ||
|               // 关闭按钮
 | ||
|               Positioned(
 | ||
|                 top: 4,
 | ||
|                 right: 4,
 | ||
|                 child: IconButton(
 | ||
|                   icon: const Icon(Icons.close, color: Colors.black54),
 | ||
|                   onPressed: () => Navigator.of(context).pop(),
 | ||
|                 ),
 | ||
|               ),
 | ||
| 
 | ||
|               // 播放控制
 | ||
|               if (_controller.value.isInitialized) _buildControls(),
 | ||
|             ],
 | ||
|           ),
 | ||
|         ),
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /// 全屏横屏播放页面:FullScreenVideoPage
 | ||
| class FullScreenVideoPage extends StatefulWidget {
 | ||
|   final VideoPlayerController controller;
 | ||
|   const FullScreenVideoPage({Key? key, required this.controller}) : super(key: key);
 | ||
| 
 | ||
|   @override
 | ||
|   State<FullScreenVideoPage> createState() => _FullScreenVideoPageState();
 | ||
| }
 | ||
| 
 | ||
| class _FullScreenVideoPageState extends State<FullScreenVideoPage> {
 | ||
|   VideoPlayerController get _controller => widget.controller;
 | ||
| 
 | ||
|   @override
 | ||
|   void initState() {
 | ||
|     super.initState();
 | ||
|     // 进入横屏、沉浸式
 | ||
|     SystemChrome.setPreferredOrientations([
 | ||
|       DeviceOrientation.landscapeLeft,
 | ||
|       DeviceOrientation.landscapeRight,
 | ||
|     ]);
 | ||
|     SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   void dispose() {
 | ||
|     // 恢复竖屏
 | ||
|     SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
 | ||
|     SystemChrome.setPreferredOrientations([
 | ||
|       DeviceOrientation.portraitUp,
 | ||
|       DeviceOrientation.portraitDown,
 | ||
|     ]);
 | ||
|     super.dispose();
 | ||
|   }
 | ||
| 
 | ||
|   @override
 | ||
|   Widget build(BuildContext context) {
 | ||
|     return Scaffold(
 | ||
|       backgroundColor: Colors.black,
 | ||
|       body: Center(
 | ||
|         child: Stack(
 | ||
|           children: [
 | ||
|             // 全屏视频
 | ||
|             if (_controller.value.isInitialized)
 | ||
|               SizedBox.expand(child: VideoPlayer(_controller))
 | ||
|             else
 | ||
|               const Center(child: CircularProgressIndicator()),
 | ||
| 
 | ||
|             // 简单控制:点击画面切换播放/暂停
 | ||
|             GestureDetector(
 | ||
|               onTap: () {
 | ||
|                 setState(() {
 | ||
|                   _controller.value.isPlaying
 | ||
|                       ? _controller.pause()
 | ||
|                       : _controller.play();
 | ||
|                 });
 | ||
|               },
 | ||
|             ),
 | ||
| 
 | ||
|             // 返回按钮
 | ||
|             Positioned(
 | ||
|               top: 20,
 | ||
|               left: 20,
 | ||
|               child: IconButton(
 | ||
|                 icon: const Icon(Icons.arrow_back, color: Colors.white, size: 28),
 | ||
|                 onPressed: () => Navigator.of(context).pop(),
 | ||
|               ),
 | ||
|             ),
 | ||
| 
 | ||
|             // 时间 & 进度条(简单版)
 | ||
|             if (_controller.value.isInitialized)
 | ||
|               Positioned(
 | ||
|                 bottom: 20,
 | ||
|                 left: 20,
 | ||
|                 right: 20,
 | ||
|                 child: Row(
 | ||
|                   children: [
 | ||
|                     Text(
 | ||
|                       '${_format(_controller.value.position)} / ${_format(_controller.value.duration)}',
 | ||
|                       style: const TextStyle(color: Colors.white),
 | ||
|                     ),
 | ||
|                     const SizedBox(width: 12),
 | ||
|                     Expanded(
 | ||
|                       child: VideoProgressIndicator(
 | ||
|                         _controller,
 | ||
|                         allowScrubbing: true,
 | ||
|                         colors: VideoProgressColors(
 | ||
|                           playedColor: Colors.red,
 | ||
|                           bufferedColor: Colors.white54,
 | ||
|                           backgroundColor: Colors.white30,
 | ||
|                         ),
 | ||
|                       ),
 | ||
|                     ),
 | ||
|                   ],
 | ||
|                 ),
 | ||
|               ),
 | ||
|           ],
 | ||
|         ),
 | ||
|       ),
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   String _format(Duration d) =>
 | ||
|       '${d.inMinutes.remainder(60).toString().padLeft(2, '0')}:'
 | ||
|           '${d.inSeconds.remainder(60).toString().padLeft(2, '0')}';
 | ||
| }
 |