1. 为什么要在 Android 7.0 上折腾 SystemUI 状态栏和导航栏
做过 ROM 定制、车机系统、自助终端或者工业平板的朋友,大概率都碰过这样一个需求:把顶部状态栏和底部导航栏藏起来,让应用全屏独占,或者反过来,在特定场景下把系统栏重新拉出来。Android 7.0(Nougat,API 24)是一个很特殊的分水岭版本,它既保留了老式的setSystemUiVisibility那套标志位玩法,又开始引入多窗口、分屏这些新特性,导致 SystemUI 的显隐逻辑比 5.x、6.x 时代复杂了不少。
我最早接触这个需求是在一个自助查询机的项目上,设备跑的是基于 Android 7.0 定制的系统,客户要求开机后状态栏和导航栏全部隐藏,只留一个全屏的业务界面,但后台维护时又要能通过一个隐藏手势把系统栏调出来。当时网上能找到的资料要么是 Android 4.x 时代的老方法,要么是直接改build.prop一刀切,真正讲清楚 SystemUI 内部机制的文章少得可怜。踩了一圈坑之后,我把状态栏和导航栏的显隐逻辑从上层 API 到底层 SystemUI 服务完整梳理了一遍,这里把整个思路和实操过程整理出来。
这篇文章主要面向三类人:一是做 ROM 定制和系统裁剪的工程师,二是开发自助终端、车机、广告机这类需要全屏沉浸的应用开发者,三是想搞清楚 SystemUI 内部到底怎么运转的 Android 系统爱好者。内容会从上层应用调用一直讲到 SystemUI 进程内部的实现,中间涉及PhoneStatusBar、PhoneWindowManager、StatusBarManagerService这几个关键角色,也会给出可以直接抄的代码和配置。读完之后,你应该能自己判断在什么场景下该用哪种方案,而不是盲目复制粘贴。
2. 先搞清楚 SystemUI 里状态栏和导航栏到底是谁在管
2.1 状态栏、导航栏和 SystemUI 进程的关系
很多人一上来就问“怎么隐藏状态栏”,但其实状态栏和导航栏并不是同一个东西,它们虽然都显示在屏幕上,背后的管理逻辑却分属不同的模块。状态栏(StatusBar)和导航栏(NavigationBar)在 Android 7.0 里都由 SystemUI 这个系统应用负责绘制,但它们的显示状态是由PhoneWindowManager和StatusBarManagerService共同协商决定的。
SystemUI 是一个跑在系统进程之外的独立 APK,包名是com.android.systemui。它内部有几个核心类:PhoneStatusBar负责状态栏和导航栏的整体管理,NavigationBarView专门画导航栏那三个键(返回、主页、最近任务),StatusBarWindowView则是状态栏窗口的根布局。Android 7.0 相比 6.0 最大的变化是引入了StatusBar接口抽象,PhoneStatusBar实现了这个接口,方便不同形态的设备(比如电视、车机)替换实现。
导航栏在 Android 7.0 里还有一个特殊之处:它默认是画在NavigationBarFrame里的,而这个 Frame 的显示隐藏受WindowManagerService的setNavBarVisibility控制。状态栏则通过StatusBarManagerService的disable()方法来控制哪些部分被禁用。这两个路径不一样,所以你在做隐藏的时候,经常会出现“状态栏藏了但导航栏还在”或者“导航栏藏了但状态栏下拉还能拉出来”的情况,根本原因就是没搞清楚它们各自的控制通道。
2.2 上层 API 和底层服务的调用链路
从应用层看,我们最熟悉的就是View.setSystemUiVisibility(int flags)这个方法。它设置的标志位最终会通过 Binder 调用传到WindowManagerService,再由PhoneWindowManager处理。PhoneWindowManager是 WindowManagerService 里负责策略的部分,它拿到这些标志位后,会计算出当前系统栏应该处于什么状态,然后调用StatusBarManagerService去实际执行。
具体链路是这样的:应用调用setSystemUiVisibility→ViewRootImpl把标志位通过IWindowSession.relayout传给 WMS →PhoneWindowManager在layoutWindowLw里读取mSystemUiVisibility→ 根据标志位决定是否要隐藏状态栏或导航栏 → 调用StatusBarManagerService.disable()或WindowManagerService.setNavBarVisibility()。
这里有个关键点:PhoneWindowManager里维护了一个mSystemUiVisibility变量,它是所有窗口标志位的“或”结果。也就是说,如果同时有两个窗口,一个要求隐藏状态栏,另一个没要求,那最终结果可能还是显示。这个机制在设计多窗口应用时特别容易踩坑,后面讲常见问题时会详细说。
2.3 状态栏和导航栏的隐藏标志位对照
Android 7.0 里跟系统栏显隐相关的标志位有好几个,很多人分不清它们的作用范围。我整理了一张表,把最常用的几个列出来:
| 标志位 | 数值 | 作用范围 | 典型场景 |
|---|---|---|---|
SYSTEM_UI_FLAG_FULLSCREEN | 0x00000004 | 隐藏状态栏 | 全屏视频、游戏 |
SYSTEM_UI_FLAG_HIDE_NAVIGATION | 0x00000002 | 隐藏导航栏 | 沉浸式阅读 |
SYSTEM_UI_FLAG_IMMERSIVE | 0x00000800 | 配合上面两个,触摸边缘不恢复 | 游戏、视频播放器 |
SYSTEM_UI_FLAG_IMMERSIVE_STICKY | 0x00001000 | 同上,但系统栏会半透明短暂显示 | 电子书、漫画 |
SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | 0x00000400 | 内容延伸到状态栏区域 | 自定义标题栏 |
SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 0x00000200 | 内容延伸到导航栏区域 | 全屏布局 |
SYSTEM_UI_FLAG_LOW_PROFILE | 0x00000001 | 状态栏图标变暗 | 夜间模式 |
需要特别注意的是,SYSTEM_UI_FLAG_FULLSCREEN和SYSTEM_UI_FLAG_HIDE_NAVIGATION是“请求隐藏”,但系统不一定马上执行。比如用户刚触摸过屏幕,导航栏可能会临时显示出来。而SYSTEM_UI_FLAG_IMMERSIVE系列是控制“隐藏后怎么恢复”的行为,不单独使用。
提示:
SYSTEM_UI_FLAG_LOW_PROFILE在 Android 7.0 上对导航栏的影响已经弱化了,它主要让状态栏图标变暗,不再像 4.x 那样把导航栏变成三个小点。
3. 应用层实现全屏隐藏的三种方案与选型
3.1 方案一:setSystemUiVisibility 动态控制
这是最常规的做法,适合应用内自己控制全屏。核心代码就几行:
// 隐藏状态栏和导航栏,触摸边缘不恢复 View decorView = getWindow().getDecorView(); int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; decorView.setSystemUiVisibility(flags);这段代码里,LAYOUT_FULLSCREEN和LAYOUT_HIDE_NAVIGATION是让内容延伸到系统栏区域,LAYOUT_STABLE是保持布局稳定,避免系统栏显隐时内容跳动。IMMERSIVE_STICKY表示隐藏后用户从边缘滑动会临时显示系统栏,几秒后自动隐藏,适合阅读类应用。
实测下来,这套方案在 Android 7.0 原生系统上表现很稳,但在某些定制 ROM 上会有延迟。原因是PhoneWindowManager处理标志位是异步的,它要等下一次窗口布局才会生效。如果你在onCreate里设置,可能第一帧还是会闪一下系统栏。解决办法是在onWindowFocusChanged里再设置一次,或者用postDelayed延迟 100 毫秒。
还有一个坑:如果你在 Fragment 里切换,不同 Fragment 设置的标志位会互相覆盖。因为mSystemUiVisibility是所有窗口标志位的或结果,A Fragment 设置了隐藏,B Fragment 没设置,切到 B 的时候可能还是隐藏状态。所以最好在 BaseActivity 里统一管理,或者在onResume里重新设置当前页面需要的标志位。
3.2 方案二:修改主题样式静态隐藏
如果整个应用都需要全屏,不想在每个 Activity 里写代码,可以直接在主题里配置:
<style name="FullScreenTheme" parent="android:Theme.Material.NoActionBar"> <item name="android:windowFullscreen">true</item> <item name="android:windowContentOverlay">@null</item> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style>windowFullscreen为 true 会隐藏状态栏,但它不会隐藏导航栏。导航栏的隐藏还是得靠代码。而且windowFullscreen在 Android 7.0 上有个副作用:它会让状态栏区域变成黑色,而不是透明,如果你的应用背景是浅色,顶部会有一条黑边。所以这个方案更适合状态栏本来就是深色的场景。
另外,windowLayoutInDisplayCutoutMode是 Android 9.0 才引入的,在 7.0 上写进去会被忽略,但不会报错。如果你只针对 7.0,可以去掉这一行。
3.3 方案三:WindowManager.LayoutParams 系统级隐藏
这个方案适合系统应用或者有系统签名的应用,可以直接操作 WindowManager 的参数:
WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN; lp.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; getWindow().setAttributes(lp);FLAG_FULLSCREEN是 WindowManager 层面的标志,它比setSystemUiVisibility更底层,生效更快。但它的缺点是只能隐藏状态栏,导航栏还是得靠systemUiVisibility。而且这个标志在 Android 7.0 上如果和setSystemUiVisibility混用,可能会出现状态不一致的情况,建议二选一。
三种方案的对比:
| 方案 | 生效速度 | 导航栏支持 | 适用场景 | 权限要求 |
|---|---|---|---|---|
| setSystemUiVisibility | 中等 | 支持 | 应用内动态控制 | 无 |
| 主题样式 | 快 | 不支持 | 全局全屏 | 无 |
| WindowManager 参数 | 快 | 部分支持 | 系统应用 | 系统签名 |
注意:如果你的应用没有系统签名,
WindowManager.LayoutParams里的一些高级标志会被系统忽略,不要指望用它来做系统级隐藏。
4. 系统级隐藏:改 SystemUI 源码还是用服务接口
4.1 通过 StatusBarManagerService 控制
如果你有系统权限,最正规的做法是调用StatusBarManagerService的disable()方法。这个方法是隐藏状态栏和导航栏的“官方通道”,它的原型是:
public void disable(int what, IBinder token, String pkg) { // what 是标志位组合 }what可以取以下值:
StatusBarManager.DISABLE_EXPAND:禁止下拉状态栏StatusBarManager.DISABLE_NOTIFICATION_ICONS:隐藏通知图标StatusBarManager.DISABLE_NOTIFICATION_ALERTS:禁止通知提醒StatusBarManager.DISABLE_HOME:禁用 Home 键StatusBarManager.DISABLE_RECENT:禁用最近任务键StatusBarManager.DISABLE_BACK:禁用返回键StatusBarManager.DISABLE_CLOCK:隐藏时钟StatusBarManager.DISABLE_SEARCH:禁用搜索键
注意,disable()方法控制的是“禁用”而不是“隐藏”。比如DISABLE_HOME会让 Home 键变灰不可点,但导航栏本身还在。如果你想彻底隐藏导航栏,需要调用WindowManagerService.setNavBarVisibility(),但这个方法是隐藏 API,需要反射调用。
反射代码大概长这样:
try { Class<?> wmsClass = Class.forName("android.view.WindowManagerService"); Method setNavBarVisibility = wmsClass.getMethod("setNavBarVisibility", boolean.class); Object wms = getSystemService("window"); setNavBarVisibility.invoke(wms, false); } catch (Exception e) { e.printStackTrace(); }实测在 Android 7.0 原生系统上,这个反射可以生效,但在一些定制 ROM 上,setNavBarVisibility可能被改名或者移除了,需要根据具体 ROM 调整。
4.2 修改 SystemUI 源码的 PhoneStatusBar
如果你是在做 ROM 定制,可以直接改 SystemUI 的源码。Android 7.0 里PhoneStatusBar.java的addNavigationBar()方法负责创建导航栏,addStatusBarWindow()负责创建状态栏窗口。如果你想开机就隐藏导航栏,可以在makeStatusBarView()里判断一个系统属性,然后决定是否调用addNavigationBar()。
比如:
boolean hideNavBar = SystemProperties.getBoolean("persist.sys.hide_navbar", false); if (!hideNavBar) { addNavigationBar(); }然后在build.prop或者default.prop里加一行persist.sys.hide_navbar=true。这样开机后导航栏就不会被创建,比运行时隐藏更彻底,也不会出现“隐藏后触摸边缘又弹出来”的问题。
状态栏的隐藏类似,可以在makeStatusBarView()里控制mStatusBarWindow的添加。但状态栏涉及的东西更多,比如下拉通知、快捷设置,直接不创建可能会导致系统功能异常。更稳妥的做法是保留状态栏窗口,但把它的高度设为 0,或者把PhoneStatusBarView的 visibility 设为 GONE。
提示:改 SystemUI 源码需要重新编译系统镜像,适合有源码环境的团队。如果只是应用层开发,不要走这条路。
4.3 用系统属性控制开机默认状态
除了改源码,还可以通过系统属性来影响 SystemUI 的行为。Android 7.0 的 SystemUI 里有一些属性判断,比如ro.statusbar.hide、persist.sys.navbar.hide等,但不同厂商的属性名不一样。原生 AOSP 里并没有统一的属性来控制状态栏和导航栏的默认显隐,所以这个方法需要你先确认目标 ROM 支持哪些属性。
一个比较通用的做法是写一个开机自启的系统服务,在BOOT_COMPLETED广播里调用StatusBarManagerService.disable()和setNavBarVisibility()。这个服务需要有系统签名,或者放在/system/priv-app目录下。代码逻辑很简单:
public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { StatusBarManager sbm = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE); sbm.disable(StatusBarManager.DISABLE_EXPAND, null, context.getPackageName()); // 反射隐藏导航栏 // ... } } }这个方案的优点是灵活,可以根据不同条件决定隐藏哪些部分;缺点是开机后有几秒的延迟,系统栏会先显示再隐藏,视觉上会闪一下。
5. 实操过程:从零实现一个可切换的系统栏控制器
5.1 环境准备和权限声明
先说一下我的测试环境:一台刷了 Android 7.0 原生系统的开发板,有 root 权限,应用放在/system/priv-app下。如果你没有系统签名,下面的系统级操作可能无法生效,但应用层的setSystemUiVisibility部分是可以直接用的。
在AndroidManifest.xml里需要声明这些权限:
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" /> <uses-permission android:name="android.permission.STATUS_BAR" /> <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />STATUS_BAR是系统级权限,普通应用申请不到,需要放在/system/priv-app或者用平台签名。EXPAND_STATUS_BAR是普通权限,用来控制状态栏展开。
5.2 封装一个 SystemBarController 工具类
我把状态栏和导航栏的控制逻辑封装成了一个工具类,核心方法有三个:hideAll()、showAll()、toggle()。先看代码:
public class SystemBarController { private Activity mActivity; private StatusBarManager mStatusBarManager; public SystemBarController(Activity activity) { mActivity = activity; mStatusBarManager = (StatusBarManager) activity.getSystemService(Context.STATUS_BAR_SERVICE); } public void hideAll() { // 应用层隐藏 View decorView = mActivity.getWindow().getDecorView(); int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; decorView.setSystemUiVisibility(flags); // 系统级隐藏(需要权限) if (mStatusBarManager != null) { mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND | StatusBarManager.DISABLE_HOME | StatusBarManager.DISABLE_RECENT | StatusBarManager.DISABLE_BACK, null, mActivity.getPackageName()); } // 反射隐藏导航栏 setNavBarVisibility(false); } public void showAll() { View decorView = mActivity.getWindow().getDecorView(); decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); if (mStatusBarManager != null) { mStatusBarManager.disable(StatusBarManager.DISABLE_NONE, null, mActivity.getPackageName()); } setNavBarVisibility(true); } private void setNavBarVisibility(boolean visible) { try { Class<?> wmsClass = Class.forName("android.view.WindowManagerService"); Method method = wmsClass.getMethod("setNavBarVisibility", boolean.class); Object wms = mActivity.getSystemService(Context.WINDOW_SERVICE); method.invoke(wms, visible); } catch (Exception e) { Log.w("SystemBarController", "setNavBarVisibility failed", e); } } }这个类里,hideAll()做了三件事:设置应用层标志位、调用StatusBarManagerService.disable()禁用按键、反射隐藏导航栏。三层保险,确保系统栏藏得干净。
showAll()则是反过来,把标志位清掉,disable()传DISABLE_NONE,导航栏设为可见。
5.3 处理触摸边缘恢复的问题
用IMMERSIVE_STICKY的时候,用户从屏幕边缘滑动会临时显示系统栏。这个行为在大多数场景下是好的,但在自助终端上,用户可能误触边缘导致系统栏弹出来,然后就不自动隐藏了。解决办法是监听系统栏的可见性变化,一旦发现它显示了,立刻再隐藏。
Android 7.0 里可以用View.OnSystemUiVisibilityChangeListener来监听:
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { @Override public void onSystemUiVisibilityChange(int visibility) { if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { // 系统栏显示了,延迟 2 秒再隐藏 decorView.postDelayed(new Runnable() { @Override public void run() { hideAll(); } }, 2000); } } });这个监听器在系统栏显隐时会回调,visibility参数是当前的标志位组合。如果SYSTEM_UI_FLAG_FULLSCREEN位为 0,说明状态栏显示了,这时候延迟 2 秒再隐藏,给用户一个短暂的查看时间。
实测下来,这个方案在 Android 7.0 上很稳,但要注意postDelayed的 Runnable 可能会被多次触发,最好加一个标志位防止重复执行。
5.4 开机自动隐藏的完整流程
如果要做成开机自动隐藏,需要把上面的工具类放到一个 Service 里,然后在BOOT_COMPLETED广播里启动。完整流程如下:
- 写一个
SystemBarService,在onCreate里初始化SystemBarController,调用hideAll()。 - 写一个
BootReceiver,监听BOOT_COMPLETED,启动SystemBarService。 - 在
AndroidManifest.xml里注册 Service 和 Receiver,Receiver 要加android:priority="1000"。 - 把 APK 放到
/system/priv-app/SystemBarController/目录下,设置权限为 644。 - 重启设备,验证开机后系统栏是否自动隐藏。
这里有个细节:BOOT_COMPLETED广播是在系统启动完成后才发出的,这时候 SystemUI 已经起来了,所以你会看到系统栏先显示几秒,然后才隐藏。如果想让开机就隐藏,必须改 SystemUI 源码,在makeStatusBarView()里就控制好。
注意:把 APK 放到
/system/priv-app需要 root 权限,而且不同 Android 版本对 priv-app 的权限要求不一样。Android 7.0 上,priv-app 里的应用默认拥有系统权限,但需要在etc/permissions/目录下有对应的 xml 文件声明。
6. 常见问题与排查技巧实录
6.1 状态栏隐藏了但下拉还能拉出来
这是最常见的问题。原因是setSystemUiVisibility只隐藏了状态栏的视觉部分,但StatusBarManagerService的下拉功能还在。用户从顶部边缘下滑,还是能把通知栏拉出来。解决办法是调用StatusBarManager.disable(DISABLE_EXPAND, ...),把下拉功能也禁掉。
如果disable()不生效,检查一下应用是否有STATUS_BAR权限。没有这个权限的话,disable()会静默失败,不会报错,但也不起作用。
6.2 导航栏隐藏后触摸边缘又弹出来
这是IMMERSIVE和IMMERSIVE_STICKY的默认行为。如果你不想让用户通过触摸边缘恢复导航栏,就不要用这两个标志,只用SYSTEM_UI_FLAG_HIDE_NAVIGATION。但这样有个问题:用户触摸屏幕任何地方,导航栏都会恢复。所以更好的做法是用IMMERSIVE_STICKY加上OnSystemUiVisibilityChangeListener,发现恢复了就立刻再隐藏。
还有一个情况是,某些定制 ROM 的PhoneWindowManager对IMMERSIVE_STICKY的处理有 bug,导航栏恢复后不会自动隐藏。这时候只能靠监听器手动处理。
6.3 多窗口模式下系统栏显隐混乱
Android 7.0 引入了多窗口模式,两个应用同时显示时,系统栏的显隐由PhoneWindowManager根据所有窗口的标志位“或”结果决定。如果 A 应用要求隐藏状态栏,B 应用没要求,那最终状态栏可能还是显示。这个行为在分屏时特别明显。
解决办法是在onMultiWindowModeChanged回调里重新设置标志位,确保当前应用获得焦点时系统栏状态正确。但要注意,分屏模式下系统栏的最终状态还是由系统决定,应用只能“建议”,不能强制。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 状态栏隐藏后下拉仍可用 | 未禁用 EXPAND | 检查 disable() 是否调用 | 调用 disable(DISABLE_EXPAND) |
| 导航栏隐藏后触摸边缘恢复 | IMMERSIVE 标志 | 检查标志位组合 | 用监听器自动重新隐藏 |
| 系统栏闪烁一下才隐藏 | 标志位设置时机太早 | 在 onWindowFocusChanged 里设置 | 延迟 100ms 再设置 |
| 分屏时系统栏状态不对 | 多窗口标志位合并 | 检查 onMultiWindowModeChanged | 重新设置当前窗口标志位 |
| disable() 不生效 | 缺少 STATUS_BAR 权限 | 检查应用是否在 priv-app | 放到 /system/priv-app |
| 反射 setNavBarVisibility 失败 | ROM 修改了方法名 | 查看系统源码 | 根据 ROM 调整反射代码 |
6.5 几个踩过的坑和独家技巧
第一个坑:在onCreate里调用setSystemUiVisibility有时候不生效,因为这时候 DecorView 还没 attach 到 Window。我试过最稳的时机是在onWindowFocusChanged(boolean hasFocus)里,当hasFocus为 true 时设置,这时候窗口已经获得焦点,标志位会立即生效。
第二个坑:SYSTEM_UI_FLAG_LAYOUT_STABLE这个标志很多人不加,导致系统栏显隐时内容会跳动。加上它之后,布局会保持稳定,系统栏隐藏时内容不会重新布局。这个标志在 Android 7.0 上效果很好,建议都加上。
第三个技巧:如果你想让状态栏透明但导航栏隐藏,可以组合使用SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_HIDE_NAVIGATION,然后在主题里把android:statusBarColor设为透明。这样状态栏区域会显示应用内容,但状态栏图标还在,导航栏则完全隐藏。
第四个技巧:在 Android 7.0 上,WindowManager.LayoutParams.FLAG_FULLSCREEN和setSystemUiVisibility(SYSTEM_UI_FLAG_FULLSCREEN)同时使用时,后者会覆盖前者。所以如果你用了FLAG_FULLSCREEN,就不要再设置SYSTEM_UI_FLAG_FULLSCREEN,否则可能会出现状态栏隐藏了但布局没延伸的情况。
7. 不同场景下的方案选择建议
7.1 应用内全屏:优先用 setSystemUiVisibility
如果你只是想让自己的应用全屏,不涉及系统级修改,那setSystemUiVisibility是最合适的。它不需要特殊权限,兼容性好,从 Android 4.0 到 7.0 都能用。关键是要选对标志位组合:视频播放器用IMMERSIVE,阅读类应用用IMMERSIVE_STICKY,游戏用IMMERSIVE加LAYOUT_STABLE。
7.2 系统级隐藏:改源码或系统服务
如果你在做 ROM 定制,需要开机就隐藏系统栏,那改 SystemUI 源码是最彻底的方案。在PhoneStatusBar.makeStatusBarView()里根据系统属性决定是否添加导航栏和状态栏窗口,这样开机后系统栏根本不会出现,也不会有闪烁问题。
如果没有源码环境,但有系统签名,可以写一个系统服务,在BOOT_COMPLETED里调用StatusBarManagerService.disable()和反射setNavBarVisibility()。这个方案有延迟,但比改源码灵活。
7.3 自助终端和车机:组合方案
自助终端和车机通常需要“默认隐藏,维护时显示”的逻辑。我的做法是:默认用系统服务隐藏系统栏,同时监听一个隐藏手势(比如连续点击屏幕左上角 5 次),触发后显示系统栏并保持 30 秒,30 秒无操作再自动隐藏。这样既保证了正常使用时全屏,又方便了维护人员。
这个逻辑的核心是一个状态机:HIDDEN、VISIBLE_TEMP、VISIBLE_LOCKED三个状态。HIDDEN是默认状态,VISIBLE_TEMP是临时显示,VISIBLE_LOCKED是维护模式。状态切换通过手势和定时器驱动。
提示:隐藏手势不要用太复杂的组合,维护人员可能记不住。连续点击左上角 5 次是个不错的选择,既不容易误触,又容易记忆。
7.4 方案选择对照表
| 场景 | 推荐方案 | 权限要求 | 生效时机 | 缺点 |
|---|---|---|---|---|
| 应用内全屏 | setSystemUiVisibility | 无 | 运行时 | 多窗口下可能失效 |
| 开机全屏 | 改 SystemUI 源码 | 源码环境 | 开机 | 需要重新编译 |
| 系统级动态控制 | StatusBarManagerService | 系统签名 | 运行时 | 有延迟 |
| 自助终端 | 组合方案 | 系统签名 | 开机+运行时 | 逻辑复杂 |
8. 最后的经验分享
我在实际项目里踩过最深的坑,是在一个 Android 7.0 的车机项目上,客户要求导航栏隐藏但状态栏保留。我一开始用setSystemUiVisibility隐藏导航栏,测试发现导航栏确实藏了,但倒车影像切换回来之后,导航栏又出现了。查了半天才发现,倒车影像是一个独立的系统窗口,它的标志位覆盖了我的应用设置。后来改成在PhoneWindowManager里加了一个判断,如果是倒车影像窗口,就不恢复导航栏,问题才解决。
这个经历告诉我,Android 7.0 的系统栏显隐不是一个简单的 API 调用,它涉及到 WindowManager、SystemUI、PhoneWindowManager 多个模块的协作。应用层能做的只是“建议”,最终决定权在系统。所以如果你发现设置不生效,不要只盯着自己的代码,要往系统层查。
另外,不同厂商的 ROM 对系统栏的处理差异很大。原生 AOSP 上能用的反射方法,在定制 ROM 上可能被改名或者移除。所以做系统级隐藏时,一定要先确认目标 ROM 的源码,或者准备好降级方案。我一般会准备两套方案:优先用系统 API,失败则用反射,再失败则改 SystemUI 源码。这样不管遇到什么 ROM,都能有办法解决。
最后分享一个小技巧:如果你不确定当前系统栏的状态,可以用dumpsys window命令查看。在 adb shell 里执行dumpsys window | grep -i "systemui",能看到当前系统栏窗口的可见性和标志位。这个命令在排查问题时特别有用,比猜来猜去高效多了。