视频播放测试

5 阅读5分钟
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(
      // appBar: const CustomTitleBar(title: '视频'),
      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';

/// 展示底部弹出抽屉时缩放盒子 Demo 页面
/// 功能:点击背景视频/图片,弹出底部抽屉;拖动抽屉时,背景页面会随之缩放。
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();

  /// 用于动态修改抽屉高度的 Stream 控制器 (在 showModalBottomSheet 内部使用)
  StreamController<double> myStreamController = StreamController<double>.broadcast();

  /// 用于控制背景页面缩放高度的 Stream 控制器
  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(() {
      // 动画进度从 0 到 1,计算当前对应的背景偏移高度
      final value = myAnimationController.value * drawerH;
      // 通过 Stream 通知背景页面进行缩放更新
      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;
    // 定义抽屉高度 (屏幕高度减去顶部预留的 300)
    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),
      // 设置为 true 以允许高度超过屏幕一半
      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');
    }
  }

  /// 监听控制器变化,主要用于同步 UI (如当前播放时间)
  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);
  }

  /// 格式化时间显示 (00:00)
  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: [
          // 进度条 (使用 Slider 以获得更好的拖动体验)
          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),
            ),
          ),
        ],
      ),
    );
  }
}