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

如何解决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,
            ),
          ),
        ],
      ),
    );
  }
}

关键修改点

  1. 全屏页面新增本地状态维护:在FullScreenPlayer中添加_isStretched变量,支持在全屏内直接切换适配模式,无需返回小窗口修改。
  2. 优化布局逻辑:
    • 拉伸模式:使用BoxFit.fill直接让视频填满屏幕,彻底消除黑边。
    • 比例适配模式:先通过AspectRatio保持视频原始比例,再用BoxFit.cover让视频填充屏幕,仅裁剪超出部分,无变形无黑边。
  3. 新增交互按钮:添加退出全屏和切换模式的按钮,提升用户体验。

其他代码(无需修改)

首页代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 17:12:32