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

Flutter实现带滚动自动播放/暂停的多类型Feed列表(缓存优化)

Flutter实现Facebook式Feed视频自动播放/暂停(保留缓存与播放进度)

问题分析

当前代码的核心问题是视频可见度低于0.3时直接销毁了CachedVideoPlayerController,导致滚动返回后需要重新初始化加载;同时缺乏对播放进度的持久化存储,无法从上次暂停位置继续播放。

解决方案

通过全局缓存控制器与播放进度、调整可见度监听逻辑解决问题,核心改动如下:

  • 用全局Map缓存每个视频URL对应的控制器与播放进度
  • 可见度不足时仅暂停视频并记录进度,不销毁控制器
  • 初始化时优先从缓存获取控制器,恢复上次播放进度
  • 严格管理活跃控制器,确保同一时间仅播放一个视频

修改后的完整代码

import 'dart:async';
import 'package:cached_video_player/cached_video_player.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:visibility_detector/visibility_detector.dart';

// 全局缓存:存储每个视频URL对应的控制器与播放进度
final Map<String, _VideoCacheItem> _videoCache = {};
CachedVideoPlayerController? _activeController;

class _VideoCacheItem {
  final CachedVideoPlayerController controller;
  double playbackPosition = 0.0;

  _VideoCacheItem({required this.controller});
}

class FeedVideoPlayer extends StatefulWidget {
  final String videoUrl;

  const FeedVideoPlayer({
    Key? key,
    required this.videoUrl,
  }) : super(key: key);

  @override
  _FeedVideoPlayerState createState() => _FeedVideoPlayerState();
}

class _FeedVideoPlayerState extends State<FeedVideoPlayer> {
  CachedVideoPlayerController? _videoController;
  bool _isPlaying = false;
  bool _isControllerReady = false;
  final UniqueKey _visibilityKey = UniqueKey();

  @override
  void initState() {
    super.initState();
    _initVideoController();
  }

  Future<void> _initVideoController() async {
    // 先尝试从缓存获取控制器
    if (_videoCache.containsKey(widget.videoUrl)) {
      final cacheItem = _videoCache[widget.videoUrl]!;
      _videoController = cacheItem.controller;
      // 恢复上次播放进度
      if (cacheItem.playbackPosition > 0) {
        await _videoController!.seekTo(Duration(seconds: cacheItem.playbackPosition.toInt()));
      }
      _isControllerReady = true;
    } else {
      // 缓存中没有则创建新控制器
      _videoController = CachedVideoPlayerController.network(widget.videoUrl);
      await _videoController!.initialize();
      _videoController!.setLooping(true);
      _videoCache[widget.videoUrl] = _VideoCacheItem(controller: _videoController!);
      _isControllerReady = true;
    }
    setState(() {});
  }

  @override
  void dispose() {
    // 仅在全局缓存清理时销毁控制器(可添加内存限制逻辑,比如缓存数量阈值)
    super.dispose();
  }

  // 切换播放状态,确保同一时间仅一个视频播放
  Future<void> _togglePlay() async {
    if (_videoController == null) return;

    if (_videoController!.value.isPlaying) {
      await _videoController!.pause();
      _updatePlaybackPosition();
      setState(() => _isPlaying = false);
      if (_activeController == _videoController) {
        _activeController = null;
      }
    } else {
      // 暂停当前活跃的其他视频
      if (_activeController != null && _activeController != _videoController) {
        await _activeController!.pause();
        // 更新活跃视频的播放进度
        final activeCacheItem = _videoCache.values.firstWhere((item) => item.controller == _activeController);
        activeCacheItem.playbackPosition = _activeController!.value.position.inSeconds.toDouble();
        (_activeController!.value as dynamic).notifyListeners();
      }

      await _videoController!.play();
      setState(() => _isPlaying = true);
      _activeController = _videoController;
    }
  }

  // 更新当前视频的播放进度到缓存
  void _updatePlaybackPosition() {
    if (_videoController == null) return;
    final cacheItem = _videoCache[widget.videoUrl];
    if (cacheItem != null) {
      cacheItem.playbackPosition = _videoController!.value.position.inSeconds.toDouble();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 20.0),
      child: VisibilityDetector(
        key: _visibilityKey,
        onVisibilityChanged: (VisibilityInfo info) async {
          if (_videoController == null) return;

          // 可见度超过70%时自动播放
          if (info.visibleFraction > 0.7 && !_videoController!.value.isPlaying) {
            await _togglePlay();
          }
          // 可见度低于30%时自动暂停
          else if (info.visibleFraction < 0.3 && _videoController!.value.isPlaying) {
            await _videoController!.pause();
            _updatePlaybackPosition();
            setState(() => _isPlaying = false);
            if (_activeController == _videoController) {
              _activeController = null;
            }
          }
        },
        child: GestureDetector(
          onTap: _togglePlay,
          child: _buildVideoContent(),
        ),
      ),
    );
  }

  Widget _buildVideoContent() {
    if (!_isControllerReady || _videoController == null) {
      return const AspectRatio(
        aspectRatio: 4 / 3,
        child: ColoredBox(
          color: Colors.black,
          child: Center(child: CircularProgressIndicator()),
        ),
      );
    }

    return Stack(
      alignment: Alignment.center,
      children: [
        AspectRatio(
          aspectRatio: 4 / 3,
          child: CachedVideoPlayer(_videoController!),
        ),
        if (!_isPlaying)
          const Icon(
            CupertinoIcons.play_arrow_solid,
            color: Colors.white70,
            size: 54,
          ),
      ],
    );
  }
}

关键修改说明

  1. 全局缓存机制:

    • 使用_videoCache Map存储每个视频URL的控制器和播放进度,避免滚动时销毁控制器
    • _VideoCacheItem类封装控制器与进度,方便统一管理
  2. 可见度监听逻辑优化:

    • 可见度低于30%时仅暂停视频并记录进度,不再销毁控制器
    • 可见度超过70%时自动触发播放,确保进入视野的视频自动播放
  3. 播放进度恢复:

    • 初始化控制器时优先从缓存获取,并通过seekTo恢复上次播放位置
    • 暂停视频时立即更新缓存中的进度值
  4. 活跃控制器管理:

    • 切换播放时自动暂停当前活跃的其他视频,确保同一时间仅一个视频播放
    • 严格维护_activeController的状态,避免多视频同时播放
  5. 内存优化建议:

    • 可根据需求添加缓存数量限制,比如最多缓存5个视频控制器,超出时销毁最早的控制器
    • 可结合ScrollController监听滚动速度,避免快速滚动时频繁触发播放/暂停

内容的提问来源于stack exchange,提问作者Mohsin Naqvi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 17:08:13