You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web场景下基于滚动值控制视频播放进度的实现问题

Flutter Web 滚动控制视频进度实现方案

方案1:基于ScrollController实现(推荐,原生支持所有滚动手势)

你原来的思路完全可行,只需要给滚动组件增加透明占位内容即可生成滚动偏移,具体实现逻辑如下:

  • 滚动组件选择SingleChildScrollView,绑定你创建的ScrollController
  • SingleChildScrollView的子组件使用Stack,第一层放置全屏视频组件,第二层放置透明占位SizedBox,高度设置为「屏幕高度 + 自定义总滚动行程」(总滚动行程可自行设置,比如滚动1000px对应播放完整段视频)
  • 监听ScrollController的偏移变化,将偏移量按比例映射为视频进度,调用seekTo调整播放位置

核心代码示例:

import 'package:video_player/video_player.dart';
import 'package:flutter/material.dart';

class ScrollControlVideoPage extends StatefulWidget {
  const ScrollControlVideoPage({super.key});

  @override
  State<ScrollControlVideoPage> createState() => _ScrollControlVideoPageState();
}

class _ScrollControlVideoPageState extends State<ScrollControlVideoPage> {
  late ScrollController _scrollController;
  late VideoPlayerController _videoController;
  Timer? _seekTimer;
  // 自定义总滚动行程:滚动1000px对应播放完完整视频
  final double _totalScrollDistance = 1000;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    // 初始化视频控制器,换成你自己的视频资源地址
    _videoController = VideoPlayerController.asset('assets/demo_video.mp4')
      ..initialize().then((_) {
        setState(() {});
        // 视频加载完成后再监听滚动事件
        _scrollController.addListener(_handleScroll);
      });
  }

  void _handleScroll() {
    if (!_videoController.value.isInitialized) return;
    // 滚动偏移映射为视频进度
    final progress = _scrollController.offset / _totalScrollDistance;
    // 进度边界处理
    final clampedProgress = progress.clamp(0.0, 1.0);
    final targetPos = _videoController.value.duration * clampedProgress;
    // 加防抖,避免频繁seek导致卡顿
    _seekTimer?.cancel();
    _seekTimer = Timer(const Duration(milliseconds: 200), () {
      _videoController.seekTo(targetPos);
    });
  }

  @override
  Widget build(BuildContext context) {
    final screenSize = MediaQuery.of(context).size;
    return Scaffold(
      body: ScrollConfiguration(
        // 隐藏默认滚动条
        behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
        child: SingleChildScrollView(
          controller: _scrollController,
          // 强制开启滚动,不受内容高度限制
          physics: const AlwaysScrollableScrollPhysics(),
          child: Stack(
            children: [
              // 第一层:全屏视频
              SizedBox(
                width: screenSize.width,
                height: screenSize.height,
                child: VideoPlayer(_videoController),
              ),
              // 第二层:透明占位,撑开滚动高度
              Opacity(
                opacity: 0,
                child: SizedBox(
                  width: screenSize.width,
                  height: screenSize.height + _totalScrollDistance,
                ),
              )
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _scrollController.dispose();
    _videoController.dispose();
    _seekTimer?.cancel();
    super.dispose();
  }
}

方案2:基于GestureDetector垂直拖拽实现

该方案无需引入滚动组件,更轻量,适合仅需拖拽控制的场景,需要额外适配鼠标滚轮事件:

  • 用GestureDetector包裹全屏视频组件
  • 监听onVerticalDragUpdate回调,累计垂直拖拽偏移量,按比例映射为视频进度
  • 如需支持鼠标滚轮,可额外监听PointerScrollEvent处理滚轮偏移

核心代码示例:

class DragControlVideoPage extends StatefulWidget {
  const DragControlVideoPage({super.key});

  @override
  State<DragControlVideoPage> createState() => _DragControlVideoPageState();
}

class _DragControlVideoPageState extends State<DragControlVideoPage> {
  late VideoPlayerController _videoController;
  Timer? _seekTimer;
  double _totalDragOffset = 0;
  // 自定义总拖拽行程:拖拽1000px对应播放完完整视频
  final double _totalDragDistance = 1000;

  @override
  void initState() {
    super.initState();
    _videoController = VideoPlayerController.asset('assets/demo_video.mp4')
      ..initialize().then((_) => setState(() {}));
  }

  void _updateProgress(double offsetDelta) {
    if (!_videoController.value.isInitialized) return;
    // 向上拖拽/滚动对应进度增加,所以偏移量做减法
    _totalDragOffset -= offsetDelta;
    // 偏移边界处理
    _totalDragOffset = _totalDragOffset.clamp(0, _totalDragDistance);
    // 映射为视频进度
    final progress = _totalDragOffset / _totalDragDistance;
    final targetPos = _videoController.value.duration * progress;
    // 防抖seek
    _seekTimer?.cancel();
    _seekTimer = Timer(const Duration(milliseconds: 200), () {
      _videoController.seekTo(targetPos);
    });
  }

  @override
  Widget build(BuildContext context) {
    final screenSize = MediaQuery.of(context).size;
    return Scaffold(
      body: Listener(
        // 监听鼠标滚轮事件
        onPointerSignal: (event) {
          if (event is PointerScrollEvent) {
            _updateProgress(event.scrollDelta.dy);
          }
        },
        child: GestureDetector(
          // 监听触摸垂直拖拽事件
          onVerticalDragUpdate: (details) => _updateProgress(details.delta.dy),
          child: SizedBox(
            width: screenSize.width,
            height: screenSize.height,
            child: VideoPlayer(_videoController),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _videoController.dispose();
    _seekTimer?.cancel();
    super.dispose();
  }
}

注意事项

  • 两种方案都需要依赖video_player插件,注意按照插件文档完成Web端的配置
  • 防抖时长可根据实际需求调整,建议设置在100~300ms区间,平衡操作跟手性和性能
  • 如果需要支持视频循环播放,可在进度超过100%时重置偏移量,实现滚动循环控制

内容的提问来源于stack exchange,提问作者Rohan Patil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 13:06:03