24、系统UI适配:隐藏导航栏、沉浸模式与系统栏控制

做TV端开发,有个绕不开的坎——系统UI。说白了,就是那个导航栏、状态栏,还有各种系统级的浮层。我刚开始做大屏适配时,以为只要把布局撑满屏幕就完事了。结果呢?用户一按遥控器,导航栏弹出来,把界面底部内容挡得严严实实。嗯,那场面,挺尴尬的。

今天咱们就把这块彻底讲透。系统UI适配,核心就三件事:隐藏、沉浸、控制

为什么系统UI会成为问题?

Android TV设备五花八门。有的电视自带虚拟导航栏,有的用物理按键,还有的用红外遥控。系统UI的显示策略,每个厂商改得都不一样。

我遇到过一台国产电视,系统导航栏高度足足有80dp。你想想看,一个1920x1080的屏幕,底部80dp被导航栏吃掉,留给应用的实际区域就缩水了。更坑的是,有些设备在播放视频时,导航栏会自动隐藏,但一按遥控器又弹出来——这体验,用户不骂娘才怪。

核心知识点一览

先看一张图,把整个知识体系串起来。我习惯先搭框架再填细节,这样不容易漏。

系统UI适配知识体系 隐藏导航栏 SYSTEM_UI_FLAG_HIDE_NAVIGATION 临时隐藏 vs 永久隐藏 手势冲突处理 沉浸模式 SYSTEM_UI_FLAG_IMMERSIVE IMMERSIVE_STICKY 从边缘滑出恢复 系统栏控制 setSystemUiVisibility WindowInsetsController Android 11+ 新API 实践要点 • 区分TV设备类型:虚拟导航栏 vs 物理按键 • 视频播放场景必须用沉浸模式 • 低版本兼容:API 19以下需特殊处理

隐藏导航栏:最基础的操作

隐藏导航栏,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)引入了IMMERSIVEIMMERSIVE_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适配不是「藏起来就完事」。你要考虑用户如何与系统交互,如何优雅地退出全屏,以及在不同Android版本上保持一致体验。

避坑指南

做系统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