Flutter仿TikTok竖屏滑动页 离开页面视频后台播放问题修复
问题根源
PageView默认会缓存当前页前后各1个子组件,滑动切页时,离开页面的VideoPlayerContent不会立即触发dispose,原有自动播放逻辑在initState执行后没有后续状态控制,视频会持续在后台播放。- 原有代码没有监听
PageView的页面切换事件,无法在页面滑出可视区域时主动触发暂停、滑入时触发播放。 - 原有代码没有监听应用生命周期,当Reels页面被压入后台(跳转其他页面、按Home键返回桌面)时,视频不会自动暂停。
- 附带代码bug:
showDialogueBox方法调用时第一个参数传入了BuildContext类型而非上下文实例,会触发语法报错。
修复步骤
1. 改造Reels主页面
把PageController从build方法提升到State层级,新增当前激活页索引变量,给控制器添加页面滑动监听,切页时更新索引并触发组件刷新。
修改后的_ReelsState代码如下:
class _ReelsState extends State<Reels> { // 提升PageController到State层级,避免build时重复创建 late final PageController _pageController; // 记录当前激活的视频页索引 int _currentActivePage = 0; @override void initState() { super.initState(); _pageController = PageController(viewportFraction: 1, initialPage: 0); // 监听页面滑动事件 _pageController.addListener(() { final page = _pageController.page?.round() ?? 0; if (page != _currentActivePage) { setState(() { _currentActivePage = page; }); } }); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( automaticallyImplyLeading: false, elevation: 0, flexibleSpace: SizedBox( height: 200, child: Padding( padding: const EdgeInsets.all(15.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton(onPressed: () => showDialogueBox(context), // 修复原代码传BuildContext类型的bug icon: const Icon(Icons.add), iconSize: 35, color: Colors.teal[300], ) ], ), ), ), backgroundColor: Colors.transparent, ), body: StreamBuilder( stream: fireStore.collection("videoReels").snapshots(), builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) { if (!snapshot.hasData) return const Center(child: CircularProgressIndicator()); return PageView.builder( itemCount: snapshot.data!.docs.length, scrollDirection: Axis.vertical, controller: _pageController, // 使用State中定义的控制器 itemBuilder: (context, index){ DocumentSnapshot dataSnapshot = snapshot.data!.docs[index]; return Stack( alignment: Alignment.bottomCenter, children: [ // 传入当前页是否激活的标记 VideoPlayerContent( videoUrl: dataSnapshot["videoUrl"], isActive: index == _currentActivePage, ), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( flex: 3, child: Container( height: MediaQuery.of(context).size.height/6, padding: const EdgeInsets.all(10.0), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( children: [ InkWell( onTap: () => {}, child: const Icon(Icons.thumb_up, color: Colors.blueAccent,), ), const SizedBox(width: 30,), InkWell( onTap: () => {}, child: const Icon(Icons.favorite, color: Colors.red,), ) ], ), const SizedBox(width: 20,), InkWell( onTap: () => {}, child: Text(" Add Comment here... ", style: TextStyle( color: Colors.grey[500] ),), ) ], ), const SizedBox(height: 30,), Padding( padding: const EdgeInsets.fromLTRB(30.0, 4.0,30.0,4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ InkWell( onTap: () => { } , child:ClipRRect( borderRadius: BorderRadius.circular(50.0), child: CircleAvatar(backgroundImage: NetworkImage(dataSnapshot['avatar']), radius: 25,), ), ), Text((dataSnapshot["videoUrl"])), ElevatedButton(onPressed: ()=> {}, child: const Text("Subscribe")) ], ), ) ], ), ), ), ], ) ], ); }, ); } ), ); } }
2. 改造VideoPlayerContent组件
新增isActive入参,在组件状态更新时判断当前是否为激活页,自动控制播放/暂停;同时添加应用生命周期监听,App退到后台时自动暂停视频。
修改后的代码如下:
import 'package:app/packageManager/package.dart'; class VideoPlayerContent extends StatefulWidget { final String videoUrl; // 新增是否为当前激活页的参数 final bool isActive; const VideoPlayerContent({Key? key, required this.videoUrl, required this.isActive}) : super(key: key); @override State<VideoPlayerContent> createState() => _VideoPlayerContentState(); } // 添加WidgetsBindingObserver监听应用生命周期 class _VideoPlayerContentState extends State<VideoPlayerContent> with WidgetsBindingObserver { late VideoPlayerController _videoController; late Future _initializeVideoPlayer; @override void initState(){ super.initState(); WidgetsBinding.instance.addObserver(this); _videoController = VideoPlayerController.network(widget.videoUrl); _initializeVideoPlayer = _videoController.initialize().then((_) { // 初始化完成后如果是当前激活页才播放 if (widget.isActive) { _videoController.play(); } _videoController.setVolume(1); _videoController.setLooping(true); setState(() {}); }); } // 监听组件配置更新,切页时触发 @override void didUpdateWidget(covariant VideoPlayerContent oldWidget) { super.didUpdateWidget(oldWidget); // 页面激活状态变化时控制播放暂停 if (oldWidget.isActive != widget.isActive) { if (widget.isActive) { _videoController.play(); } else { _videoController.pause(); } } } // 监听应用生命周期变化 @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); // 应用退到后台、不可见时暂停视频 if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive || state == AppLifecycleState.detached) { _videoController.pause(); } // 应用回到前台且当前是激活页时恢复播放 if (state == AppLifecycleState.resumed && widget.isActive) { _videoController.play(); } } @override void dispose (){ WidgetsBinding.instance.removeObserver(this); _videoController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, child: FutureBuilder( future: _initializeVideoPlayer, builder: (context, snapshot){ if (snapshot.connectionState == ConnectionState.done){ return VideoPlayer(_videoController); }else{ return Container( color: Colors.black, child: const Center( child: CircularProgressIndicator( value: 0.8, valueColor: AlwaysStoppedAnimation<Color>(Colors.purpleAccent), ), ) ); } }, ), ); } }
额外优化建议
- 如果需要减少内存占用,可以给
PageView.builder添加cacheExtent: 0参数,禁用前后页缓存,滑走的页面会立即dispose释放播放器资源,注意这个配置会让滑到下一个视频时需要重新初始化加载,可根据需求取舍。 - 可以给视频添加点击暂停/播放的交互,只需要在VideoPlayer外层套
GestureDetector,点击时判断_controller.value.isPlaying执行pause/play即可。
内容的提问来源于stack exchange,提问作者Olaniyi Philip
相关产品推荐
相关产品推荐

