import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player/video_player.dart'; class VideoPlayerWidget extends StatefulWidget { final VideoPlayerController? controller; final String coverUrl; final double aspectRatio; final bool allowSeek; final bool isFullScreen; const VideoPlayerWidget({ Key? key, required this.controller, required this.coverUrl, required this.aspectRatio, this.allowSeek = true, this.isFullScreen = false, }) : super(key: key); @override _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(); } class _VideoPlayerWidgetState extends State { bool _visibleControls = true; Timer? _hideTimer; late Timer _positionTimer; Duration _currentPosition = Duration.zero; Duration _totalDuration = Duration.zero; bool _isPlaying = false; final ValueNotifier _sliderValue = ValueNotifier(0.0); @override void initState() { super.initState(); _startHideTimer(); _startPositionTimer(); if (widget.controller != null) { widget.controller!.addListener(_controllerListener); if (widget.controller!.value.isInitialized) { _updateControllerValues(); } } } @override void didUpdateWidget(covariant VideoPlayerWidget oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { oldWidget.controller?.removeListener(_controllerListener); widget.controller?.addListener(_controllerListener); _updateControllerValues(); } } void _controllerListener() { if (mounted) setState(() {}); if (mounted && widget.controller != null) { _updateControllerValues(); } } void _updateControllerValues() { final controller = widget.controller!; setState(() { _isPlaying = controller.value.isPlaying; _totalDuration = controller.value.duration; _currentPosition = controller.value.position; _sliderValue.value = _currentPosition.inMilliseconds.toDouble(); }); } @override void dispose() { _hideTimer?.cancel(); _positionTimer.cancel(); _sliderValue.dispose(); widget.controller?.removeListener(_controllerListener); super.dispose(); } void _startHideTimer() { _hideTimer?.cancel(); _hideTimer = Timer(const Duration(seconds: 3), () { if (mounted) setState(() => _visibleControls = false); }); } void _startPositionTimer() { _positionTimer = Timer.periodic(const Duration(milliseconds: 200), (_) { if (mounted && widget.controller != null && widget.controller!.value.isInitialized) { setState(() { _currentPosition = widget.controller!.value.position; _totalDuration = widget.controller!.value.duration; _isPlaying = widget.controller!.value.isPlaying; _sliderValue.value = _currentPosition.inMilliseconds.toDouble(); }); } }); } void _toggleControls() { setState(() => _visibleControls = !_visibleControls); if (_visibleControls) _startHideTimer(); else _hideTimer?.cancel(); } void _togglePlayPause() { if (widget.controller == null) return; setState(() { _isPlaying = !_isPlaying; }); if (_isPlaying) { widget.controller!.play(); } else { widget.controller!.pause(); } _startHideTimer(); } void _enterFullScreen() { // 锁定横屏 SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); // 设置全屏模式 SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); Navigator.of(context).push(MaterialPageRoute( builder: (ctx) => Scaffold( backgroundColor: Colors.black, // 使用SafeArea包裹整个全屏播放器 body: SafeArea( top: false, // 顶部不使用安全区域(状态栏区域) bottom: false, // 底部不使用安全区域(导航栏区域) child: Stack( children: [ // 全屏视频播放器 VideoPlayerWidget( controller: widget.controller, coverUrl: widget.coverUrl, aspectRatio: max( widget.aspectRatio, MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height ), allowSeek: widget.allowSeek, isFullScreen: true, ), // 添加退出按钮,并包裹在SafeArea中 SafeArea( child: Align( alignment: Alignment.topLeft, child: Padding( padding: const EdgeInsets.all(16.0), child: GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.black38, shape: BoxShape.circle, ), child: const Icon( Icons.arrow_back, color: Colors.white, size: 20, ), ), ), ), ), ), ], ), ), ), )).then((_) { // 恢复竖屏 SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // 恢复系统UI SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); }); } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; final fullScreenAspectRatio = max( widget.aspectRatio, screenSize.width / screenSize.height ); return GestureDetector( onTap: _toggleControls, child: Stack( fit: widget.isFullScreen ? StackFit.expand : StackFit.loose, children: [ // 视频播放区域 if (widget.controller != null && widget.controller!.value.isInitialized) AspectRatio( aspectRatio: widget.isFullScreen ? fullScreenAspectRatio : widget.aspectRatio, child: VideoPlayer(widget.controller!), ) else Image.network( widget.coverUrl, fit: BoxFit.cover, width: widget.isFullScreen ? double.infinity : null, height: widget.isFullScreen ? double.infinity : null, ), // 控制面板 if (_visibleControls) Positioned( bottom: 0, left: 0, right: 0, child: Container( height: 50, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black.withOpacity(0.7), Colors.transparent], ), ), padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( padding: EdgeInsets.zero, icon: Icon( _isPlaying ? Icons.pause : Icons.play_arrow, size: 28, color: Colors.white, ), onPressed: _togglePlayPause, ), const SizedBox(width: 0), Expanded( child: ValueListenableBuilder( valueListenable: _sliderValue, builder: (context, value, child) { return SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: Colors.white, inactiveTrackColor: Colors.white54, thumbColor: Colors.white, overlayColor: Colors.white24, trackHeight: 2, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), ), child: Slider( value: value, min: 0, max: _totalDuration.inMilliseconds.toDouble(), onChanged: widget.allowSeek && widget.controller != null ? (v) { widget.controller!.seekTo(Duration(milliseconds: v.toInt())); setState(() { _currentPosition = Duration(milliseconds: v.toInt()); }); _sliderValue.value = v; _startHideTimer(); } : null, ), ); } ), ), const SizedBox(width: 0), // 使用固定宽度的文本容器,避免进度条跳动 SizedBox( width: 110, // 固定宽度,防止进度条长度变化 child: Text( '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', style: const TextStyle( color: Colors.white, fontSize: 12, fontFeatures: [FontFeature.tabularFigures()], // 等宽字体 ), ), ), const SizedBox(width: 0), IconButton( padding: EdgeInsets.zero, icon: Icon( widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, size: 28, color: Colors.white, ), onPressed: () { widget.isFullScreen ? Navigator.of(context).pop() : _enterFullScreen(); }, ), ], ), ), ), ], ), ); } String _formatDuration(Duration d) { String twoDigits(int n) => n.toString().padLeft(2, '0'); final hours = d.inHours; final minutes = d.inMinutes.remainder(60); final seconds = d.inSeconds.remainder(60); if (hours > 0) { return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}'; } return '${twoDigits(minutes)}:${twoDigits(seconds)}'; } }