7、WindowManager 的 Flags 标志位:FLAG_NOT_FOCUSABLE、FLAG_NOT_TOUCHABLE、FLAG_KEEP_SCREEN_ON、FLAG_FULLSCREEN 等常用标志

Flags 标志位,说白了就是 WindowManager 的「开关面板」。你往窗口上贴一个 flag,系统就按这个 flag 的行为去处理窗口。我刚开始接触这部分时,觉得不就是几个常量嘛,背下来就行了。后来踩了几个坑才明白——每个 flag 背后都对应一套完整的窗口策略,用错了轻则界面异常,重则 ANR。

今天咱们挑几个最常用的 flag 来拆解。它们分别是:FLAG_NOT_FOCUSABLEFLAG_NOT_TOUCHABLEFLAG_KEEP_SCREEN_ONFLAG_FULLSCREEN。嗯,一个一个来。

7.1 FLAG_NOT_FOCUSABLE:我不要焦点

这个 flag 的意思是:当前窗口不接收按键事件,也不获取输入焦点。加了它之后,窗口虽然显示在屏幕上,但用户按返回键、菜单键,它都不响应。焦点会穿透到它下面的窗口。

我举个例子你就明白了。比如你做一个悬浮球——类似手机管家的那种小圆点。它飘在所有界面之上,但你点击它时,不希望它吃掉按键事件,而是让背后的 Activity 继续正常工作。这时候就该用 FLAG_NOT_FOCUSABLE

核心行为:加了此 flag 的窗口,WindowManager 在分发按键事件时会直接跳过它。焦点窗口仍然是它下面的那个 Activity 或 Dialog。

代码里怎么加?很简单:

WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

不过要注意一点——加了 NOT_FOCUSABLE 之后,窗口默认也收不到触摸事件了吗? 不是的。触摸事件和焦点事件是两套机制。触摸事件走的是触摸命中测试,只要你的窗口区域被用户点到了,系统还是会分发 TouchEvent 给你。除非你同时加了 FLAG_NOT_TOUCHABLE

我的经验:做悬浮窗时,我习惯把 FLAG_NOT_FOCUSABLEFLAG_NOT_TOUCH_MODAL 一起用。后者可以让窗口外的触摸事件正常传递到下层窗口,避免「触摸穿透」问题。

7.2 FLAG_NOT_TOUCHABLE:触摸与我无关

这个 flag 更狠——窗口完全不接收任何触摸事件。用户点上去,就像点在了空气上。事件直接穿透到下面的窗口。

你可能会问:那这个 flag 有什么用?

我遇到过这样一个场景:做一个全局的「遮罩层」,用来阻止用户操作。比如下载大文件时,我不想让用户乱点屏幕。但遮罩层本身不需要处理触摸,它只是「挡住」而已。这时候用 FLAG_NOT_TOUCHABLE 反而错了——因为它会让触摸穿透,用户照样能点到下面的按钮。

正确的做法是:不加 NOT_TOUCHABLE,而是加 FLAG_NOT_FOCUSABLE,然后在遮罩层上消费掉触摸事件。嗯,这里容易搞混,我专门记过一笔。

FLAG_NOT_TOUCHABLE 到底用在哪?

  • 纯信息展示窗口:比如 Toast 的替代方案,只显示文字,不交互。
  • 后台状态指示:比如录音时的红点,用户不需要点它。
  • 调试用覆盖层:显示 FPS、内存信息,但不影响用户操作。
params.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;

注意:加了此 flag 后,窗口的 onTouchEvent() 永远不会被调用。如果你在窗口里还注册了 OnTouchListener,那也是白搭。系统在触摸命中阶段就直接跳过了。

7.3 FLAG_KEEP_SCREEN_ON:屏幕别灭

这个 flag 我太熟了。做视频播放器时,用户看视频看到一半,屏幕突然灭了——那体验简直灾难。FLAG_KEEP_SCREEN_ON 就是用来解决这个问题的。

它的原理很简单:只要当前窗口可见,系统就不会进入休眠状态,也不会调低屏幕亮度。注意,它不影响用户手动按电源键关屏,只阻止系统的自动休眠。

用法:

params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;

或者更推荐的方式——在 Activity 的布局根节点上加 android:keepScreenOn="true"。但如果你用的是 WindowManager 直接添加的窗口,那就只能通过 flag 来设了。

我踩过的坑:曾经在一个直播项目里,我用 FLAG_KEEP_SCREEN_ON 保持屏幕常亮。结果用户切到后台再切回来,flag 失效了。后来发现是因为窗口被重新添加了,flag 没有持久化。解决方案是在 onResume() 里重新设置一次 flag。

另外,这个 flag 和 PowerManager.WakeLock 有什么区别?简单说:WakeLock 更底层,能阻止 CPU 休眠;而 KEEP_SCREEN_ON 只控制屏幕,不阻止 CPU 休眠。如果你只是想让屏幕不灭,用 flag 就够了,别滥用 WakeLock,那玩意耗电。

7.4 FLAG_FULLSCREEN:全屏显示

这个 flag 的作用是隐藏状态栏和导航栏,让窗口占据整个屏幕。注意,它和 Activity 的 android:theme 里的全屏效果是一样的,但 WindowManager 级别的全屏更底层。

代码:

params.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;

加了之后,状态栏会消失。但有个细节——如果用户从屏幕顶部下滑,状态栏会重新出现。这是因为系统有「沉浸模式」的交互逻辑。如果你希望状态栏彻底不出现,需要配合 SYSTEM_UI_FLAG_IMMERSIVE_STICKY 使用。

不过,FLAG_FULLSCREEN 在 Android 11 之后行为有变化。系统对全屏窗口的限制更严格了,尤其是非 Activity 的窗口(比如 Service 添加的窗口),可能无法真正全屏。嗯,这个后面讲版本适配时再细说。

7.5 常用 Flags 组合速查表

我把这几个 flag 的核心属性整理了一下,方便你对照:

Flag 作用 影响焦点 影响触摸 影响系统UI
FLAG_NOT_FOCUSABLE 不获取按键焦点
FLAG_NOT_TOUCHABLE 不接收触摸事件
FLAG_KEEP_SCREEN_ON 阻止屏幕休眠
FLAG_FULLSCREEN 隐藏状态栏/导航栏

7.6 标志位之间的相互作用

这几个 flag 不是孤立的。它们组合起来会产生不同的效果。我画了一张图,帮你理清它们的关系:

WindowManager Flags 相互作用图 FLAG_NOT_FOCUSABLE 按键事件穿透 FLAG_NOT_TOUCHABLE 触摸事件穿透 FLAG_KEEP_SCREEN_ON 阻止系统休眠 FLAG_FULLSCREEN 隐藏系统UI 常组合使用 无直接关系 全屏时通常需要 视频场景常组合 实线箭头:常见组合关系 | 虚线箭头:无直接依赖但可搭配使用

从图上你能看到:FLAG_NOT_FOCUSABLEFLAG_NOT_TOUCHABLE 经常一起出现,比如做悬浮窗时。而 FLAG_FULLSCREENFLAG_KEEP_SCREEN_ON 在视频场景下是黄金搭档。

7.7 避坑指南

最后,分享几个我实际项目中踩过的坑:

  • FLAG_NOT_FOCUSABLE 导致输入法弹不出:如果你在 EditText 所在的窗口加了 NOT_FOCUSABLE,输入法永远不会弹出。因为窗口没有焦点,系统不会把输入法绑定给它。解决方案是去掉 NOT_FOCUSABLE,或者用 FLAG_ALT_FOCUSABLE_IM 做特殊处理。
  • FLAG_KEEP_SCREEN_ON 在锁屏后失效:用户按电源键锁屏,flag 就管不住了。这是设计如此——系统优先尊重用户的锁屏操作。如果你需要在锁屏后仍然保持屏幕亮着,得用 WakeLock 配合 FLAG_SHOW_WHEN_LOCKED
  • FLAG_FULLSCREEN 在 Android 10+ 的兼容问题:从 Android 10 开始,系统对全屏窗口增加了「手势导航」的适配。如果你直接设 FULLSCREEN,导航栏可能会以半透明形式出现。需要同时设置 LayoutParams.layoutInDisplayCutoutModeLAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

我的习惯:每次设置 flag 时,我都会在代码旁边加一行注释,说明「为什么加这个 flag」。比如 // 悬浮球不需要焦点,避免干扰底层 Activity。半年后回来看代码,你会感谢自己的。

好了,这几个常用 flag 就聊到这儿。它们单个看都不复杂,但组合起来能玩出很多花样。你想想看——一个窗口可以既不要焦点、又不要触摸、还要保持屏幕常亮,同时全屏显示。这种组合在 Kiosk 模式(自助终端)里非常常见。嗯,后面讲窗口类型时,我们还会再遇到它们。