如何解决Flutter中video_player全屏播放时的黑边问题?
Flutter video_player 全屏黑边问题解决指南
问题说明
使用video_player包播放视频时,切换全屏模式后屏幕两侧或上下出现黑边,需要让视频完全填充屏幕,支持拉伸或按比例适配两种方式。

已尝试方案
BoxFit.cover:缩放视频填充屏幕,但会裁剪部分内容BoxFit.fill:拉伸视频消除黑边,但画面会变形- 对比视频与屏幕尺寸:确认是宽高比不匹配导致黑边
- 切换
BoxFit.fill/BoxFit.contain:部分场景仍存在黑边
解决方案
核心问题在于全屏播放器的布局逻辑错误,当前代码强制用屏幕尺寸包裹视频,导致适配逻辑失效。以下是修复后的完整代码及关键修改:
修复后的全屏播放器代码
import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import 'package:flutter/services.dart'; class CustomVideoPlayer extends StatefulWidget { const CustomVideoPlayer({super.key}); @override _CustomVideoPlayerState createState() => _CustomVideoPlayerState(); } class _CustomVideoPlayerState extends State<CustomVideoPlayer> { late VideoPlayerController _controller; bool isPlaying = false; bool isStretched = false; @override void initState() { super.initState(); _controller = VideoPlayerController.asset("assets/videos/sample.mp4") ..initialize().then((_) { setState(() {}); }); } @override void dispose() { _controller.dispose(); super.dispose(); } void togglePlayPause() { setState(() { if (isPlaying) { _controller.pause(); } else { _controller.play(); } isPlaying = !isPlaying; }); } void toggleStretch() { setState(() { isStretched = !isStretched; }); } void toggleFullScreen() async { if (_controller.value.isPlaying) { _controller.pause(); } await Navigator.push( context, MaterialPageRoute( builder: (context) => FullScreenPlayer(controller: _controller, isStretched: isStretched), ), ); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); } @override Widget build(BuildContext context) { return Column( children: [ _controller.value.isInitialized ? SizedBox( width: double.infinity, height: 250, child: FittedBox( fit: isStretched ? BoxFit.fill : BoxFit.cover, child: SizedBox( width: _controller.value.size.width, height: _controller.value.size.height, child: VideoPlayer(_controller), ), ), ) : const CircularProgressIndicator(), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), onPressed: togglePlayPause, ), IconButton( icon: const Icon(Icons.fullscreen), onPressed: toggleFullScreen, ), IconButton( icon: Icon(isStretched ? Icons.aspect_ratio : Icons.fit_screen), onPressed: toggleStretch, ), ], ), ], ); } } class FullScreenPlayer extends StatefulWidget { final VideoPlayerController controller; final bool isStretched; const FullScreenPlayer({super.key, required this.controller, required this.isStretched}); @override _FullScreenPlayerState createState() => _FullScreenPlayerState(); } class _FullScreenPlayerState extends State<FullScreenPlayer> { late bool _isStretched; @override void initState() { super.initState(); _isStretched = widget.isStretched; SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); widget.controller.addListener(() { if (mounted) setState(() {}); }); widget.controller.play(); } @override void dispose() { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); super.dispose(); } void _toggleStretch() { setState(() { _isStretched = !_isStretched; }); } @override Widget build(BuildContext context) { if (!widget.controller.value.isInitialized) { return const Scaffold( backgroundColor: Colors.black, body: Center( child: CircularProgressIndicator(color: Colors.white), ), ); } final videoAspectRatio = widget.controller.value.aspectRatio; final screenSize = MediaQuery.of(context).size; return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ Positioned.fill( child: _isStretched ? // 拉伸填充,无黑边(可能变形) FittedBox( fit: BoxFit.fill, child: SizedBox( width: widget.controller.value.size.width, height: widget.controller.value.size.height, child: VideoPlayer(widget.controller), ), ) : // 按比例填充,无变形(裁剪超出部分) AspectRatio( aspectRatio: videoAspectRatio, child: FittedBox( fit: BoxFit.cover, child: SizedBox( width: screenSize.width, height: screenSize.height, child: VideoPlayer(widget.controller), ), ), ), ), // 退出全屏按钮 Positioned( top: 20, right: 20, child: IconButton( icon: const Icon(Icons.close, color: Colors.white, size: 30), onPressed: () => Navigator.pop(context), ), ), // 全屏内切换适配模式按钮 Positioned( bottom: 20, right: 20, child: IconButton( icon: Icon(_isStretched ? Icons.aspect_ratio : Icons.fit_screen, color: Colors.white, size: 30), onPressed: _toggleStretch, ), ), ], ), ); } }
关键修改点
- 全屏页面新增本地状态维护:在
FullScreenPlayer中添加_isStretched变量,支持在全屏内直接切换适配模式,无需返回小窗口修改。 - 优化布局逻辑:
- 拉伸模式:使用
BoxFit.fill直接让视频填满屏幕,彻底消除黑边。 - 比例适配模式:先通过
AspectRatio保持视频原始比例,再用BoxFit.cover让视频填充屏幕,仅裁剪超出部分,无变形无黑边。
- 拉伸模式:使用
- 新增交互按钮:添加退出全屏和切换模式的按钮,提升用户体验。
其他代码(无需修改)
首页代码
import 'package:flutter/material.dart'; import 'package:livestream_project/pages/video_player/player_code.dart'; // 按实际路径修改 class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: const Center( child: CustomVideoPlayer(), ), ); } }
main.dart
import 'package:flutter/material.dart'; import 'pages/homepage_page.dart'; // 按实际路径修改 void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Live Stream Player', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const HomePage(), ); } }
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:label="livestream_project" android:name="${applicationName}" android:icon="@mipmap/ic_launcher"> <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <meta-data android:name="flutterEmbedding" android:value="2" /> </application> <queries> <intent> <action android:name="android.intent.action.PROCESS_TEXT"/> <data android:mimeType="text/plain"/> </intent> </queries> </manifest>
内容的提问来源于stack exchange,提问作者shubham goyal
相关产品推荐
相关产品推荐

