flutter_integrated_whb/lib/customWidget/video_player_widget.dart

478 lines
14 KiB
Dart
Raw Normal View History

2025-07-18 17:13:38 +08:00
import 'dart:async';
import 'dart:math';
2025-07-22 13:34:34 +08:00
import 'dart:ui';
2025-07-18 17:13:38 +08:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
2025-08-29 09:52:48 +08:00
import 'package:qhd_prevention/tools/tools.dart';
2025-07-18 17:13:38 +08:00
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;
2025-08-29 09:52:48 +08:00
Timer? _positionTimer;
2025-07-18 17:13:38 +08:00
Duration _currentPosition = Duration.zero;
Duration _totalDuration = Duration.zero;
bool _isPlaying = false;
2025-08-29 09:52:48 +08:00
final ValueNotifier<double> _sliderValue = ValueNotifier<double>(0.0);
2025-08-29 20:33:23 +08:00
// 判断是否有controller避免被销毁后访问
2025-08-29 09:52:48 +08:00
bool get _hasController => widget.controller != null;
2025-08-29 20:33:23 +08:00
/// 安全判断 controller 是否初始化,避免抛异常
2025-08-29 09:52:48 +08:00
bool _controllerInitializedSafe() {
try {
final c = widget.controller;
if (c == null) return false;
return c.value.isInitialized;
} catch (e) {
return false;
}
}
2025-07-18 17:13:38 +08:00
@override
void initState() {
super.initState();
2025-08-29 20:33:23 +08:00
// 初始化时立即启动隐藏控制栏的定时器
2025-07-18 17:13:38 +08:00
_startHideTimer();
2025-08-29 20:33:23 +08:00
// 如果有controller并且初始化了则启动进度定时器
2025-08-29 09:52:48 +08:00
_maybeStartPositionTimer();
2025-08-29 20:33:23 +08:00
// 给controller添加监听
2025-08-29 09:52:48 +08:00
_addControllerListenerSafely();
2025-08-29 20:33:23 +08:00
// 如果controller已经初始化获取初始值
2025-08-29 09:52:48 +08:00
if (_controllerInitializedSafe()) {
_updateControllerValuesSafe();
2025-07-18 17:13:38 +08:00
}
}
@override
void didUpdateWidget(covariant VideoPlayerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
2025-08-29 09:52:48 +08:00
2025-08-29 20:33:23 +08:00
// 如果controller发生变化需要移除旧的监听添加新的监听
2025-08-29 09:52:48 +08:00
if (oldWidget.controller != widget.controller) {
_removeControllerListenerSafely(oldWidget.controller);
_addControllerListenerSafely();
_restartPositionTimer();
_updateControllerValuesSafe();
}
}
void _addControllerListenerSafely() {
try {
2025-07-18 17:13:38 +08:00
widget.controller?.addListener(_controllerListener);
2025-08-29 09:52:48 +08:00
} catch (_) {
2025-08-29 20:33:23 +08:00
// 忽略已经被销毁的情况
2025-08-29 09:52:48 +08:00
}
}
void _removeControllerListenerSafely([VideoPlayerController? ctrl]) {
final c = ctrl ?? widget.controller;
if (c == null) return;
try {
c.removeListener(_controllerListener);
} catch (_) {
2025-08-29 20:33:23 +08:00
// 忽略
2025-07-18 17:13:38 +08:00
}
}
void _controllerListener() {
2025-07-22 13:34:34 +08:00
if (!mounted) return;
2025-08-29 09:52:48 +08:00
_updateControllerValuesSafe();
2025-07-18 17:13:38 +08:00
}
2025-08-29 20:33:23 +08:00
// 更新播放状态、进度等信息
2025-08-29 09:52:48 +08:00
void _updateControllerValuesSafe() {
final c = widget.controller;
if (c == null) {
2025-08-29 20:33:23 +08:00
// 没有controller时清空数据
2025-08-29 09:52:48 +08:00
if (mounted) {
setState(() {
_isPlaying = false;
_totalDuration = Duration.zero;
_currentPosition = Duration.zero;
_sliderValue.value = 0;
});
}
return;
}
2025-07-18 17:13:38 +08:00
2025-08-29 09:52:48 +08:00
try {
final value = c.value;
if (!value.isInitialized) {
if (mounted) {
setState(() {
_isPlaying = false;
_totalDuration = Duration.zero;
_currentPosition = Duration.zero;
_sliderValue.value = 0;
});
}
return;
}
final pos = value.position;
final dur = value.duration;
final playing = value.isPlaying;
if (mounted) {
setState(() {
_isPlaying = playing;
_totalDuration = dur;
_currentPosition = pos;
_sliderValue.value = pos.inMilliseconds.toDouble();
});
}
} catch (e) {
2025-08-29 20:33:23 +08:00
// controller被销毁时忽略
2025-08-29 09:52:48 +08:00
}
2025-07-18 17:13:38 +08:00
}
2025-08-29 20:33:23 +08:00
// 启动隐藏控制栏的定时器3秒后隐藏
2025-07-18 17:13:38 +08:00
void _startHideTimer() {
_hideTimer?.cancel();
_hideTimer = Timer(const Duration(seconds: 3), () {
if (mounted) setState(() => _visibleControls = false);
});
}
2025-08-29 20:33:23 +08:00
// 启动进度定时器(定时刷新播放进度)
2025-08-29 09:52:48 +08:00
void _maybeStartPositionTimer() {
if (_positionTimer != null && _positionTimer!.isActive) return;
if (!_hasController) return;
_positionTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
if (!mounted) return;
if (!_controllerInitializedSafe()) return;
try {
2025-07-22 13:34:34 +08:00
final c = widget.controller!;
2025-08-29 09:52:48 +08:00
final pos = c.value.position;
final dur = c.value.duration;
final playing = c.value.isPlaying;
setState(() {
_currentPosition = pos;
_totalDuration = dur;
_isPlaying = playing;
_sliderValue.value = pos.inMilliseconds.toDouble();
});
} catch (_) {
2025-08-29 20:33:23 +08:00
// controller销毁时忽略
2025-08-29 09:52:48 +08:00
}
2025-07-18 17:13:38 +08:00
});
}
2025-08-29 09:52:48 +08:00
void _restartPositionTimer() {
_positionTimer?.cancel();
_maybeStartPositionTimer();
}
void _stopPositionTimer() {
try {
_positionTimer?.cancel();
} catch (_) {}
_positionTimer = null;
}
2025-08-29 20:33:23 +08:00
// 点击切换控制栏显示/隐藏
2025-07-18 17:13:38 +08:00
void _toggleControls() {
setState(() => _visibleControls = !_visibleControls);
2025-08-29 09:52:48 +08:00
if (_visibleControls)
_startHideTimer();
else
_hideTimer?.cancel();
2025-07-18 17:13:38 +08:00
}
2025-08-29 20:33:23 +08:00
// 播放/暂停切换
2025-07-18 17:13:38 +08:00
void _togglePlayPause() {
2025-08-29 09:52:48 +08:00
final c = widget.controller;
if (c == null) return;
try {
if (_isPlaying) {
c.pause();
setState(() => _isPlaying = false);
} else {
c.play();
setState(() => _isPlaying = true);
}
_startHideTimer();
2025-08-29 20:33:23 +08:00
} catch (_) {}
2025-07-18 17:13:38 +08:00
}
2025-08-29 20:33:23 +08:00
// 进入全屏播放
2025-08-29 09:52:48 +08:00
Future<void> _enterFullScreen() async {
try {
2025-08-29 20:33:23 +08:00
// 设置横屏、沉浸式UI
2025-08-29 09:52:48 +08:00
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
await NativeOrientation.setLandscape();
} catch (_) {}
2025-07-18 17:13:38 +08:00
2025-08-29 20:33:23 +08:00
// 跳转到全屏页面
2025-08-29 09:52:48 +08:00
await Navigator.of(context).push(
2025-07-22 13:34:34 +08:00
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,
2025-07-18 17:13:38 +08:00
),
2025-07-22 13:34:34 +08:00
allowSeek: widget.allowSeek,
isFullScreen: true,
),
2025-07-18 17:13:38 +08:00
),
),
),
2025-08-29 09:52:48 +08:00
);
2025-08-29 20:33:23 +08:00
// 返回后恢复竖屏和UI
2025-08-29 09:52:48 +08:00
try {
await NativeOrientation.setPortrait();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} catch (_) {}
if (!mounted) return;
2025-08-29 20:33:23 +08:00
setState(() {}); // 强制刷新修复退出全屏后UI问题
2025-08-29 09:52:48 +08:00
}
@override
void dispose() {
_hideTimer?.cancel();
_stopPositionTimer();
_sliderValue.dispose();
_removeControllerListenerSafely();
super.dispose();
2025-07-18 17:13:38 +08:00
}
@override
Widget build(BuildContext context) {
2025-08-29 09:52:48 +08:00
final media = MediaQuery.of(context);
final screenW = media.size.width;
final screenH = media.size.height;
2025-08-29 20:33:23 +08:00
// 非全屏高度:按照宽高比计算,但不超过屏幕一半高度
final preferredNonFullHeight = min(
screenW / (widget.aspectRatio <= 0 ? (16 / 9) : widget.aspectRatio),
screenH * 0.5,
);
2025-08-29 09:52:48 +08:00
2025-08-29 20:33:23 +08:00
final containerW = screenW;
final containerH = widget.isFullScreen ? screenH : preferredNonFullHeight;
2025-07-18 17:13:38 +08:00
2025-07-22 13:34:34 +08:00
return Center(
child: SizedBox(
width: containerW,
height: containerH,
child: GestureDetector(
2025-08-29 09:52:48 +08:00
behavior: HitTestBehavior.translucent,
2025-07-22 13:34:34 +08:00
onTap: _toggleControls,
child: Stack(
fit: StackFit.expand,
children: [
2025-08-29 20:33:23 +08:00
// 视频画面或封面图
2025-08-29 09:52:48 +08:00
_buildVideoOrCover(containerW, containerH),
2025-08-29 20:33:23 +08:00
// 全屏模式时左上角显示返回按钮
2025-08-29 09:52:48 +08:00
if (widget.isFullScreen)
2025-07-22 13:34:34 +08:00
Positioned(
2025-08-29 20:33:23 +08:00
top: MediaQuery.of(context).padding.top + 8,
2025-08-29 09:52:48 +08:00
left: 8,
child: SafeArea(
top: true,
bottom: false,
child: ClipOval(
child: Material(
2025-08-29 20:33:23 +08:00
color: Colors.black38,
2025-08-29 09:52:48 +08:00
child: InkWell(
onTap: () async {
try {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} catch (_) {}
Navigator.of(context).maybePop();
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.arrow_back,
2025-07-22 13:34:34 +08:00
color: Colors.white,
2025-08-29 09:52:48 +08:00
size: 22,
2025-07-22 13:34:34 +08:00
),
),
),
2025-08-29 09:52:48 +08:00
),
2025-07-18 17:13:38 +08:00
),
2025-07-22 13:34:34 +08:00
),
2025-07-18 17:13:38 +08:00
),
2025-08-29 20:33:23 +08:00
// 控制栏
2025-08-29 09:52:48 +08:00
if (_visibleControls) _buildControls(),
2025-07-22 13:34:34 +08:00
],
),
),
2025-07-18 17:13:38 +08:00
),
);
}
2025-08-29 20:33:23 +08:00
// 构建视频画面或封面图
2025-08-29 09:52:48 +08:00
Widget _buildVideoOrCover(double containerW, double containerH) {
final c = widget.controller;
if (c != null) {
try {
if (c.value.isInitialized) {
2025-08-29 20:33:23 +08:00
final vidAspect =
(c.value.size.height > 0) ? (c.value.size.width / c.value.size.height) : widget.aspectRatio;
2025-08-29 09:52:48 +08:00
return Center(
child: AspectRatio(
aspectRatio: vidAspect > 0 ? vidAspect : widget.aspectRatio,
child: VideoPlayer(c),
),
);
}
2025-08-29 20:33:23 +08:00
} catch (_) {}
2025-08-29 09:52:48 +08:00
}
if (widget.coverUrl.isNotEmpty) {
return Image.network(
widget.coverUrl,
width: containerW,
height: containerH,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(color: Colors.black),
);
}
return Container(color: Colors.black);
}
2025-08-29 20:33:23 +08:00
// 构建底部控制栏
2025-08-29 09:52:48 +08:00
Widget _buildControls() {
final totalMs = _totalDuration.inMilliseconds.toDouble();
final sliderMax = totalMs > 0 ? totalMs : 1.0;
final sliderValue = _sliderValue.value.clamp(0.0, sliderMax).toDouble();
return 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<double>(
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: Slider(
value: sliderValue,
min: 0,
max: sliderMax,
onChanged: (v) {
if (widget.allowSeek && widget.controller != null) {
try {
widget.controller!.seekTo(Duration(milliseconds: v.toInt()));
setState(() => _currentPosition = Duration(milliseconds: v.toInt()));
_sliderValue.value = v;
_startHideTimer();
} catch (_) {}
}
},
),
),
),
),
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: () {
if (widget.isFullScreen) {
Navigator.of(context).maybePop();
} else {
_enterFullScreen();
}
},
),
],
),
),
);
}
2025-08-29 20:33:23 +08:00
// 格式化时间
2025-07-18 17:13:38 +08:00
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
2025-07-22 13:34:34 +08:00
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)}';
2025-07-18 17:13:38 +08:00
}
2025-07-22 13:34:34 +08:00
}