import 'dart:async'; import 'dart:math'; import 'dart:ui'; 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) return; _updateControllerValues(); } void _updateControllerValues() { final c = widget.controller!; setState(() { _isPlaying = c.value.isPlaying; _totalDuration = c.value.duration; _currentPosition = c.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) return; setState(() { final c = widget.controller!; _currentPosition = c.value.position; _totalDuration = c.value.duration; _isPlaying = c.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, body: SafeArea( top: false, bottom: false, child: 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, ), ), ), ), ) .then((_) { SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); }); } @override Widget build(BuildContext context) { final screenW = MediaQuery.of(context).size.width; final containerW = widget.isFullScreen ? double.infinity : screenW; final containerH = widget.isFullScreen ? double.infinity : containerW / widget.aspectRatio; return Center( child: SizedBox( width: containerW, height: containerH, child: GestureDetector( behavior: HitTestBehavior.translucent, // ← 允许空白区域也响应 onTap: _toggleControls, child: Stack( fit: StackFit.expand, children: [ // 视频或封面 if (widget.controller != null && widget.controller!.value.isInitialized) FittedBox( fit: BoxFit.contain, alignment: Alignment.center, child: SizedBox( width: widget.controller!.value.size.width, height: widget.controller!.value.size.height, child: VideoPlayer(widget.controller!), ), ) else if (widget.coverUrl.length > 0) Image.network( widget.coverUrl, fit: BoxFit.cover, width: containerW, height: containerH, ), // 控制栏 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( children: [ IconButton( padding: EdgeInsets.zero, icon: Icon( _isPlaying ? Icons.pause : Icons.play_arrow, size: 28, color: Colors.white, ), onPressed: _togglePlayPause, ), Expanded( child: ValueListenableBuilder( valueListenable: _sliderValue, builder: (_, value, __) => SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: Colors.white, inactiveTrackColor: Colors.white54, thumbColor: Colors.white, overlayColor: Colors.white24, trackHeight: 2, thumbShape: RoundSliderThumbShape( enabledThumbRadius: 8), ), child: SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: Colors.white, // 活跃轨道颜色 inactiveTrackColor: Colors.grey[400],// 非活跃轨道颜色 thumbColor: Colors.white, // 滑块颜色 overlayColor: Colors.white.withAlpha(0x33), // 滑块按下外圈 disabledActiveTrackColor: Colors.white, // 禁用时也用同样的活跃轨道 disabledInactiveTrackColor: Colors.grey[400], disabledThumbColor: Colors.white, ), child: Slider( value: value, min: 0, max: _totalDuration.inMilliseconds.toDouble(), // 不管 allowSeek 如何,都不改变 onChanged onChanged: (v) { if (widget.allowSeek && widget.controller != null) { widget.controller!.seekTo(Duration(milliseconds: v.toInt())); setState(() => _currentPosition = Duration(milliseconds: v.toInt())); _sliderValue.value = v; _startHideTimer(); } }, ), ), ), ), ), SizedBox( width: 110, child: Text( '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', style: TextStyle( color: Colors.white, fontSize: 12, fontFeatures: [FontFeature.tabularFigures()], ), ), ), 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 h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60); if (h > 0) return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}'; return '${twoDigits(m)}:${twoDigits(s)}'; } }