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