266 lines
8.2 KiB
Dart
266 lines
8.2 KiB
Dart
|
||
import 'package:flutter/material.dart';
|
||
import 'package:video_player/video_player.dart';
|
||
import 'package:flutter/services.dart';
|
||
|
||
///弹窗组件:VideoPlayerPopup
|
||
class VideoPlayerPopup extends StatefulWidget {
|
||
/// 视频地址
|
||
final String videoUrl;
|
||
const VideoPlayerPopup({Key? key, required this.videoUrl}) : super(key: key);
|
||
|
||
@override
|
||
State<VideoPlayerPopup> createState() => _VideoPlayerPopupState();
|
||
|
||
}
|
||
|
||
class _VideoPlayerPopupState extends State<VideoPlayerPopup> {
|
||
late VideoPlayerController _controller;
|
||
bool _showControls = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller = VideoPlayerController.networkUrl(
|
||
Uri.parse(widget.videoUrl),
|
||
)..initialize().then((_) {
|
||
setState(() {});
|
||
_controller.play();
|
||
});
|
||
// 自动隐藏控件
|
||
_controller.addListener(() {
|
||
if (_controller.value.isPlaying && _showControls) {
|
||
Future.delayed(const Duration(seconds: 3), () {
|
||
if (_controller.value.isPlaying && mounted) {
|
||
setState(() => _showControls = false);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Widget _buildControls() {
|
||
final pos = _controller.value.position;
|
||
final dur = _controller.value.duration;
|
||
String fmt(Duration d) =>
|
||
'${d.inMinutes.remainder(60).toString().padLeft(2, '0')}:'
|
||
'${d.inSeconds.remainder(60).toString().padLeft(2, '0')}';
|
||
|
||
return Positioned.fill(
|
||
child: AnimatedOpacity(
|
||
opacity: _showControls ? 1 : 0,
|
||
duration: const Duration(milliseconds: 300),
|
||
child: GestureDetector(
|
||
onTap: () => setState(() => _showControls = !_showControls),
|
||
child: Container(
|
||
color: Colors.black45,
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
// 进度条
|
||
Slider(
|
||
value: pos.inMilliseconds.toDouble().clamp(0, dur.inMilliseconds.toDouble()),
|
||
max: dur.inMilliseconds.toDouble(),
|
||
onChanged: (v) {
|
||
_controller.seekTo(Duration(milliseconds: v.toInt()));
|
||
},
|
||
),
|
||
// 时间 + 控制按钮
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
IconButton(
|
||
icon: Icon(
|
||
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
|
||
color: Colors.white,
|
||
),
|
||
onPressed: () {
|
||
setState(() {
|
||
_controller.value.isPlaying
|
||
? _controller.pause()
|
||
: _controller.play();
|
||
});
|
||
},
|
||
),
|
||
Text(
|
||
'${fmt(pos)} / ${fmt(dur)}',
|
||
style: const TextStyle(color: Colors.white),
|
||
),
|
||
const Spacer(),
|
||
IconButton(
|
||
icon: const Icon(Icons.fullscreen, color: Colors.white),
|
||
onPressed: () {
|
||
Navigator.of(context).push(MaterialPageRoute(
|
||
builder: (_) => FullScreenVideoPage(
|
||
controller: _controller)));
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Center(
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: Container(
|
||
constraints: BoxConstraints(
|
||
maxWidth: MediaQuery.of(context).size.width * 0.9,
|
||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
// 视频内容
|
||
if (_controller.value.isInitialized)
|
||
AspectRatio(
|
||
aspectRatio: _controller.value.aspectRatio,
|
||
child: VideoPlayer(_controller),
|
||
)
|
||
else
|
||
const Center(child: CircularProgressIndicator()),
|
||
|
||
// 关闭按钮
|
||
Positioned(
|
||
top: 4,
|
||
right: 4,
|
||
child: IconButton(
|
||
icon: const Icon(Icons.close, color: Colors.black54),
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
),
|
||
),
|
||
|
||
// 播放控制
|
||
if (_controller.value.isInitialized) _buildControls(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 全屏横屏播放页面:FullScreenVideoPage
|
||
class FullScreenVideoPage extends StatefulWidget {
|
||
final VideoPlayerController controller;
|
||
const FullScreenVideoPage({Key? key, required this.controller}) : super(key: key);
|
||
|
||
@override
|
||
State<FullScreenVideoPage> createState() => _FullScreenVideoPageState();
|
||
}
|
||
|
||
class _FullScreenVideoPageState extends State<FullScreenVideoPage> {
|
||
VideoPlayerController get _controller => widget.controller;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// 进入横屏、沉浸式
|
||
SystemChrome.setPreferredOrientations([
|
||
DeviceOrientation.landscapeLeft,
|
||
DeviceOrientation.landscapeRight,
|
||
]);
|
||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
// 恢复竖屏
|
||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||
SystemChrome.setPreferredOrientations([
|
||
DeviceOrientation.portraitUp,
|
||
DeviceOrientation.portraitDown,
|
||
]);
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.black,
|
||
body: Center(
|
||
child: Stack(
|
||
children: [
|
||
// 全屏视频
|
||
if (_controller.value.isInitialized)
|
||
SizedBox.expand(child: VideoPlayer(_controller))
|
||
else
|
||
const Center(child: CircularProgressIndicator()),
|
||
|
||
// 简单控制:点击画面切换播放/暂停
|
||
GestureDetector(
|
||
onTap: () {
|
||
setState(() {
|
||
_controller.value.isPlaying
|
||
? _controller.pause()
|
||
: _controller.play();
|
||
});
|
||
},
|
||
),
|
||
|
||
// 返回按钮
|
||
Positioned(
|
||
top: 20,
|
||
left: 20,
|
||
child: IconButton(
|
||
icon: const Icon(Icons.arrow_back, color: Colors.white, size: 28),
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
),
|
||
),
|
||
|
||
// 时间 & 进度条(简单版)
|
||
if (_controller.value.isInitialized)
|
||
Positioned(
|
||
bottom: 20,
|
||
left: 20,
|
||
right: 20,
|
||
child: Row(
|
||
children: [
|
||
Text(
|
||
'${_format(_controller.value.position)} / ${_format(_controller.value.duration)}',
|
||
style: const TextStyle(color: Colors.white),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: VideoProgressIndicator(
|
||
_controller,
|
||
allowScrubbing: true,
|
||
colors: VideoProgressColors(
|
||
playedColor: Colors.red,
|
||
bufferedColor: Colors.white54,
|
||
backgroundColor: Colors.white30,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
String _format(Duration d) =>
|
||
'${d.inMinutes.remainder(60).toString().padLeft(2, '0')}:'
|
||
'${d.inSeconds.remainder(60).toString().padLeft(2, '0')}';
|
||
}
|