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, ), ], ); } }
关键修改说明
全局缓存机制:
- 使用
_videoCacheMap存储每个视频URL的控制器和播放进度,避免滚动时销毁控制器 _VideoCacheItem类封装控制器与进度,方便统一管理
- 使用
可见度监听逻辑优化:
- 可见度低于30%时仅暂停视频并记录进度,不再销毁控制器
- 可见度超过70%时自动触发播放,确保进入视野的视频自动播放
播放进度恢复:
- 初始化控制器时优先从缓存获取,并通过
seekTo恢复上次播放位置 - 暂停视频时立即更新缓存中的进度值
- 初始化控制器时优先从缓存获取,并通过
活跃控制器管理:
- 切换播放时自动暂停当前活跃的其他视频,确保同一时间仅一个视频播放
- 严格维护
_activeController的状态,避免多视频同时播放
内存优化建议:
- 可根据需求添加缓存数量限制,比如最多缓存5个视频控制器,超出时销毁最早的控制器
- 可结合
ScrollController监听滚动速度,避免快速滚动时频繁触发播放/暂停
内容的提问来源于stack exchange,提问作者Mohsin Naqvi
相关产品推荐
相关产品推荐

