flutter_integrated_whb/lib/customWidget/video_player_widget.dart

326 lines
11 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)}';
}
}