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

Flutter故事页全屏背景适配问题:iOS两侧出现白边

iOS设备Flutter沉浸式页面背景出现白边的解决方法

问题场景

开发故事页面时,背景使用图片或视频,通过SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive)实现全屏覆盖(包含状态栏),该方案在Android上表现正常,但iOS设备背景两侧出现白边。

问题截图:
iOS白边问题截图1
iOS白边问题截图2

原页面代码:

class StoriesPage extends StatefulWidget {
  final String title;
  final String text;
  final String backgroundMedia;
  final String? promoCode;
  final MediaTypes mediaType;
  const StoriesPage({
    Key? key,
    required this.title,
    required this.text,
    required this.backgroundMedia,
    this.promoCode,
    required this.mediaType,
  }) : super(key: key);

  @override
  State<StoriesPage> createState() => _StoriesPageState();
}

class _StoriesPageState extends State<StoriesPage> {
  late VideoPlayerController _videoController;

  @override
  void initState() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
    if (widget.mediaType == MediaTypes.video) {
      _videoController =
          VideoPlayerController.networkUrl(Uri.parse(widget.backgroundMedia))
            ..initialize().then((value) {
              _videoController.play();
              _videoController.setLooping(true);
              setState(() {});
            });
    }
    super.initState();
  }

  @override
  void dispose() {
    widget.mediaType == MediaTypes.video ? _videoController.dispose() : null;
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(children: [
      widget.mediaType == MediaTypes.image
          ? Container(
              decoration: BoxDecoration(
                  color: Colors.transparent,
                  image: DecorationImage(
                    image: NetworkImage(widget.backgroundMedia),
                    fit: BoxFit.cover,
                  )),
            )
          : _videoController.value.isInitialized
              ? SizedBox.expand(
                  child: FittedBox(
                    fit: BoxFit.cover,
                    child: SizedBox(
                      width: _videoController.value.size.width,
                      height: _videoController.value.size.height,
                      child: VideoPlayer(_videoController),
                    ),
                  ),
                )
              : Center(
                  child: CircularProgressIndicator(
                    color: context.colors.primary,
                  ),
                ),
      Container(
        padding: const EdgeInsets.all(16),
        width: double.infinity,
        decoration: BoxDecoration(
          gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                const Color.fromARGB(255, 6, 11, 17).withOpacity(0),
                const Color.fromARGB(60, 21, 44, 67).withOpacity(0.5),
                const Color.fromARGB(60, 10, 20, 31).withOpacity(1)
              ]),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            Text(
              widget.title,
              style: context.text.headline1.copyWith(
                  height: 1.21,
                  color: Colors.white,
                  fontWeight: FontWeight.w500,
                  fontSize: 34),
            ),
            const SizedBox(height: 16),
            Text(
              widget.text,
              style: context.text.body3.copyWith(color: Colors.white),
            ),
            widget.promoCode != null
                ? Column(
                    children: [
                      const SizedBox(height: 32),
                      SizedBox(
                        width: double.infinity,
                        child: ElevatedButton(
                            style: const ButtonStyle(
                                backgroundColor: MaterialStatePropertyAll(
                                    Colors.transparent),
                                elevation: MaterialStatePropertyAll(0),
                                side: MaterialStatePropertyAll(
                                    BorderSide(color: Colors.white))),
                            onPressed: () async {
                              await Clipboard.setData(
                                      ClipboardData(text: widget.promoCode!))
                                  .then((_) {
                                ScaffoldMessenger.of(context).showSnackBar(
                                    const SnackBar(
                                        content: Text('Скопировано')));
                              });
                            },
                            child: Text(widget.promoCode.toString())),
                      ),
                      const SizedBox(height: 10),
                      SizedBox(
                        width: double.infinity,
                        child: ElevatedButton(
                            onPressed: () async {
                              await Clipboard.setData(ClipboardData(
                                      text: widget.promoCode.toString()))
                                  .then((_) {
                                ScaffoldMessenger.of(context).showSnackBar(
                                    const SnackBar(
                                        content: Text('Скопировано')));
                              });
                            },
                            child: const Text('Скопировать')),
                      ),
                    ],
                  )
                : const SizedBox(),
            const SizedBox(height: 20),
          ],
        ),
      ),
      Padding(
        padding: const EdgeInsets.all(20),
        child: Align(
          alignment: Alignment.topRight,
          child: GestureDetector(
            onTap: () => Navigator.of(context).pop(),
            child: Container(
              height: 48,
              width: 48,
              decoration: const ShapeDecoration(
                  shape: CircleBorder(), color: Colors.white),
              child: const Padding(
                padding: EdgeInsets.all(8.0),
                child: Icon(
                  Icons.close,
                  size: 24,
                ),
              ),
            ),
          ),
        ),
      ),
    ]);
  }
}

问题原因

iOS的安全区域机制与Flutter页面默认布局约束冲突,导致背景无法完全延伸至屏幕边缘;同时SystemUiMode.immersive在iOS上需要配合额外的系统UI样式配置,才能彻底消除留白。

解决步骤及修改代码

1. 让Stack强制占满屏幕

给外层Stack添加fit: StackFit.expand属性,确保所有子Widget填充整个屏幕空间。

2. 优化iOS系统UI配置

在initState中添加SystemChrome.setSystemUIOverlayStyle,设置状态栏和导航栏为透明,让内容延伸至安全区域外。

3. 调整背景容器约束

给图片背景的Container设置宽高为double.infinity,并添加黑色 fallback 颜色,避免加载前出现白边。

修改后的关键代码片段:

@override
void initState() {
  super.initState();
  // 配置沉浸式模式+透明状态栏/导航栏,适配iOS
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
    statusBarColor: Colors.transparent,
    navigationBarColor: Colors.transparent,
    statusBarIconBrightness: Brightness.light,
    navigationBarIconBrightness: Brightness.light,
  ));
  
  if (widget.mediaType == MediaTypes.video) {
    _videoController = VideoPlayerController.networkUrl(Uri.parse(widget.backgroundMedia))
      ..initialize().then((value) {
        _videoController.play();
        _videoController.setLooping(true);
        setState(() {});
      });
  }
}

@override
Widget build(BuildContext context) {
  return Stack(
    fit: StackFit.expand, // 关键:让Stack占满整个屏幕
    children: [
      widget.mediaType == MediaTypes.image
          ? Container(
              width: double.infinity,
              height: double.infinity,
              decoration: BoxDecoration(
                color: Colors.black, // 加载前 fallback 颜色
                image: DecorationImage(
                  image: NetworkImage(widget.backgroundMedia),
                  fit: BoxFit.cover,
                ),
              ),
            )
          : _videoController.value.isInitialized
              ? SizedBox.expand(
                  child: FittedBox(
                    fit: BoxFit.cover,
                    child: SizedBox(
                      width: _videoController.value.size.width,
                      height: _videoController.value.size.height,
                      child: VideoPlayer(_videoController),
                    ),
                  ),
                )
              : Container(
                  color: Colors.black, // 加载时背景设为黑色
                  child: Center(
                    child: CircularProgressIndicator(
                      color: context.colors.primary,
                    ),
                  ),
                ),
      // 其余代码保持不变
    ],
  );
}

额外说明

  • 设置黑色 fallback 颜色是为了避免图片/视频加载完成前出现白边;
  • SystemUiOverlayStyle中的图标亮度可根据背景颜色调整,保证可读性;
  • dispose中恢复系统UI模式的逻辑无需修改。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 05:13:15