24、系统UI适配:隐藏导航栏、沉浸模式与系统栏控制
做TV端开发,有个绕不开的坎——系统UI。说白了,就是那个导航栏、状态栏,还有各种系统级的浮层。我刚开始做大屏适配时,以为只要把布局撑满屏幕就完事了。结果呢?用户一按遥控器,导航栏弹出来,把界面底部内容挡得严严实实。嗯,那场面,挺尴尬的。
今天咱们就把这块彻底讲透。系统UI适配,核心就三件事:隐藏、沉浸、控制。
为什么系统UI会成为问题?
Android TV设备五花八门。有的电视自带虚拟导航栏,有的用物理按键,还有的用红外遥控。系统UI的显示策略,每个厂商改得都不一样。
我遇到过一台国产电视,系统导航栏高度足足有80dp。你想想看,一个1920x1080的屏幕,底部80dp被导航栏吃掉,留给应用的实际区域就缩水了。更坑的是,有些设备在播放视频时,导航栏会自动隐藏,但一按遥控器又弹出来——这体验,用户不骂娘才怪。
核心知识点一览
先看一张图,把整个知识体系串起来。我习惯先搭框架再填细节,这样不容易漏。
隐藏导航栏:最基础的操作
隐藏导航栏,Android提供了SYSTEM_UI_FLAG_HIDE_NAVIGATION这个flag。用法很简单:
// 隐藏导航栏
View decorView = getActivity().getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
decorView.setSystemUiVisibility(uiOptions);
但有个坑——用户只要触摸屏幕任意位置,导航栏就会重新出现。这在TV上尤其烦人。用户按一下遥控器,导航栏弹出来,然后就不回去了。
HIDE_NAVIGATION 只是临时隐藏。用户交互后系统会自动恢复导航栏。你需要监听系统UI变化事件,在恢复后再次隐藏。
我曾经在一个直播项目里踩过这个坑。用户看球赛正嗨,按一下遥控器换台,导航栏就赖着不走,挡住了比分信息。后来怎么解决的?我加了一个OnSystemUiVisibilityChangeListener,检测到导航栏显示后,延迟200ms再次隐藏。
decorView.setOnSystemUiVisibilityChangeListener(
new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
// 导航栏又出来了,再藏一次
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
}
});
沉浸模式:真正的全屏体验
隐藏导航栏只是第一步。真正让用户感觉「这应用就是全屏的」,得用沉浸模式。Android 4.4(API 19)引入了IMMERSIVE和IMMERSIVE_STICKY两个flag。
| 模式 | 行为 | 适用场景 |
|---|---|---|
| IMMERSIVE | 隐藏系统栏,用户从边缘滑动时系统栏出现,且不会自动消失 | 游戏、需要用户主动退出的场景 |
| IMMERSIVE_STICKY | 隐藏系统栏,用户从边缘滑动时系统栏短暂出现,然后自动消失 | 视频播放、阅读器、全屏展示 |
我个人习惯在视频播放器里用IMMERSIVE_STICKY。用户不小心滑到边缘,系统栏闪一下就回去了,不影响观看。但如果是游戏,我会用IMMERSIVE,因为游戏里用户需要明确知道怎么退出全屏。
// 沉浸模式(粘性)
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
系统栏控制:Android 11+ 的新方式
从Android 11开始,Google推荐使用WindowInsetsController来控制系统栏。旧的setSystemUiVisibility方法在API 30+被标记为废弃。
说实话,新API确实好用。它把系统栏的控制权交还给应用,而且支持更精细的控制。
// Android 11+ 推荐方式
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
// 隐藏导航栏和状态栏
controller.hide(WindowInsets.Type.navigationBars()
| WindowInsets.Type.statusBars());
// 设置系统栏行为:从边缘滑动显示
controller.setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
}
这里有个细节要注意:BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE对应的是旧版的IMMERSIVE_STICKY行为。如果你想要用户滑动后系统栏一直显示,用BEHAVIOR_SHOW_BARS_BY_SWIPE。
兼容性处理:老设备不能丢
TV设备更新换代慢。我见过不少Android 5.0、6.0的电视还在服役。所以兼容性必须做。
public static void hideSystemUI(Activity activity) {
View decorView = activity.getWindow().getDecorView();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+
WindowInsetsController controller = activity.getWindow()
.getInsetsController();
if (controller != null) {
controller.hide(WindowInsets.Type.navigationBars()
| WindowInsets.Type.statusBars());
controller.setSystemBarsBehavior(
WindowInsetsController
.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
} else {
// Android 4.4 - 10
int flags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(flags);
}
}
避坑指南
做系统UI适配,我踩过的坑能写满一页A4纸。挑几个典型的说说:
- 布局被导航栏遮挡——设置了
SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION后,布局会延伸到导航栏下方。记得给底部留出安全区域,或者用WindowInsets动态调整。 - 焦点丢失——隐藏导航栏后,有些遥控器的「返回」键会失效。这是因为系统把按键事件拦截了。我一般会在
onKeyDown里手动处理返回键。 - Activity重建——配置变化(比如横竖屏切换)会导致系统UI状态丢失。记得在
onResume里重新设置UI flag。 - Launcher应用——如果你的应用是TV Launcher,不要轻易隐藏导航栏。用户需要导航栏来切换应用、进入设置。
我曾经在一个项目里,把导航栏隐藏后,用户按遥控器「主页」键没反应。排查了半天,发现是SYSTEM_UI_FLAG_HIDE_NAVIGATION把系统按键事件也拦截了。解决方案是在隐藏导航栏的同时,保留SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,让布局延伸到导航栏区域,但导航栏本身仍然可以响应按键。
总结一下
系统UI适配,说白了就是跟系统抢屏幕空间。你要让用户感觉「这应用就是为这块屏幕量身定做的」。隐藏导航栏是基础,沉浸模式是进阶,系统栏控制是精细化。三者结合,才能做出真正的大屏体验。
嗯,这块内容就这些。代码示例可以直接拿去用,但记得根据你的设备做适配测试。不同厂商的TV,系统UI行为可能不一样——我遇到过一台电视,设置IMMERSIVE_STICKY后,导航栏根本不会自动消失,最后只能走厂商的私有API。
公众号:蓝海资料掘金营,微信deep3321