☰
RK3588 Android12系统定制:源码级彻底隐藏导航栏与状态栏方案
2026/9/28 8:05:08 网站建设 项目流程

最近在搞 RK3588 开发板的 Android12 系统定制,客户给了个挺硬的需求:机器出厂后,导航栏和状态栏必须彻底消失,整个屏幕只能显示自家应用,不能让用户切出去,也不允许任何系统栏露出来。我第一反应是走沉浸式那套方案,但试了一圈发现,App 层处理沉浸式只能管住自己,一旦用户回到桌面、弹出系统弹窗或者切到设置页,导航栏和状态栏又会冒出来。没办法,最后还是回到源码修改这条路,从 SystemUI 这一层把系统栏“掐死”,才算真正解决。

这篇东西适合正在做 RK3588、RK3568 这类瑞芯微平台 Android 系统定制的朋友,尤其是做广告机、一体机、自助终端、会议平板的,几乎都会碰到这类需求。我会把完整的修改思路、具体代码位置、编译验证方法和踩坑记录都写出来,尽量让你拿到就能照着改。

1. 隐藏前先搞清楚:导航栏和状态栏到底是谁管着的

1.1 三种常见隐藏方案,为什么最后选了源码改

先说说项目里试过的几种方案,方便你判断自己到底该走哪条路。

第一种是 App 层沉浸式,也就是在应用里调用 WindowInsets、SYSTEM_UI_FLAG_HIDE_NAVIGATION 这类 API,把系统栏临时隐藏掉。优点是改起来快,不用碰系统源码;缺点是只管得住当前应用,一旦弹窗、对话框、系统级界面出现,或者应用切到后台再回来,系统栏就会重新出现,而且部分设备上还会有底部白条闪一下。对于产品化项目来说,这种“时灵时不灵”的状态很难接受。

第二种是用 adb 命令或设置数据库来隐藏,比如直接执行 settings 相关的命令,或者通过系统设置里的隐藏导航栏选项。这个方案比 App 层稍好一点,但依然不是根上的解决:用户在某些设置入口里可以把导航栏调回来,而且有些 Android12 版本对状态栏的隐藏支持并不完整,重启后也不一定能保持。

第三种就是源码级修改,直接改 framework 和 SystemUI 的代码,从系统层的渲染链路上把导航栏、状态栏拿掉或者彻底缩小到 0。改完之后,系统自身已经不再绘制这两条栏,任何应用都唤不出来,重启也出不来,才符合商用终端的出货要求。代价是要编译整个系统,调试周期长一点,还得懂点 Android Framework 的常识。

我最后选择的是第三种方案,并且额外做了一个系统属性开关,方便产线调试时临时把导航栏调回来。这个开关后面会讲,你如果只是要“隐藏”这一个结果,前面的核心步骤已经足够。

1.2 Android12 的 SystemUI 架构变化,改动前必须知道的几个点

Android 12 的 SystemUI 做了非常大的一次重构。以前版本里的 StatusBar.java 是核心入口,但到了 Android12,大量逻辑被拆分到了 CentralSurfaces 接口和 CentralSurfacesImpl.java 里,StatusBar.java 本身变成了一个偏壳的类。导航栏这边则引入了 NavigationBarFragment,由 Fragment 来管理三键导航区、手势指示条和返回箭头。

这个架构变化对修改的影响很大。你在网上搜 Android 11 的隐藏导航栏教程,很多会告诉你去改 StatusBar.java 里的某个方法,但放到 Android12 的 RK3588 SDK 里,可能这个类已经变了,或者方法被抽到别的接口里了。所以我会在下面标注清楚:修改时一定要以你手上的 SDK 源码为准,先搜索类名确认存在再做改动。

另外有一点必须提醒:瑞芯微的 RK3588 Android12 SDK 并不完全是干净的 AOSP,它里面加了瑞芯微自家的一些显示适配、多屏输出、硬件解码相关的代码,SystemUI 的代码路径可能和 Google 原版有差异。最典型的区别是,瑞芯微 SDK 的导航栏和状态栏在横竖屏、HDMI 热插拔时会有额外的刷新逻辑,所以有时候你改了某个值,默认方向生效了,切到横屏又冒出来,就是因为还有另一套 dimens 配置没改到。

2. 源码修改第一步:全局配置和尺寸做零化

2.1 关闭三键导航的系统开关

先改最基础的一个配置项:config_showNavigationBar。这个布尔值定义在 framework-res 的配置里,PhoneWindowManager 等核心类会读取它来判断系统是否需要创建虚拟导航栏。

文件路径:

  • frameworks/base/core/res/res/values/config.xml

打开后搜索config_showNavigationBar,默认值一般是 true,把它改成 false:

<bool name="config_showNavigationBar">false</bool>

这里要解释一下为什么先改它。如果不关掉这个开关,后面的 SystemUI 代码即使把导航栏 View 设成 GONE,系统在 WindowManager 那一层仍认为有导航栏空间,某些应用拿到的 window insets 里还是会带一个底部高度,布局上容易出现底部多出一块空白。提前关掉这个开关,相当于告诉整个系统框架“这台设备没有虚拟导航栏”,从根上就把导航栏的“合法存在”取消了。

状态栏没有像这样的全局开关,framework-res 里没有一个叫config_showStatusBar的布尔量能直接关闭它。这也是为什么状态栏后面必须在 SystemUI 代码层处理,而不是靠一个配置项就能解决。

改完这个之后,接下来要动 dimens,把两条栏的尺寸全部归零。

2.2 把系统栏高度/宽度全部改成 0,给两种栏的布局“清零”

只改config_showNavigationBar还不够,手势导航模式下的白色横条、以及状态栏的高度仍然会占据屏幕空间。最省事的做法是把 SystemUI 里和状态栏、导航栏相关的尺寸值全部改成 0。

文件路径:

  • frameworks/base/packages/SystemUI/res/values/dimens.xml
  • frameworks/base/packages/SystemUI/res/values-land/dimens.xml
  • frameworks/base/packages/SystemUI/res/values-sw600dp/dimens.xml

默认 dimens.xml 里主要有这些项:

<dimen name="status_bar_height">24dp</dimen> <dimen name="status_bar_height_portrait">24dp</dimen> <dimen name="status_bar_height_landscape">24dp</dimen> <dimen name="navigation_bar_height">48dp</dimen> <dimen name="navigation_bar_height_landscape">48dp</dimen> <dimen name="navigation_bar_width">42dp</dimen> <dimen name="navigation_bar_edge_width">0dp</dimen>

把这些值全部改成0dp:

<dimen name="status_bar_height">0dp</dimen> <dimen name="status_bar_height_portrait">0dp</dimen> <dimen name="status_bar_height_landscape">0dp</dimen> <dimen name="navigation_bar_height">0dp</dimen> <dimen name="navigation_bar_height_landscape">0dp</dimen> <dimen name="navigation_bar_width">0dp</dimen> <dimen name="navigation_bar_edge_width">0dp</dimen>

注意,这才是我说的“从头改到尾”的关键。很多朋友只改代码隐藏 View,不改 dimens,结果应用拿到的 insets 里依然包含系统栏高度,全屏布局会被无端压缩。dimens 改成 0 后,WindowManager 在计算窗口 insets 时就不会给应用多留空间了。

瑞芯微的 SDK 里,横屏和平板尺寸的 dimens 文件尤其重要。RK3588 经常外接 HDMI 大屏,横屏场景非常常见,如果你只改默认 values/dimens.xml,横屏下导航栏照样可能闪出来。所以 values-land、values-sw600dp 这些变体目录一定要一起改,别漏。

2.3 手势导航指示条的处理,不处理干净会留一个“半透明小白条”

Android12 里有一种情况很坑:config_showNavigationBar设为 false、dimens 也全部归零之后,底部有时还是会残留一条几像素高的半透明白条,那是手势导航的指示条(GestureHintView)。它不是传统意义上的三键导航栏,而是手势导航模式下的一根小白线,由 SystemUI 的 NavigationBarFragment 在初始化时绘制。

处理方式有两种。第一种是在布局阶段直接屏蔽:在 NavigationBarFragment 里找到mGestureHintView,把它也设成 GONE。第二种更彻底:在系统设置里把导航模式切成 3 按钮模式,然后不让三键导航显示。不过在源码层我更推荐直接改 SystemUI 代码,后面第 3 节会详细写。

如果你不想动代码,也可以考虑把config_navBarInteractionMode这个配置项设为三键导航模式,但那样可能会在设置里暴露导航切换选项,对商用终端不友好,所以我没有采用这个路子。

3. 源码修改第二步:SystemUI 代码级隐藏,杜绝残留

3.1 状态栏部分:从 CentralSurfacesImpl 的启动链路下手

状态栏的隐藏,我选择在 SystemUI 的启动阶段直接处理。Android12 里,状态栏的显示逻辑主要在frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java(部分 SDK 里仍在 StatusBar.java),你搜索一下CentralSurfacesImpl这个类,找不到就看 StatusBar.java。

核心思路是:在 SystemUI 初始化完成、状态栏 View 创建之后,立刻把它从屏幕上拿掉。Java 里的实现可以放在start()方法或者onStatusBarViewCreated之类的回调里。

参考片段如下:

// CentralSurfacesImpl.java 或其他 StatusBar 初始化类中 @Override public void start() { // ...原有逻辑... if (needHideSystemBar()) { // 找到状态栏对应的 WindowView,直接隐藏 if (mNotificationShadeWindowViewController != null) { mNotificationShadeWindowViewController.getView().setVisibility(View.GONE); } // 如果系统有 PhoneStatusBarViewController,也一并隐藏 if (mPhoneStatusBarViewController != null) { mPhoneStatusBarViewController.getView().setVisibility(View.GONE); } } }

这段代码里,mNotificationShadeWindowViewController负责的是整个通知阴影窗口,也就是从屏幕顶部往下展开的整个系统 UI 层,里面包含状态栏。把它 GONE 掉之后,状态栏和下拉通知都会一起消失。

需要说明的是,不同 SDK 里这个类的字段名可能不完全一样,你直接搜NotificationShadeWindowViewController或者PhoneStatusBarViewController,找到对应实例再处理。如果没有那么复杂的注入结构,更粗暴一点的方式是直接在makeStatusBarView创建完视图后,对根布局调用setVisibility(View.GONE)。

关键点在于:用 GONE 而不是 INVISIBLE。INVISIBLE 只是让 View 不可见,但还占着布局空间,应用仍然拿不到全屏;GONE 则会把 View 从布局中彻底移除,WindowManager 计算出来的 insets 才会跟着变成 0。

3.2 导航栏部分:NavigationBarFragment 的一行 GONE

导航栏在 Android12 中被设计成一个 Fragment,文件路径:

  • frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarFragment.java

这个 Fragment 在onViewCreated里会创建NavigationBarView。我们直接在这个回调里做判断,把整个 View 隐藏掉。

参考代码:

@Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // 这里原本有一堆 inflate 和初始化逻辑 mNavigationBarView = (NavigationBarView) view; // 产品化定制:默认隐藏导航栏 if (needHideSystemBar()) { mNavigationBarView.setVisibility(View.GONE); } }

needHideSystemBar()是我自己在 SystemUI 里写的一个方法,用来读系统属性,后面第 3.3 节会讲。如果你不想做属性开关,直接写死mNavigationBarView.setVisibility(View.GONE)也没问题。

导航栏这里建议在onViewCreated里直接隐藏,而不是在 show 事件里处理。因为 NavigationBarFragment 的生命周期会被系统多次触达,比如亮屏、熄屏、横竖屏切换、输入法弹出时,它都可能重新走一遍初始化。如果你只是“在某个时机设了一次 GONE”,后续系统重新把它加回来,导航栏就“诈尸”了。在onViewCreated里控制,相当于在每次重建视图时都强制隐藏,可靠性高得多。

另外,我还会顺手在处理手势条的地方加一层保险:找到mGestureHintView,同样setVisibility(View.GONE)。这个字段在部分 SDK 里存在,作用就是画那根小白线。

if (mNavigationBarView != null) { mNavigationBarView.setVisibility(View.GONE); View gestureHint = mNavigationBarView.findViewById( com.android.internal.R.id.gesture_hint_view); if (gestureHint != null) { gestureHint.setVisibility(View.GONE); } }

这一步做完,导航栏的“尸体”都清了。

3.3 用系统属性做总开关,方便产线随时切换

实际做产品时,经常遇到“出厂要隐藏,但产线上测试时又想临时调出导航栏”的矛盾。为此我建议在 SystemUI 里加一个系统属性开关:ro.app.hide_systembar,默认值为 true,SystemUI 启动时读取它来决定是否隐藏两条栏。

方法很简单,在 SystemUI 的代码里加一个工具方法:

private boolean needHideSystemBar() { return android.os.SystemProperties.getBoolean("ro.app.hide_systembar", true); }

然后把第 3.1 和 3.2 节里两处if (needHideSystemBar())判断用上。这样产品在量产阶段默认隐藏,调试时如果要临时显示,只需在板子上执行:

adb root adb remount adb shell setprop ro.app.hide_systembar false adb shell stop && adb shell start

或者直接写进产品配置里,在device/rockchip/rk3588/rk3588.mk中加入:

PRODUCT_PROPERTY_OVERRIDES += ro.app.hide_systembar=true

以后哪批货想打开导航栏,只要改这一行属性重新打包就行,不用再动 Java 代码。我在多个项目里都用这个方案,产线兄弟从没因为这事情回来找过我。

4. 编译、烧录与验证

4.1 单编 SystemUI 和 framework-res 的正确姿势

改完源码,接下来就是编译。RK3588 的 Android12 SDK 一般有两种编译方式:全编和单编。全编时间太长,如果只是改 SystemUI 和 framework-res,建议单编,能省下一大把时间。

先初始化编译环境:

source build/envsetup.sh lunch rk3588-userdebug

然后单独编译 framework-res 和 SystemUI:

make framework-res -j16 make SystemUI -j16

如果你的 SDK 版本较老,mmm写法也可以:

mmm frameworks/base/core/res mmm frameworks/base/packages/SystemUI

编译产物会在out/target/product/rk3588/下。framework-res 对应的是 framework-res.apk,SystemUI 对应的是 SystemUI.apk。不同 SDK 里 SystemUI 可能被编到 system_ext 分区还是 product 分区,路径不太一样,编译完直接搜一下.apk就行。

4.2 推包挂在 system 分区的注意事项

单编完推包时,要注意分区挂载。RK3588 Android12 的 SystemUI 一般放在/system_ext/priv-app/SystemUIGoogle/或者/system/priv-app/SystemUI/下。先执行:

adb root adb remount

然后找到编译产物路径,push 到对应目录:

adb push out/target/product/rk3588/system_ext/priv-app/SystemUIGoogle/SystemUIGoogle.apk /system_ext/priv-app/SystemUIGoogle/ adb shell chmod 644 /system_ext/priv-app/SystemUIGoogle/SystemUIGoogle.apk

framework-res 推包路径一般是:

adb push out/target/product/rk3588/system/framework/framework-res.apk /system/framework/ adb push out/target/product/rk3588/system/framework/framework-res.apk /system/system_ext/framework/

有的 SDK 里 framework-res 同时存在于 system 和 system_ext 两份,两个位置都要推,否则开机后系统可能还是沿用旧配置。推完记得同步一下权限:

adb shell chmod 644 /system/framework/framework-res.apk adb shell chmod 644 /system/system_ext/framework/framework-res.apk adb reboot

个人经验:能全编还是尽量全编一次再烧机,单编推包适合快速迭代,但如果改动涉及 framework-res 和 SystemUI 的跨分区依赖,容易出现文件版本不一致导致的奇怪问题,比如 SystemUI 启动崩溃或者重叠绘制。我一般是先用单编快速验证代码逻辑,确认没问题后再全编出镜像给产线。

4.3 验证命令与检查项

重启之后,怎么确认系统栏真的没了?

先看窗口状态:

adb shell dumpsys window windows | grep -iE "navigation|statusbar"

如果你能看到mShowingLw=false或者相关 Bar 的视图信息为空,说明窗口层已经没有系统栏了。

再看窗口 insets:

adb shell dumpsys window insets

重点看contentInsets和visibleInsets,理想状态是顶部和底部的 inset 都为 0。如果这里还有数值,说明 dimens 没改干净,或者 config.xml 里的开关没生效。

最后用截图看一眼实际效果:

adb shell screencap -p /data/local/tmp/screen.png adb pull /data/local/tmp/screen.png

正常状态下,截图应该是满屏全屏内容,顶部没有时间、电池图标,底部没有三键导航或手势小白条。

另外,建议再用adb shell dumpsys window policy看一眼当前的 navigation bar 状态。注意这里会显示系统认为的导航栏是否可见,如果mNavigationBarVisible=false,基本就到位了。

5. 踩坑实录:那些“隐藏了又冒出来”的疑难杂症

5.1 顶部出现一块黑色或空白区域

这个坑我刚开始也踩过。只改了 SystemUI 代码把状态栏 GONE,但 dimens 里的status_bar_height没改,结果顶部虽然看不到状态栏了,应用的内容却像被系统栏压着一样,顶部有一条黑色或空白区域,点击事件也进不去。

排查方法很简单,dumpsys window insets一看,顶部 inset 还是 24dp 或 28dp,那就知道是 dimens 没清零。把values/dimens.xml、values-land/dimens.xml、values-sw600dp/dimens.xml里的状态栏高度全部改成 0dp,重新编译 framework-res 后再试。

5.2 熄屏亮屏、横竖屏切换后导航栏“诈尸”

这个问题非常经典。只把某个临时show()方法里的导航栏 GONE 掉,但 NavigationBarFragment 在横竖屏切换或熄屏亮屏时会重新创建 View,原来的隐藏状态就丢了,导航栏又出现在底部。

解决办法就是我前面强调的:把隐藏逻辑放在onViewCreated里,而不是放在某个一次性事件回调里。这样每次 Fragment 重建视图时都会重新执行隐藏,不会出现漏网之鱼。

如果加了属性开关,还要确保needHideSystemBar()在每次 onViewCreated 时都被重新调用,不要用一个静态变量缓存结果。

5.3 弹出输入法时,底部输入框被系统栏空位顶起

部分全屏应用在输入框聚焦时会调用adjustResize,这时 WindowManager 会给 IME 预留空间。如果系统栏的尺寸没完全清零,底部会多出一段空白,看起来就像导航栏“复活”了,只不过它是透明的。

这个问题更多是应用层和系统层的显示策略冲突。如果你们有自己的 App,可以在 App 的 Activity 里设置WindowCompat.setDecorFitsSystemWindows(getWindow(), false),让应用内容真正放到屏幕最底部,配合系统栏尺寸为 0,就不会被顶起来了。

如果是第三方 App,确实没法改代码,那就只能靠系统层把status_bar_height、navigation_bar_height全部清零,让 WindowManager 认为没有任何系统栏空间,IME 布局也能恢复正常。

5.4 桌面 Launcher 和第三方 App 的全屏适配问题

系统栏隐藏之后,原来依赖状态栏做避让的 Launcher 或部分 App 会出现内容顶到屏幕边缘的情况,比如文字被刘海遮挡或者按钮贴边。对 RK3588 这种方形屏工控板来说,刘海问题几乎没有,更多是底部虚拟按键区域的点击热区消失,导致某些 App 里原本需要点击“三键”才能完成的操作没地方点了。

这个问题属于产品定义层面。商用终端如果是全屏单应用模式,一般不存在这个问题;但如果你的设备还需要进入系统设置或者打开第三方 App,建议在应用层适配全屏显示:启动 Activity 时设置沉浸式布局,并自行在 App 内部提供返回、主页按钮。

5.5 不同 Android12 小版本、不同定制 SDK 的差异性

最后说一个容易让人头疼的事:瑞芯微的 SDK 版本很多,同一个 RK3588 平台,可能基于 Android12 的小版本都不一样,SystemUI 代码位置和类名也有差异。有些 SDK 里CentralSurfacesImpl已经被提前合并,有些还在用老式的 StatusBar 结构。

我的建议是不要死记代码路径,而是用搜索代替记忆:在代码根目录执行:

grep -rn "class CentralSurfacesImpl" frameworks/base/packages/SystemUI/src/ grep -rn "class NavigationBarFragment" frameworks/base/packages/SystemUI/src/

找到实际存在的类之后,再按照“启动阶段强制 GONE + dimens 全部清零”的思路去改,基本不会跑偏。

最后再分享一个我的工作习惯:改完系统栏隐藏后,先别急着把属性开关关掉。我会在开机脚本里加一条 n 秒延迟的日志输出,把当前系统栏的显示状态写到 logcat 里,现场看板子跑个半天,确认没有“诈尸”情况后再把调试日志删掉。这样比反复人工盯屏要靠谱得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询