import 'package:flutter/material.dart'; import 'package:media_kit_video/media_kit_video.dart'; import '../../../core/player/feed_player_pool.dart'; /// Feed 用的抖音风格视频播放器: /// - 覆盖 Video(controller: pool.controllerAt(slot)) /// - 底部一条 2px 细线进度条(已播白 / 未播半透明白) /// - 按住拖动变厚 + 出现 thumb,松手 seek 后恢复 class VideoPlayerView extends StatefulWidget { const VideoPlayerView({ super.key, required this.pool, required this.slotIndex, required this.coverUrl, required this.isCurrent, }); final FeedPlayerPool pool; final int slotIndex; final String coverUrl; /// 该页是否为当前页(暂停图标只在当前页显示,避免划走后旧页残留大图标) final bool isCurrent; @override State createState() => _VideoPlayerViewState(); } class _VideoPlayerViewState extends State { late final PlayerSlot _slot = widget.pool.slotAt(widget.slotIndex); Duration _position = Duration.zero; Duration _duration = Duration.zero; bool _seeking = false; double _seekValue = 0; // 拖动中的视觉值(0~1),拖动期间直接控制 painter @override void initState() { super.initState(); _slot.player.stream.position.listen((p) { if (mounted && !_seeking) setState(() => _position = p); }); _slot.player.stream.duration.listen((d) { if (mounted) setState(() => _duration = d); }); } double get _value { if (_seeking) return _seekValue; final total = _duration.inMilliseconds; if (total <= 0) return 0; final cur = _position.inMilliseconds.clamp(0, total); return cur / total; } void _onSeekStart(double v) { setState(() { _seeking = true; _seekValue = v; }); } void _onSeek(double v) { setState(() => _seekValue = v); } void _onSeekEnd(double v) { setState(() => _seeking = false); final totalMs = _duration.inMilliseconds; if (totalMs > 0) { _slot.player.seek(Duration(milliseconds: (v * totalMs).round())); } } @override Widget build(BuildContext context) { return Stack( fit: StackFit.expand, children: [ // 底层:纯画面(media_kit 官方 Video,无控制条) Video( controller: _slot.controller, controls: NoVideoControls, fit: BoxFit.contain, ), // 封面兜底:position==0 时显示(仅未渲染第一帧) StreamBuilder( stream: _slot.player.stream.position, initialData: _slot.player.state.position, builder: (_, snapshot) { final pos = snapshot.data ?? Duration.zero; final showCover = widget.coverUrl.isNotEmpty && pos == Duration.zero; return showCover ? Image.network( widget.coverUrl, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ) : const SizedBox.shrink(); }, ), // 暂停态大播放图标:只在当前页、且该槽位确实处于暂停态时显示 if (widget.isCurrent) IgnorePointer( child: StreamBuilder( stream: _slot.player.stream.playing, initialData: _slot.player.state.playing, builder: (_, snapshot) { final playing = snapshot.data ?? false; if (playing) return const SizedBox.shrink(); return const Center( child: Icon( Icons.play_arrow_rounded, size: 88, color: Colors.white70, ), ); }, ), ), // 抖音风格细进度条(贴底,常驻) Positioned( left: 0, right: 0, bottom: 0, child: _ThinProgressBar( value: _value, onChanged: _onSeek, onChangeStart: _onSeekStart, onChangeEnd: _onSeekEnd, ), ), ], ); } } class _ThinProgressBar extends StatefulWidget { const _ThinProgressBar({ required this.value, required this.onChanged, required this.onChangeStart, required this.onChangeEnd, }); final double value; final ValueChanged onChanged; final ValueChanged onChangeStart; final ValueChanged onChangeEnd; @override State<_ThinProgressBar> createState() => _ThinProgressBarState(); } class _ThinProgressBarState extends State<_ThinProgressBar> { bool _dragging = false; double _dragValue = 0; void _update(Offset local, double width) { final v = (local.dx / width).clamp(0.0, 1.0); _dragValue = v; widget.onChanged(v); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (_, constraints) { final w = constraints.maxWidth; return GestureDetector( behavior: HitTestBehavior.opaque, onHorizontalDragStart: (d) { setState(() => _dragging = true); widget.onChangeStart(widget.value); _update(d.localPosition, w); }, onHorizontalDragUpdate: (d) => _update(d.localPosition, w), onHorizontalDragEnd: (_) { setState(() => _dragging = false); widget.onChangeEnd(_dragValue.clamp(0.0, 1.0)); }, onTapDown: (d) { setState(() => _dragging = true); widget.onChangeStart(widget.value); _update(d.localPosition, w); }, onTapUp: (_) { setState(() => _dragging = false); widget.onChangeEnd(_dragValue.clamp(0.0, 1.0)); }, child: Container( height: 32, // 触摸热区 32px(视觉 2px,好点) alignment: Alignment.bottomCenter, child: CustomPaint( painter: _ProgressPainter( value: _dragging ? _dragValue : widget.value, dragging: _dragging, ), size: Size(w, _dragging ? 20 : 4), ), ), ); }, ); } } class _ProgressPainter extends CustomPainter { _ProgressPainter({required this.value, required this.dragging}); final double value; final bool dragging; @override void paint(Canvas canvas, Size size) { final h = dragging ? 3.0 : 2.0; final y = size.height / 2; final track = Paint() ..color = Colors.white.withValues(alpha: dragging ? 0.35 : 0.22) ..strokeCap = StrokeCap.round ..strokeWidth = h; canvas.drawLine(Offset(0, y), Offset(size.width, y), track); if (value > 0) { final played = Paint() ..color = Colors.white ..strokeCap = StrokeCap.round ..strokeWidth = h; canvas.drawLine(Offset(0, y), Offset(value * size.width, y), played); } if (dragging) { canvas.drawCircle( Offset(value * size.width, y), 6, Paint()..color = Colors.white..style = PaintingStyle.fill, ); } } @override bool shouldRepaint(covariant _ProgressPainter old) => value != old.value || dragging != old.dragging; }