import 'package:flutter/material.dart';
import 'video_player_box_widget.dart';
class VideoFeedPage extends StatefulWidget {
const VideoFeedPage({super.key});
@override
State<VideoFeedPage> createState() => _VideoFeedPageState();
}
class _VideoFeedPageState extends State<VideoFeedPage> {
late PageController _pageController;
late ValueNotifier<int> _currentIndex;
List<String> blocks = [
'http://1257092661.vod2.myqcloud.com/0d1bb4cevodtransgzp1257092661/184056d63270835009344025820/v.f30.mp4',
'assets/images/fj1.mp4',
'assets/images/fj2.mp4',
'assets/images/fj3.mp4',
];
@override
void initState() {
super.initState();
_pageController = PageController(keepPage: false);
_currentIndex = ValueNotifier(0);
}
@override
void dispose() {
_pageController.dispose();
_currentIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
extendBody: true,
extendBodyBehindAppBar: true,
backgroundColor: Colors.black,
body: PageView.builder(
controller: _pageController,
itemCount: blocks.length,
allowImplicitScrolling: true,
scrollDirection: Axis.vertical,
physics: const AlwaysScrollableScrollPhysics(),
onPageChanged: (index) => _currentIndex.value = index,
itemBuilder: (context, index) {
final block = blocks[index];
return ListenableBuilder(
listenable: _currentIndex,
builder: (context, _) {
final isCurrent = index == _currentIndex.value;
return VideoPlayerBoxWidget(
key: ValueKey(block),
videoUrl: block,
autoPlay: isCurrent,
);
});
},
),
);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:szxdflutter/pages/mine/page/video/video_player_widget.dart';
class VideoPlayerBoxWidget extends StatefulWidget {
const VideoPlayerBoxWidget({
super.key,
this.autoPlay = true,
this.looping = true,
this.fit = BoxFit.contain,
this.videoUrl,
});
final bool autoPlay;
final bool looping;
final BoxFit fit;
final String? videoUrl;
@override
VideoPlayerBoxWidgetState createState() => VideoPlayerBoxWidgetState();
}
class VideoPlayerBoxWidgetState extends State<VideoPlayerBoxWidget> with SingleTickerProviderStateMixin {
final ScrollController myScrollController = ScrollController();
StreamController<double> myStreamController = StreamController<double>.broadcast();
StreamController<double> myBottomSheetController = StreamController<double>();
double get maxW => MediaQuery.of(context).size.width;
double drawerH = 0.0;
double touchPosition = 0.0;
late AnimationController myAnimationController;
@override
void initState() {
super.initState();
myAnimationController = BottomSheet.createAnimationController(this);
myAnimationController.addListener(() {
final value = myAnimationController.value * drawerH;
myBottomSheetController.sink.add(value);
});
}
@override
void dispose() {
myStreamController.close();
myBottomSheetController.close();
myAnimationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
double topH = MediaQuery.of(context).padding.top;
double bottomH = MediaQuery.of(context).padding.bottom;
double maxH = MediaQuery.of(context).size.height - topH - bottomH - 0;
drawerH = maxH - 300;
return Scaffold(
backgroundColor: Colors.black,
body: _buildPageBody(maxH),
floatingActionButton: Container(
margin: const EdgeInsets.only(bottom: 200),
child: FloatingActionButton(
onPressed: () => _openBottomSheet(),
child: const Icon(Icons.add, color: Colors.white),
),
),
);
}
Widget _buildPageBody(double maxH) {
return SizedBox(
width: maxW,
height: maxH,
child: Stack(
children: [
StreamBuilder<double>(
stream: myBottomSheetController.stream,
initialData: 0,
builder: (_, snapshot) {
return AnimatedContainer(
duration: const Duration(milliseconds: 20),
height: maxH - snapshot.data!,
alignment: Alignment.center,
child: CommonVideoPlayer(
autoPlay: widget.autoPlay,
looping: widget.looping,
fit: widget.fit,
videoUrl: widget.videoUrl,
),
);
},
)
],
),
);
}
void _openBottomSheet() {
showModalBottomSheet(
context: context,
clipBehavior: Clip.antiAlias,
barrierColor: Colors.black.withOpacity(0),
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
),
transitionAnimationController: myAnimationController,
builder: (BuildContext context) {
return _buildBottomSheetContent();
},
);
}
Widget _buildBottomSheetContent() {
return StreamBuilder<double>(
stream: myStreamController.stream,
initialData: drawerH,
builder: (context, snapshot) {
double currentHeight = snapshot.data ?? drawerH;
return AnimatedContainer(
duration: const Duration(milliseconds: 100),
height: currentHeight,
color: const Color(0xFF2C2C2C),
child: Listener(
onPointerDown: (event) {
touchPosition = event.position.dy + myScrollController.offset;
},
onPointerMove: (event) => _handlePointerMove(event, currentHeight),
onPointerUp: (event) => _handlePointerUp(event, currentHeight),
child: _buildDrawerList(currentHeight),
),
);
},
);
}
void _handlePointerMove(PointerMoveEvent event, double currentHeight) {
if (myScrollController.offset != 0) {
return;
}
double distance = event.position.dy - touchPosition;
if (distance.abs() > 0) {
double updatedHeight = drawerH - distance;
if (updatedHeight > drawerH) {
return;
}
myStreamController.sink.add(updatedHeight);
}
}
void _handlePointerUp(PointerUpEvent event, double currentHeight) {
if (currentHeight < (drawerH * 0.5)) {
Navigator.pop(context);
} else {
myStreamController.sink.add(drawerH);
}
}
Widget _buildDrawerList(double currentHeight) {
return ListView.builder(
controller: myScrollController,
physics: currentHeight != drawerH ? const NeverScrollableScrollPhysics() : const ClampingScrollPhysics(),
itemCount: 20,
itemBuilder: (_, index) {
return Container(
width: double.infinity,
height: 40,
alignment: Alignment.center,
child: Text(
'项目 ${index + 1}',
style: const TextStyle(color: Colors.white, fontSize: 20),
),
);
},
);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
class CommonVideoPlayer extends StatefulWidget {
final bool autoPlay;
final bool looping;
final BoxFit fit;
final String? videoUrl;
const CommonVideoPlayer({
this.autoPlay = true,
this.looping = true,
this.fit = BoxFit.cover,
this.videoUrl,
super.key,
});
@override
State<CommonVideoPlayer> createState() => _CommonVideoPlayerState();
}
class _CommonVideoPlayerState extends State<CommonVideoPlayer> {
VideoPlayerController? _controller;
bool _showControls = true;
Timer? _hideTimer;
bool _isDragging = false;
double _dragValue = 0.0;
bool _isFastForwarding = false;
@override
void initState() {
super.initState();
_initializeController();
}
@override
void didUpdateWidget(CommonVideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.videoUrl != widget.videoUrl) {
_initializeController();
} else if (oldWidget.autoPlay != widget.autoPlay) {
if (widget.autoPlay) {
_controller?.play();
_startHideTimer();
} else {
_controller?.pause();
}
}
}
Future<void> _initializeController() async {
final oldController = _controller;
final VideoPlayerController newController;
if (widget.videoUrl!.startsWith('http')) {
newController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl!));
} else {
newController = VideoPlayerController.asset(widget.videoUrl!);
}
try {
await newController.initialize();
if (!mounted) {
await newController.dispose();
return;
}
if (oldController != null) {
oldController.removeListener(_onControllerUpdate);
await oldController.dispose();
}
setState(() {
_controller = newController;
});
newController.addListener(_onControllerUpdate);
if (widget.autoPlay) {
newController.play();
_startHideTimer();
}
newController.setLooping(widget.looping);
} catch (e) {
debugPrint('视频初始化失败: $e');
}
}
void _onControllerUpdate() {
if (mounted && !_isDragging) {
setState(() {});
}
}
@override
void dispose() {
_hideTimer?.cancel();
_controller?.removeListener(_onControllerUpdate);
_controller?.dispose();
super.dispose();
}
void _toggleControls() {
setState(() {
_showControls = !_showControls;
});
if (_showControls) {
_startHideTimer();
} else {
_hideTimer?.cancel();
}
}
void _startHideTimer() {
_hideTimer?.cancel();
_hideTimer = Timer(const Duration(seconds: 3), () {
if (mounted && _controller != null && _controller!.value.isPlaying && !_isDragging && !_isFastForwarding) {
setState(() {
_showControls = false;
});
}
});
}
void _startFastForward() {
if (_controller == null || !_controller!.value.isInitialized) return;
HapticFeedback.mediumImpact();
setState(() {
_isFastForwarding = true;
_showControls = false;
});
_controller?.setPlaybackSpeed(2.0);
}
void _stopFastForward() {
if (_controller == null || !_controller!.value.isInitialized) return;
setState(() {
_isFastForwarding = false;
});
_controller?.setPlaybackSpeed(1.0);
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, "0");
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return "$twoDigitMinutes:$twoDigitSeconds";
}
@override
Widget build(BuildContext context) {
if (_controller == null || !_controller!.value.isInitialized) {
return const Center(child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2));
}
return GestureDetector(
onTap: _toggleControls,
onLongPressStart: (_) => _startFastForward(),
onLongPressEnd: (_) => _stopFastForward(),
behavior: HitTestBehavior.opaque,
child: Stack(
children: [
Positioned.fill(
child: Center(
child: FittedBox(
fit: widget.fit,
clipBehavior: Clip.hardEdge,
child: SizedBox(
width: _controller!.value.size.width,
height: _controller!.value.size.height,
child: VideoPlayer(_controller!),
),
),
),
),
if (_isFastForwarding)
Positioned(
top: 40,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(20),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.fast_forward, color: Colors.white, size: 16),
SizedBox(width: 8),
Text("2.0X 快进中", style: TextStyle(color: Colors.white, fontSize: 14)),
],
),
),
),
),
if (_showControls || !_controller!.value.isPlaying || _isDragging) ...[
_buildControlOverlay(),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _buildBottomWarpper(),
),
]
],
),
);
}
Widget _buildControlOverlay() {
final isPlaying = _controller!.value.isPlaying;
return Container(
color: Colors.black26,
child: Stack(
children: [
Center(
child: IconButton(
iconSize: 50,
icon: Icon(
isPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled,
color: Colors.white,
),
onPressed: () {
setState(() {
if (isPlaying) {
_controller?.pause();
_hideTimer?.cancel();
} else {
_controller?.play();
_startHideTimer();
}
});
},
),
),
],
),
);
}
Container _buildBottomWarpper() {
final value = _controller!.value;
final duration = value.duration.inMilliseconds.toDouble();
final position = _isDragging ? _dragValue : value.position.inMilliseconds.toDouble();
return Container(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [Colors.black54, Colors.transparent]),
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: _isDragging ? 8 : 2,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: _isDragging ? 14 : 6),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white.withOpacity(0.3),
thumbColor: Colors.white,
overlayColor: Colors.white.withOpacity(0.2),
),
child: Slider(
value: position.clamp(0.0, duration),
min: 0.0,
max: duration > 0 ? duration : 1.0,
onChanged: (val) {
setState(() {
_isDragging = true;
_dragValue = val;
});
},
onChangeEnd: (val) async {
await _controller?.seekTo(Duration(milliseconds: val.toInt()));
setState(() {
_isDragging = false;
});
if (_controller != null && _controller!.value.isPlaying) {
_startHideTimer();
}
},
),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0, left: 0),
child: Text(
'${_formatDuration(Duration(milliseconds: position.toInt()))} / ${_formatDuration(value.duration)}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
],
),
);
}
}