flutter_integrated_whb/lib/customWidget/video_player_widget.dart

326 lines
11 KiB
Dart
Raw Permalink Normal View History

2025-07-18 17:13:38 +08:00
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<VideoPlayerWidget> {
bool _visibleControls = true;
Timer? _hideTimer;
late Timer _positionTimer;
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
bool _isPlaying = false;
final ValueNotifier<double> _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<double>(
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)}';
}
}