
很多拿到 RK3588 开发板做安卓工控屏、自助终端、AIO 一体机的兄弟应该都遇到过同一个麻烦Android12 系统下想彻底隐藏导航栏和状态栏按网上的老方法试了一圈要么藏得不干净要么过几分钟又被应用顶出来甚至底部还会多出一条半透明白边。这问题我在 RK3588 的 Android12 源码定制上反复折腾过最终找到一套比较完整的源码修改思路。这篇就把我的改法、避坑记录和排查链路全部摊开照着做基本能一次搞定。1. 为什么升级到 Android12 后老一套隐藏大法失灵了1.1 源码里的两条栏到底由谁管控要彻底隐藏导航栏和状态栏首先得搞清楚它们在 Android 系统里到底是谁在管。简单说导航栏和状态栏都属于 SystemUI 系统界面进程SystemUI 内部拆成很多模块状态栏对应StatusBarAndroid12 上源码路径通常是packages/SystemUI/src/com/android/systemui/statusbar/phone/导航栏对应NavigationBarController。SystemUI 在启动时会读取框架层的资源配置判断当前设备有没有导航栏、状态栏高度应该是多少、要不要显示底部手势条。框架层的资源配置在frameworks/base/core/res/res/values/里具体是config.xml和dimens.xml。config.xml里的开关决定这条栏该不该出现在系统图里dimens.xml里的尺寸决定这条栏物理上占多高。两头配合才能让系统界面和普通应用拿到的窗口 insets 全部归零否则就会出现栏已经没了但应用还是预留了顶部状态栏高度这种错位。1.2 Android 12 强制 Edge-to-edge状态栏高度被硬编码进各应用布局Android 10 以前很多设备上只要在build.prop里加一行qemu.hw.mainkeys1就能干掉导航栏状态栏也可以把dimens.xml改成 0。那个年代很多 App 是直接使用getStatusBarHeight()或者window.decorView.systemUiVisibility来处理显示区域系统栏隐藏后它们一般也会跟着调整。但 Android 11 开始系统强制 Edge-to-edgeAndroid12 更是把WindowInsets机制贯彻到最底层。新的逻辑是系统栏看不见不等于不存在。导航栏和状态栏即使高度为 0系统也会通过WindowInsets把安全区域告诉应用图层如果底层资源还认为这个设备有状态栏应用就会白白给顶部留一块空白。反过来说如果你只是改了 SystemUI 让它别画出来某些不做 Edge-to-edge 适配的 App 仍然会调用setDecorFitsSystemWindows(true)把内容缩在系统栏下方——结果就是屏幕上出现一条永远盖不住的黑边或白边。所以在 Android12 上做彻底隐藏本质上要做三类事关掉 SystemUI 的创建入口、把资源层的高度和开关全部归零、再处理系统级 immersive 策略让 App 层认为自己拿到的 insets 是 0。这也是老教程只改一个点达不到效果的原因。1.3 原厂 SDK 与 AOSP 的差异RK3588 常见变体RK3588 的 Android12 源码主要来自 Rockchip 官方发布的 SDK它是在 AOSP 基础上做的二次封装。不同版本的 SDK 对 SystemUI 的改动程度不一样有的分支里StatusBar的代码已经和 AOSP 上游差很多还有的分支里导航栏变成了手势导航的Taskbar。我这边主要讲通用原理具体操作时你搜到的类名以自己仓库实际代码为准千万别直接照抄网上针对骁龙平台或者谷歌 Pixel 的补丁。2. 动手前先定方案三种隐藏方式怎么选2.1 资源覆盖层最轻量适合全系列统一隐藏资源覆盖是在编译阶段把frameworks/base/core/res以及 SystemUI 里的资源值用你项目的值替换不需要改 Java 代码也不会破坏原有逻辑。适合所有 RK3588 板卡统一隐藏比如你就是做一款固定型号的商显一体机用这一招就能满足 70% 的需求。具体来说就是在构建配置里加一层 overlay 目录比如device/rockchip/rk3588/overlay/然后在里面重建改动文件路径例如frameworks/base/core/res/res/values/config.xml只写需要覆盖的 config。这套机制 AOSP 原生支持SDK 的device/rockchip/rk3588/BoardConfig.mk或device.mk里一般已经配置了 overlay你只需要把文件放对位置编译的时候它会自动合并。资源覆盖层的优点是稳定、不容易编译挂掉缺点是你没法彻底控制 SystemUI 的运行时行为比如下拉通知栏还能不能拉出来这种交互逻辑就靠资源层管不了。2.2 SystemUI 代码层最彻底连下拉面板一起禁掉如果客户端要求极高不允许用户在任何角落拉出通知面板、不允许切换最近任务那就必须动 SystemUI 得意处。在源码里找到NotificationShadeWindowView和NavigationBarController的创建和触摸入口按需求直接短路。改代码的好处是系统行为完全可控坏处是升级 SDK 或者换平台时你的差异补丁要重新适配。如果你的项目是 SDK 长期固定版本非常建议用代码层如果后续要频繁升 Android 版本那最好把代码层的改动控制到最少尽量用资源覆盖。2.3 运行时策略仅调试用固件交付不推荐还有一种办法是跑起来之后通过命令或属性强制 immersive比如adb shell settings put global policy_control immersive.full* adb shell settings put global policy_control immersive.navigation*测试的时候很快能立刻看到效果。但这类设置对设备重启后的持久性依赖系统服务启动顺序而且用户可以随时进设置 - 显示里把它关掉。如果你的设备是公版系统、不锁权限用户一折腾就又冒出来了。所以它只能作为前期验证手段真上线还是要源码改。2.4 我选的组合方案我这台 RK3588 最后用的是资源覆盖层为主SystemUI 代码层为辅的组合资源层关闭config_showNavigationBar让框架层不请求导航栏资源层把status_bar_height和navigation_bar_height全部降到 0SystemUI 代码层屏蔽通知面板的下拉入口在编译包里把policy_control默认写成 immersive.full作为兜底保证一些顽固 App 拿到的 insets 也被压掉。这套组合实际跑下来桌面、预装应用、第三方 APK 基本都是全屏状态没有出现底部白边或顶部灰条。3. RK3588 Android12 源码修改实战3.1 第一步拉取代码并确认 device 目录先保证你本地已经编译通过过一次拿到能正常启动的镜像再进行修改。没有这一步后面任何改动都会分不清是改出来的问题还是环境问题。RK3588 Android12 SDK 拉取后通常能看到如下目录device/rockchip/rk3588/ device/rockchip/common/ frameworks/base/ packages/SystemUI/我建议先看下device/rockchip/rk3588/下是否有overlay目录如果没有就自己建一个。然后看构建配置文件里有没有声明 overlay 路径不同 SDK 版本可能使用DEVICE_PACKAGE_OVERLAYS : device/rockchip/rk3588/overlayPRODUCT_PACKAGE_OVERLAYS : ...如果你不确定 overlay 有没有生效可以从源码的中间产物确认# 编完后查看 framework-res.apk 中相关值 aapt2 dump resources $OUT/system/framework/framework-res.apk | grep config_showNavigationBar3.2 第二步关闭导航栏的核心开关把下面这个文件放到你的 overlay 目录下对应frameworks/base/core/res/device/rockchip/rk3588/overlay/frameworks/base/core/res/res/values/config.xml?xml version1.0 encodingutf-8? resources !-- 关闭导航栏 -- bool nameconfig_showNavigationBarfalse/bool /resourcesconfig_showNavigationBar是框架层判断当前设备是否提供导航栏的关键开关。SystemUI 在启动时会通过WindowManager查询这个值拿到 false 之后就不会创建标准导航栏视图三键导航和手势导航条都会被跳过。这里要特别注意部分国产 App 或者启动器会主动读取导航栏是否存在从而决定自己要不要画一个底部 tab bar比如微信小程序、某些扫码 App。你把config_showNavigationBar关了之后它们可能认为设备没有系统导航栏但自己的底部按钮又没适配功能上没问题视觉上会多一层 padding。这部分属于应用侧适配后面可以用 immersive 兜底来缓解。3.3 第三步把状态栏和导航栏尺寸归零关掉导航栏开关后导航栏默认的基本上不会出来了。但状态栏还挂着因为 Android12 里状态栏的显示逻辑更依赖尺寸资源。你需要在同一层 overlay 里再加一个dimens.xmldevice/rockchip/rk3588/overlay/frameworks/base/core/res/res/values/dimens.xml?xml version1.0 encodingutf-8? resources !-- Android12 状态栏高度 -- dimen namestatus_bar_height0dp/dimen dimen namestatus_bar_height_default0dp/dimen dimen namestatus_bar_height_landscape0dp/dimen dimen namestatus_bar_height_portrait0dp/dimen !-- 导航栏高度兜底 -- dimen namenavigation_bar_height0dp/dimen dimen namenavigation_bar_height_landscape0dp/dimen dimen namenavigation_bar_height_nav0dp/dimen /resources为什么要写这么多维度因为 Android12 里状态栏高度在竖屏和横屏是两个不同资源而导航栏高度在不同系统版本里会引用不同资源名。你只改一个status_bar_height的话横屏桌面切过来可能又冒出来。干脆全部置 0。另外packages/SystemUI/res/values/dimens.xml里也有一些尺寸覆盖部分 SDK 版本会直接引用SystemUI自己的资源而不是 framework 的资源所以最好在下面这个路径再放一份同名覆盖device/rockchip/rk3588/overlay/packages/SystemUI/res/values/dimens.xml?xml version1.0 encodingutf-8? resources dimen namestatus_bar_height0dp/dimen dimen namestatus_bar_height_default0dp/dimen dimen namenavigation_bar_height0dp/dimen dimen namenavigation_bar_height_landscape0dp/dimen /resources注意如果你把status_bar_height改成 0状态栏区域里的时间、电量、Wi-Fi 图标也全部消失因为视图容器高度变成 0 之后里面东西再怎么画都看不见。3.4 第四步在 SystemUI 里设置导航栏不创建如果你只依赖config_showNavigationBarfalse理论上已经够用。但我在实际 RK3588 项目上发现某些 SDK 分支里 SystemUI 的NavigationBarController会强行重新创建导航栏视图尤其是横屏时或接上 HDMI 副屏之后底部又会出现一条系统导航条。为了彻底压制我建议在 SystemUI 源码里找到导航栏控制器加一个硬判断。路径参考packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarController.java在createNavigationBar或者onNavigationModeChanged入口处加逻辑private boolean isCustomNavBarDisabled() { return android.os.SystemProperties.getBoolean(persist.sys.hidenav, true); }然后在创建导航栏的方法开头if (isCustomNavBarDisabled()) { return null; }这样不管是系统刚启动还是横竖屏切换、副屏插拔都不会补建导航栏。用SystemProperties的好处是后续你想临时开回来排查问题只需要adb shell setprop persist.sys.hidenav false然后重启 SystemUI不需要重新编译整个固件。这个persist.sys.hidenav名字是我自己起的只要你代码里前后一致就行。实际上你也可以用persist.sys.navbar.disabled之类的名字。如果你不想加属性开关直接写return null也可以只是后续排查时不太方便。3.5 第五步在 SystemUI 里屏蔽通知面板下拉这一步很多人会漏掉导致状态栏没了但屏幕顶部边缘用手指一抹通知面板还是能拉出来。Android12 上通知面板的阴影层由NotificationShadeWindowView负责状态栏只是它的挂载点。状态栏缩成 0 高度后触摸响应区域理论上也变小但某些版本会把手势区域扩大到几乎全屏方便用户在顶部通过 App 内滑动唤出通知中心。这样在自助终端上非常危险用户随便从顶部下滑就把系统菜单调出来了。我的做法是找到 SystemUI 里NotificationShadeWindowController或者NotificationShadeWindowViewController把它的触摸事件拦截关掉。不同 SDK 差异较大提供一个通用排查思路首先编译烧录后抓当前窗口信息adb shell dumpsys window windows | grep -E NotificationShade|Shade|StatusBar|Taskbar找到对应窗口类名再回源码里搜NotificationShadeWindowView它的触摸拦截通常由onInterceptTouchEvent或ViewConfiguration相关代码控制。在你需要禁用的分支里直接让触摸事件返回 falseOverride public boolean onInterceptTouchEvent(MotionEvent ev) { // RK3588 定制固件禁止从顶部下拉通知栏 if (mIsCustomLockDown) { return false; } return super.onInterceptTouchEvent(ev); }如果你使用的 SDK 版本里这个方法不存在也可以更简单粗暴一点把NotificationShadeWindowView从onLayout或onMeasure阶段拿到高度 0触摸区域自然也没有了。不过这会导致某些预装应用配合窗口 insets 时计算异常建议优先用触摸拦截方法。如果不允许通知面板显示还有一处要改packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java或CentralSurfacesImpl.java里搜索start方法观察它是否启动了一个NotificationShadeWindow的窗口。你可以在启动后立即调用一次mNotificationShadeWindowController.getNotificationShadeWindowView().setVisibility(View.GONE);这样即使触摸逻辑没拦住视觉上也不会露出。3.6 第六步让全局 immersive 策略默认开启源码层还有一个兜底招直接改PhoneWindowManager让系统启动后自动把沉浸式全屏策略应用到所有应用。原理是系统在Settings.Global.POLICY_CONTROL_IMMERSIVE_FULL里存字符串形如immersive.full*这个值会在系统策略控制里被应用层读取强制系统栏隐藏。我见过很多厂商的方案是在系统服务开机时通过Settings.Global.putStringForUser写入你可以在frameworks/base/packages/SystemUI的一个BootReceiver里执行也可以在device/rockchip/rk3588下的 init rc 脚本里用settings命令写入# init.rc 或你的开机脚本里添加 on boot setprop sys.boot_completed 1 # 等待系统就绪后再写入全局沉浸配置 exec u:r:system_app:s0 -- /system/bin/settings put global policy_control immersive.full*如果你希望更彻底一点直接改源码在PhoneWindowManager.loadSettings()方法末尾加入Settings.Global.putStringForUser( mContext.getContentResolver(), Settings.Global.POLICY_CONTROL_IMMERSIVE_FULL, immersive.full*;immersive.navigation*, ActivityManager.getCurrentUser());这段代码会让WindowManager在加载系统策略时默认对所有应用开启 immersive应用自以为拿到的系统栏 insets 已经是 0整体布局会贴近屏幕边缘。我提醒一句这么改后一定要在设置 App 里多测几个页面因为设置自身可能因为强制 immersive 出现一边倒或顶部重叠的问题。但对我们常见的自助终端/工控面板来说设置 App 反正也不需要给用户用无所谓。3.7 第七步千万别忘了编译进的预置 App很多 RK3588 项目里会预装自己的 Launcher比如一个只有几个大按钮的商显 Launcher。如果你只改系统层但 Launcher 还在调用setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)那就没意义了。最简单的做法Launcher 里直接在onCreate或onResume使用WindowInsetsControllergetWindow().setDecorFitsSystemWindows(false); WindowInsetsController controller getWindow().getInsetsController(); if (controller ! null) { controller.hide(WindowInsets.Type.systemBars()); controller.setSystemBarsBehavior( WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); }不过这不是本文重点只是想提醒你系统层做到位后预置 App 自己不要再把系统栏调出来。4. 编译、打包、验证一次跑通4.1 编译命令与常见报错如果你用的 Rockchip 官方 SDK通常source build/envsetup.sh lunch rk3588-userdebug然后make -j$(nproc)全编译耗时会比较长RK3588 这种项目首次全编建议至少准备 16 核的机器内存建议 32G 以上。你也可以只编部分镜像来快速验证make framework-res # 验证 overlay 是否生效 make SystemUI如果只想编 framework 相关的framework-res.apk然后单独推镜像测试adb root adb remount adb push $OUT/system/framework/framework-res.apk /system/framework/framework-res.apk adb push $OUT/system/framework/arm64/boot-framework.oat /system/framework/arm64/boot-framework.oat adb reboot但改动跨到 SystemUI、SystemServer、PhoneWindowManager 时还是老老实实整包编译不然会遇到 dex 优化不一致导致的类冲突、行为异常。我初期为了省时间只推了 framework-res.apk结果 SystemUI 还是按旧资源启动状态栏也还在排查了很久才发现是 boot image 里也嵌了一份资源。4.2 烧录后如何确认六个关键状态烧录完别急着测应用先用命令确认系统到底认不认你的修改。我一般会按顺序看这几项config_showNavigationBar是否已经关掉adb shell dumpsys window | grep -E config_showNavigationBar|mNavigationBarVisible状态栏窗口是否还存在adb shell dumpsys window windows | grep -E StatusBar|NotificationShade正常做到底后上面这些窗口要么不存在要么高度为 0。窗口还在但高度 0 是可以接受的因为系统组件还需要这个窗口接收某些事件。全局策略是否生效adb shell settings get global policy_control看到immersive.full*就是对了。应用窗口拿到的 insets 是不是 0adb shell dumpsys window | grep -E mInsetsStateController|mSystemBars系统属性是否正常adb shell getprop persist.sys.hidenav实际截图看效果adb shell screencap -p /sdcard/screen.png adb pull /sdcard/screen.png如果第 4 步还看到非零 insets优先确认资源 overlay 是否真的被编译进去了。可以在 host 上反查 framework-res.apkaapt2 dump resources $OUT/system/framework/framework-res.apk | grep -A1 config_showNavigationBar aapt2 dump resources $OUT/system/framework/framework-res.apk | grep -A1 status_bar_height4.3 开机自启 Kiosk 应用的联动设置既然是要彻底隐藏通常系统里只有一个全屏的 Launcher 或业务 App。我建议在packages/apps/下做一个自定义SystemUI隐藏开关的BroadcastReceiver开机后自动进入 LockTask 模式DevicePolicyManager dpm (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE); if (dpm.isLockTaskPermitted(getPackageName())) { startLockTask(); }LockTask 模式下用户无法退出应用再配合我们源码层隐藏的两条栏才算真正锁死。这一步不是必须的但如果你做的是商场导览机、门禁管理机建议加上。5. 我实际踩过的坑以及恢复现场的方法5.1 状态栏没了但是顶部出现一条难看的白边这个坑我印象特别深。第一次改完烧录后桌面确实没有状态栏和导航栏了但顶栏区域有一条宽度大约 24dp 的白边手指点上去能触发通知面板。看dumpsys才发现虽然status_bar_height已经是 0但SystemUI用的还是系统进程里旧的资源缓存这也是 SDK 经常出现的问题资源覆盖层编译进去了但 SystemUI 的ResourcesManager在开机时提前加载了同一份资源 ID导致尺寸没更新。解决方案很直接把 SystemUI 的设置和framework-res设置保持一致并且在验证时必须先执行adb shell stop adb shell start或者干脆重启系统。开机后如果还有白边多半是你覆盖的资源名不对例如只改了status_bar_height但代码里引用的是status_bar_height_default。重新运行上面的aapt2命令把实际覆盖结果对一下。5.2 状态栏没了但下拉通知面板还能拉出来这个前面讲了原理。我改完status_bar_height0后发现屏幕最顶部 1~2 像素区域还是能触发下拉这就是NotificationShadeWindowView的触摸区域被做大了。我在代码层把触摸拦截掉后问题才彻底解决。具体你怎么找这段代码建议先在代码里搜索NotificationShadeWindowView再搜索intercept或touch。不同版本实现位置差异较大但基本都是围绕这个类。改完以后专门用手指在屏幕顶部、应用内顶部边缘反复滑动确认没有任何下拉弹层。5.3 底部输入法弹出来后界面被顶上去一块隐藏了导航栏后输入法弹出来通常没问题因为导航栏高度是 0输入法窗口的 insets 会直接作用于应用底部不会再叠加一条系统栏。但如果你把config_showNavigationBarfalse之后某些旧版输入法会检测不到导航栏以为自己是全屏键盘底栏高度就异常了。我实际遇到的一次是搜狗输入法把“输入法自身工具栏”显示出来了因为系统告诉它设备没有导航栏它就把系统栏的位置用自己的工具栏填了。解决方式有两个要么在DeviceConfig或SettingsProvider里配置默认输入法为定制版要么让输入法走adjustResize模式不要用adjustPan。这是应用侧的适配问题但对隐藏系统栏的固件来说很常见。5.4 没有源码也能应急的调试命令如果你在客户现场只有板子没法立刻烧一个新固件可以先应急用命令压制一下虽然没有源码级那么彻底但至少能撑到下次烧录adb shell settings put global policy_control immersive.full* adb shell settings put global policy_control immersive.navigation* adb shell wm density 420 adb shell wm size 1080x2160 adb shell cmd statusbar disable 61465最后一行cmd statusbar disable可以禁用通知图标、下拉入口等状态栏能力。但注意这个disable值在不同 Android 版本上会变你最好在 AOSP 源码里查一下StatusBarManager.DisableFlags的定义再传。5.5 编译通过但开机黑屏/重启循环通常如果不小心把config_showNavigationBar或status_bar_height改成 0 导致黑屏不太可能是死循环的原因。真正容易死循环的是你在PhoneWindowManager.loadSettings()里加了Settings.Global.putStringForUser而该方法的调用发生在系统还没有准备好在当前用户里写入设置的时候可能触发空指针。我在一个分支上遇到过系统服务里抛SecurityException导致WindowManagerService启动失败开机进 Recovery。后来我加了一层层 try-catch并且在成功写入前判断if (mContext.getUserId() UserHandle.USER_SYSTEM) { ... }如果你没有把握那就不要在PhoneWindowManager里写 Settings改用 init 脚本的settings命令系统层面更稳定。5.6 排查链路一个奇怪的全屏应用底部留白案例最后分享一次比较典型的排查过程。客户预装了一个视频 App明明系统栏都隐藏了但 App 播放页面底部总有一块等高的暗色区域刚好就是导航栏原来 48dp 的位置。第一次碰到我还以为是 App 做了刘海屏适配抓窗口信息后发现adb shell dumpsys window | grep -E NavigationBar|mSystemBars|policy输出显示系统已经没有任何导航栏但 App 自己通过ViewCompat.setOnApplyWindowInsetsListener拿到的 insets 底部仍然是非零。原因是 SDK 的SystemUI还保留了一个全屏窗口它的 insets source 没有被完全抹平App 读到的底部 insets 就是那块区域。我当时的处理是两步在NavigationBarController里完全不再创建任何导航栏窗口在框架层WindowManagerService输出 insets 时把WindowInsets.Type.navigationBars()全部置 0代码对应的位置一般在PhoneWindowManager的getNonDecorInsetHeight或setUserActivity相关逻辑里。对大多数项目来说不需要做到第二步因为 Android12 的资源层和 immersive 策略已经把大部分 insets 处理了。如果你的用户群里有特别老的 App它们没按新 insets 规范开发那就得用这一步兜底。6. 最后再说两句改完这套以后我的 RK3588 设备已经连续开着跑了两三周没有出现过系统栏复活的情况也没有因为status_bar_height0出现布局错乱。最值得强调的是隐藏导航栏和状态栏不能只靠某一个文件或某一行配置必须把config_showNavigationBar、status_bar_height、navigation_bar_height、SystemUI 的触摸入口以及全局 immersive 策略联动起来才算真正的彻底。你在自己的平台上动手之前先把源码分支确定尤其是 Rockchip SDK 里 SystemUI 的代码路径多看几个类比直接在网上找现成补丁更有用。最后再分享一个小技巧改完 SystemUI 后不要急着整包烧录可以先把编译出来的 SystemUI.apk 用adb push到设备里临时验证确认交互没问题了再做全量包。这样每次迭代能节省不少时间也能降低反复烧录损坏板子的风险。