Flutter Edge-to-Edge 介绍及适配使用指南

一、什么是 Edge-to--Edge?

Edge-to-Edge,中文可以理解为边到边显示沉浸式边缘布局

在传统 Android 应用中,状态栏和底部导航栏通常会占用固定区域,App 内容不会绘制到这些系统栏后面。

而 Edge-to-Edge 模式下,App 的内容可以延伸到整个屏幕,包括状态栏和导航栏背后区域,从视觉上看更加沉浸、现代,也更接近 iOS 的全屏体验。

简单理解:

传统模式:

复制代码
状态栏
----------------
App 内容区域
----------------
导航栏

Edge-to-Edge:

App 内容从屏幕顶部一直延伸到底部

状态栏 / 导航栏浮在 App 内容之上

从 Android 15 开始,如果应用 targetSdkVersion 达到 35,系统会默认强制启用 Edge-to-Edge;Android 16 起,官方已经不再允许通过旧的 opt-out 方式退出 Edge-to-Edge。Flutter 官方也已经将 SystemUiMode 默认行为调整到 Edge-to-Edge 方向。


二、为什么 Flutter 项目必须适配 Edge-to-Edge?

以前很多 Flutter 项目可能会这样写:

dart 复制代码
Scaffold(
  appBar: AppBar(title: const Text('首页')),
  body: PageContent(),
  bottomNavigationBar: BottomNavigationBar(...),
)

在旧 Android 版本上,这样通常没问题,因为系统会自动帮你预留状态栏和导航栏区域。

但在 Android 15+ 的 Edge-to-Edge 模式下,App 内容会绘制到系统栏下面。如果没有正确处理安全区域,可能会出现:

  1. 顶部标题被状态栏遮挡
  2. 底部按钮被系统导航栏遮挡
  3. BottomNavigationBar 和手势导航条重叠
  4. 页面底部输入框、提交按钮无法点击
  5. 弹窗、BottomSheet 底部间距异常
  6. 全屏图片、视频页面沉浸式效果不一致

Android 官方明确说明,target SDK 35 或更高的应用在 Android 15+ 设备上会默认 Edge-to-Edge,开发者需要主动处理 system bars、display cutout 和 system gesture 等 inset,避免内容被系统 UI 遮挡。


三、Flutter 中如何开启 Edge-to-Edge?

在 Flutter 中,可以通过 SystemChrome.setEnabledSystemUIMode 开启 Edge-to-Edge。

一般建议在 main() 中设置:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemChrome.setEnabledSystemUIMode(
    SystemUiMode.edgeToEdge,
  );
  runApp(const MyApp());
}

如果你还需要设置状态栏和导航栏颜色,可以配合 SystemChrome.setSystemUIOverlayStyle 使用:

dart 复制代码
SystemChrome.setSystemUIOverlayStyle(
  const SystemUiOverlayStyle(
    statusBarColor: Colors.transparent,
    systemNavigationBarColor: Colors.transparent,
    statusBarIconBrightness: Brightness.dark,
    systemNavigationBarIconBrightness: Brightness.dark,
  ),
);

完整示例:

dart 复制代码
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemChrome.setEnabledSystemUIMode(
    SystemUiMode.edgeToEdge,
  );
  SystemChrome.setSystemUIOverlayStyle(
    const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      systemNavigationBarColor: Colors.transparent,
      statusBarIconBrightness: Brightness.dark,
      systemNavigationBarIconBrightness: Brightness.dark,
    ),
  );
  runApp(const MyApp());
}

需要注意的是,如果你的 App 已经 target Android 15 或更高版本,即使你不主动调用这段代码,在 Android 15+ 上也可能已经默认进入 Edge-to-Edge 行为。Flutter 官方说明,从 Flutter 3.27 开始,默认 target SDK 升级到 Android 15 后,项目会受到 Edge-to-Edge 默认行为影响。


四、适配核心:SafeArea 和 MediaQuery

Edge-to-Edge 的关键不是「开启」,而是「处理遮挡区域」。

Flutter 中主要有两个常用方式:

  1. SafeArea
  2. MediaQuery.padding / MediaQuery.viewPadding / MediaQuery.viewInsets

五、使用 SafeArea 处理基础页面

最简单的适配方式是使用 SafeArea

dart 复制代码
Scaffold(
  body: SafeArea(
    child: Column(
      children: [
        Text('标题'),
        Expanded(
          child: ListView(
            children: [
              // 页面内容
            ],
          ),
        ),
      ],
    ),
  ),
);

SafeArea 会自动根据设备状态栏、刘海屏、底部导航栏等区域给子组件添加合适的 padding。

适合场景:

  1. 普通页面
  2. 表单页面
  3. 列表页面
  4. 有固定顶部标题的页面
  5. 有底部按钮的页面

但 SafeArea 并不是所有场景都适合。例如全屏图片、视频播放、沉浸式首页 Banner 等页面,通常希望背景延伸到状态栏后面,只保护文字、按钮等关键内容即可。


六、顶部适配:状态栏区域

1. 普通页面写法

dart 复制代码
Scaffold(
  body: SafeArea(
    bottom: false,
    child: Column(
      children: [
        _Header(),
        Expanded(child: _Content()),
      ],
    ),
  ),
);

这里 bottom: false 表示只处理顶部安全区域,不处理底部。

适合顶部有标题栏,但底部由其他组件单独处理的页面。

2. 自定义 AppBar 适配

如果你不用 Flutter 自带的 AppBar,而是自己写顶部导航栏,可以这样处理:

dart 复制代码
class CustomAppBar extends StatelessWidget {
  const CustomAppBar({super.key});

  @override
  Widget build(BuildContext context) {
    final topPadding = MediaQuery.paddingOf(context).top;
    return Container(
      padding: EdgeInsets.only(top: topPadding),
      height: topPadding + 56,
      alignment: Alignment.center,
      child: const Text(
        '首页',
        style: TextStyle(
          fontSize: 18,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}

这样顶部高度会自动包含状态栏区域。


七、底部适配:导航栏和底部按钮

Edge-to-Edge 最容易出问题的地方通常是底部。

例如:

dart 复制代码
Scaffold(
  body: ListView(...),
  bottomNavigationBar: Container(
    height: 56,
    child: Text('底部导航'),
  ),
);

在 Android 手势导航或三键导航下,底部区域可能会被系统导航栏遮挡。

推荐写法:

dart 复制代码
class BottomActionBar extends StatelessWidget {
  const BottomActionBar({super.key});

  @override
  Widget build(BuildContext context) {
    final bottomPadding = MediaQuery.paddingOf(context).bottom;
    return Container(
      padding: EdgeInsets.only(
        left: 16,
        right: 16,
        top: 12,
        bottom: bottomPadding + 12,
      ),
      child: SizedBox(
        height: 48,
        width: double.infinity,
        child: ElevatedButton(
          onPressed: () {},
          child: const Text('提交'),
        ),
      ),
    );
  }
}

这样按钮不会贴到底部,也不会被导航栏挡住。


八、Scaffold 的推荐适配方式

1. 普通页面

dart 复制代码
Scaffold(
  body: SafeArea(
    child: ListView(
      children: const [
        Text('页面内容'),
      ],
    ),
  ),
);

适合首页、详情页、图片头图等场景。

dart 复制代码
Scaffold(
  extendBodyBehindAppBar: true,
  body: Stack(
    children: [
      Image.network(
        'https://example.com/banner.png',
        width: double.infinity,
        height: 260,
        fit: BoxFit.cover,
      ),
      SafeArea(
        child: Column(
          children: [
            _CustomHeader(),
            Expanded(child: _Content()),
          ],
        ),
      ),
    ],
  ),
);

这里背景图可以延伸到状态栏后面,但标题、返回按钮等关键元素仍然通过 SafeArea 保护。

3. 底部导航页面

dart 复制代码
Scaffold(
  body: const _HomeContent(),
  bottomNavigationBar: SafeArea(
    top: false,
    child: BottomNavigationBar(
      currentIndex: 0,
      items: const [
        BottomNavigationBarItem(
          icon: Icon(Icons.home),
          label: '首页',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.person),
          label: '我的',
        ),
      ],
    ),
  ),
);

如果你的底部导航栏有自定义背景色、圆角、悬浮效果,可以用 MediaQuery.paddingOf(context).bottom 手动加底部间距。


九、输入框页面适配键盘

除了状态栏和导航栏,输入框页面还要考虑键盘弹起。

键盘高度可以通过:
MediaQuery.viewInsetsOf(context).bottom 获取。

示例:

dart 复制代码
class ChatInputBar extends StatelessWidget {
  const ChatInputBar({super.key});

  @override
  Widget build(BuildContext context) {
    final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
    final bottomPadding = MediaQuery.paddingOf(context).bottom;
    return AnimatedPadding(
      duration: const Duration(milliseconds: 200),
      curve: Curves.easeOut,
      padding: EdgeInsets.only(
        bottom: bottomInset > 0 ? bottomInset : bottomPadding,
      ),
      child: Container(
        padding: const EdgeInsets.all(12),
        child: const TextField(
          decoration: InputDecoration(
            hintText: '请输入内容',
          ),
        ),
      ),
    );
  }
}

这里的逻辑是:

  • 键盘弹出:使用 viewInsets.bottom
  • 键盘收起:使用安全区 bottom padding

适合聊天页、评论页、搜索页、登录注册页。


十、BottomSheet 适配

BottomSheet 也是 Edge-to-Edge 下很容易出问题的组件。

推荐写法:

dart 复制代码
showModalBottomSheet(
  context: context,
  isScrollControlled: true,
  builder: (context) {
    final bottomPadding = MediaQuery.paddingOf(context).bottom;
    final keyboardHeight = MediaQuery.viewInsetsOf(context).bottom;
    return Padding(
      padding: EdgeInsets.only(
        bottom: keyboardHeight,
      ),
      child: Container(
        padding: EdgeInsets.only(
          left: 16,
          right: 16,
          top: 16,
          bottom: bottomPadding + 16,
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('底部弹窗内容'),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {},
              child: const Text('确认'),
            ),
          ],
        ),
      ),
    );
  },
);

如果 BottomSheet 内部有输入框,isScrollControlled: true 基本是必加的。


十一、状态栏图标颜色适配

Edge-to-Edge 下,状态栏通常是透明的,所以状态栏图标颜色要根据背景明暗调整。

浅色背景

dart 复制代码
SystemChrome.setSystemUIOverlayStyle(
  const SystemUiOverlayStyle(
    statusBarColor: Colors.transparent,
    statusBarIconBrightness: Brightness.dark,
    systemNavigationBarColor: Colors.transparent,
    systemNavigationBarIconBrightness: Brightness.dark,
  ),
);

深色背景

dart 复制代码
SystemChrome.setSystemUIOverlayStyle(
  const SystemUiOverlayStyle(
    statusBarColor: Colors.transparent,
    statusBarIconBrightness: Brightness.light,
    systemNavigationBarColor: Colors.transparent,
    systemNavigationBarIconBrightness: Brightness.light,
  ),
);

如果某些页面背景图比较复杂,建议给顶部区域增加渐变遮罩:

dart 复制代码
Positioned(
  top: 0,
  left: 0,
  right: 0,
  height: 120,
  child: IgnorePointer(
    child: DecoratedBox(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            Colors.black.withOpacity(0.4),
            Colors.transparent,
          ],
        ),
      ),
    ),
  ),
);

这样可以避免白色图标叠在浅色背景上看不清。


十二、Android 原生侧配置注意事项

如果项目还没有完成适配,在 Android 15 上曾经可以通过 windowOptOutEdgeToEdgeEnforcement 暂时退出 Edge-to-Edge:

xml 复制代码
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>

但这只是过渡方案。Android 官方已经说明,Android 16 target SDK 36 后,这个 opt-out 属性会被废弃并禁用,应用不能再退出 Edge-to-Edge。

所以不建议长期依赖这个配置。正确做法是:

  1. 接受 Edge-to-Edge 是未来默认行为
  2. 检查所有页面顶部和底部遮挡问题
  3. 使用 SafeArea / MediaQuery 正确处理 inset
  4. 特殊页面单独设计沉浸式效果

十三、常见页面适配建议

1. 普通列表页

推荐:

dart 复制代码
SafeArea(
  child: ListView(...),
)

2. 首页沉浸式头图

推荐:

dart 复制代码
Stack(
  children: [
    HeaderBackground(),
    SafeArea(child: HeaderContent()),
  ],
)

3. 底部固定按钮

推荐:

dart 复制代码
final bottom = MediaQuery.paddingOf(context).bottom;
Padding(
  padding: EdgeInsets.only(bottom: bottom + 16),
  child: Button(),
)

4. 聊天输入框

推荐:

dart 复制代码
final keyboard = MediaQuery.viewInsetsOf(context).bottom;
final bottom = MediaQuery.paddingOf(context).bottom;
paddingBottom = keyboard > 0 ? keyboard : bottom;

5. 视频/图片全屏页

推荐:
SystemUiMode.edgeToEdge + Stack + 局部 SafeArea

背景可以全屏,操作按钮需要放在 SafeArea 内。


十四、项目级封装建议

在实际 Flutter 项目中,不建议每个页面都手写 MediaQuery.paddingOf(context).bottom

可以封装一个通用底部安全容器:

dart 复制代码
class AppSafeBottom extends StatelessWidget {
  const AppSafeBottom({
    super.key,
    required this.child,
    this.minimum = 12,
  });

  final Widget child;
  final double minimum;

  @override
  Widget build(BuildContext context) {
    final bottom = MediaQuery.paddingOf(context).bottom;
    return Padding(
      padding: EdgeInsets.only(
        bottom: bottom + minimum,
      ),
      child: child,
    );
  }
}

使用:

dart 复制代码
AppSafeBottom(
  child: ElevatedButton(
    onPressed: () {},
    child: const Text('提交'),
  ),
)

也可以封装一个页面容器:

dart 复制代码
class AppPage extends StatelessWidget {
  const AppPage({
    super.key,
    required this.child,
    this.safeTop = true,
    this.safeBottom = true,
  });

  final Widget child;
  final bool safeTop;
  final bool safeBottom;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      top: safeTop,
      bottom: safeBottom,
      child: child,
    );
  }
}

使用:

dart 复制代码
Scaffold(
  body: AppPage(
    child: ListView(
      children: const [
        Text('内容'),
      ],
    ),
  ),
);

十五、适配检查清单

升级 Flutter 或 target Android 15+ 后,建议重点检查以下页面:

  1. 首页
  2. 登录页
  3. 聊天页
  4. 搜索页
  5. 详情页
  6. 图片预览页
  7. 视频播放页
  8. 有 BottomNavigationBar 的页面
  9. 有底部固定按钮的页面
  10. 有 BottomSheet 的页面
  11. 有 TextField 的页面
  12. 全屏弹窗页面

重点看这些问题:

  1. 顶部内容是否被状态栏遮挡
  2. 返回按钮是否离屏幕顶部太近
  3. 底部按钮是否被导航栏遮挡
  4. BottomNavigationBar 是否和系统手势条重叠
  5. 键盘弹起后输入框是否正常显示
  6. 深色背景下状态栏图标是否可见
  7. 浅色背景下状态栏图标是否可见
  8. Android 三键导航模式下是否正常
  9. Android 手势导航模式下是否正常
  10. 刘海屏、挖孔屏设备是否正常

Android 官方也建议开发者在 Edge-to-Edge 模式下重点处理 system bars、display cutout 和 system gesture insets,特别是可点击控件不能被系统栏遮挡。


十六、推荐最终实践方案

对于大多数 Flutter 项目,可以采用下面这套规则:

  1. main() 中主动设置 SystemUiMode.edgeToEdge
  2. 状态栏、导航栏设置透明
  3. 普通页面使用 SafeArea
  4. 沉浸式页面使用 Stack + 局部 SafeArea
  5. 底部按钮统一封装 SafeBottom
  6. 输入框页面处理 viewInsets.bottom
  7. BottomSheet 设置 isScrollControlled: true
  8. 不再长期依赖 Android opt-out 配置
  9. 必须测试 Android 15 / Android 16 / 手势导航 / 三键导航

入口代码:

dart 复制代码
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemChrome.setEnabledSystemUIMode(
    SystemUiMode.edgeToEdge,
  );
  SystemChrome.setSystemUIOverlayStyle(
    const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      systemNavigationBarColor: Colors.transparent,
      statusBarIconBrightness: Brightness.dark,
      systemNavigationBarIconBrightness: Brightness.dark,
    ),
  );
  runApp(const MyApp());
}

页面代码:

dart 复制代码
Scaffold(
  body: SafeArea(
    child: YourPageContent(),
  ),
);

底部按钮:

dart 复制代码
class SafeBottomButton extends StatelessWidget {
  const SafeBottomButton({
    super.key,
    required this.text,
    required this.onPressed,
  });

  final String text;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    final bottom = MediaQuery.paddingOf(context).bottom;
    return Padding(
      padding: EdgeInsets.fromLTRB(
        16,
        12,
        16,
        bottom + 12,
      ),
      child: SizedBox(
        height: 48,
        width: double.infinity,
        child: ElevatedButton(
          onPressed: onPressed,
          child: Text(text),
        ),
      ),
    );
  }
}

十七、总结

Edge-to-Edge 不是一个简单的视觉效果,而是 Android 新版本下的默认布局趋势。

对于 Flutter 项目来说,适配 Edge-to-Edge 的核心不是写一行:

dart 复制代码
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

而是要真正处理好:

  1. 顶部状态栏安全区域
  2. 底部导航栏安全区域
  3. 键盘弹起区域
  4. 刘海屏和挖孔屏
  5. 沉浸式页面和普通页面的差异
  6. Android 15 / Android 16 的默认行为变化

推荐尽早完成适配,而不是依赖临时 opt-out。因为从 Android 16 开始,退出 Edge-to-Edge 的旧方案已经不再可靠,Flutter 项目后续也会越来越默认地朝 Edge-to-Edge 方向演进。

相关推荐
xmdy58661 小时前
Flutter + 开源鸿蒙实战|城市智慧停车管理系统 Day4 停车订单生成+多状态管理+在线缴费+我的订单+会员中心+个人中心完善
flutter·开源·harmonyos
xmdy58661 小时前
Flutter + 开源鸿蒙实战|城市智慧停车管理系统 Day8 进阶美化与真机调优篇
flutter·华为·harmonyos
Zender Han1 小时前
Flutter 高斯模糊介绍与具体实现
android·flutter·ios
AFinalStone1 小时前
Android 16系统源码_无障碍辅助(三)权限弹窗无法被无障碍服务识别
android
zhangphil1 小时前
Android图形系统Graphics来源、内存占用量统计、为什么很大,如何优化
android
黄林晴1 小时前
Android Show I/O 2026:开发者该关注这几件事
android
Kapaseker1 小时前
最简单的 Compose 动画 — animateDpAsState
android·kotlin