4. WindowManager.LayoutParams 详解:type、flags、token、gravity 等核心参数
说实话,WindowManager 的 API 看起来就那么几个方法——addView、updateViewLayout、removeView。但真正让 Android 窗口系统变得复杂的,是 LayoutParams 里那一堆参数。我见过不少开发者,调了半天的窗口显示不出来,或者显示出来但行为诡异,最后发现就是某个参数没设对。
今天我们就来拆解一下 LayoutParams 里最核心的几个参数。我个人习惯把它们分成四类:窗口类型、行为标志、身份令牌、布局规则。搞懂了这四类,你基本就能驾驭 WindowManager 了。
4.1 type:窗口的「户口本」
type 决定了你的窗口在系统里的「阶级」。Android 把窗口分成了三大类,每一类的权限和层级都不一样。
| 类型 | 取值范围 | 典型场景 | 需要权限 |
|---|---|---|---|
| 应用窗口 | FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW (1~99) | Activity 的主窗口 | 否 |
| 子窗口 | FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW (1000~1999) | PopupWindow、Dialog | 否 |
| 系统窗口 | FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW (2000~2999) | Toast、状态栏、输入法 | 是(SYSTEM_ALERT_WINDOW) |
这里有个坑,我必须要说。很多人以为 TYPE_TOAST 是系统窗口,其实它是个「半系统窗口」——不需要权限就能显示,但层级很低。我曾经在项目里想做一个全局悬浮球,图省事用了 TYPE_TOAST,结果发现它永远盖不住输入法键盘。后来老老实实换成 TYPE_APPLICATION_OVERLAY,才解决问题。
4.2 flags:窗口的「行为开关」
flags 是一个 int 型的位掩码。说白了,就是一堆开关,每个 bit 控制一个行为。我刚开始学的时候觉得这设计太古老了,后来发现位运算确实高效——一次方法调用就能设置十几个开关。
常用的 flags 我列一下:
- FLAG_NOT_FOCUSABLE (0x8):窗口不接收按键事件。触摸事件会穿透到下层窗口。我经常用它做悬浮窗。
- FLAG_NOT_TOUCHABLE (0x10):窗口完全不接收触摸事件。适合做纯展示的覆盖层。
- FLAG_KEEP_SCREEN_ON (0x80):保持屏幕常亮。比 PowerManager 的唤醒锁更轻量。
- FLAG_LAYOUT_IN_SCREEN (0x100):窗口布局时忽略状态栏。配合系统窗口使用。
- FLAG_DIM_BEHIND (0x2):在窗口后面画一层暗色遮罩。Dialog 的经典效果。
- FLAG_NOT_TOUCH_MODAL (0x20):窗口外的触摸事件会传递给下层窗口。嗯,这个和 FLAG_NOT_FOCUSABLE 经常一起用。
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN这样窗口不抢焦点,不拦截外部触摸,而且能铺满全屏。
4.3 token:窗口的「身份证」
token 可能是最容易被忽略的参数。但它其实非常重要——它是窗口和某个「容器」绑定的凭证。
举个例子:你通过 WindowManager 添加一个子窗口,必须传入一个有效的父窗口 token。否则系统会直接抛异常:BadTokenException: Unable to add window -- token null is not valid。这个错误我调试过不下十次。
token 从哪里来?
- Activity 的 token:通过
getWindow().getAttributes().token获取 - Dialog 的 token:通常来自 Activity 的 window token
- 系统窗口:不需要 token,但需要权限
我记得有一次,同事在 Service 里直接 new 了一个 Dialog,结果崩溃了。原因就是 Service 没有 window token。后来我建议他用 dialog.getWindow().setType(TYPE_APPLICATION_OVERLAY),配合系统窗口权限,才搞定。
4.4 gravity:窗口的「定位规则」
gravity 控制窗口在屏幕上的位置。它和 View 的 gravity 类似,但作用在窗口级别。
常用的值:
Gravity.TOP | Gravity.LEFT:左上角Gravity.CENTER:正中间Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL:底部居中
配合 x、y 偏移量,可以实现精确的定位。比如做一个右下角的悬浮球:
LayoutParams params = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT,
TYPE_APPLICATION_OVERLAY,
FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
params.x = 16; // 距离右边 16px
params.y = 16; // 距离底部 16px
这里要注意:x 和 y 的参考系是由 gravity 决定的。gravity 设了 BOTTOM|RIGHT,那 x 就是距离右边的距离,y 就是距离底部的距离。这个逻辑我一开始也搞反过。
4.5 其他重要参数
除了上面四个,还有几个参数值得提一下:
- width / height:可以是 MATCH_PARENT、WRAP_CONTENT,或者具体像素值。系统窗口建议用 MATCH_PARENT 或具体值,因为 WRAP_CONTENT 在某些设备上表现不一致。
- format:像素格式。常用 PixelFormat.TRANSLUCENT(半透明)或 PixelFormat.TRANSPARENT(透明)。默认是 OPAQUE,如果你窗口有透明区域,记得改掉。
- alpha / dimAmount:窗口透明度(0~1)和背景遮罩透明度(0~1)。配合 FLAG_DIM_BEHIND 使用。
- softInputMode:控制输入法弹出时的窗口调整策略。比如
SOFT_INPUT_ADJUST_RESIZE或SOFT_INPUT_ADJUST_PAN。
4.6 核心参数关系图
下面这张图是我自己整理的 LayoutParams 核心参数关系。你可以看到 type 决定层级,flags 控制行为,token 绑定身份,gravity 和 x/y 决定位置。它们共同决定了窗口的最终表现。
4.7 避坑指南
最后,分享几个我踩过的坑:
子窗口(TYPE_APPLICATION_PANEL 等)必须绑定父窗口的 token。如果你在 Service 或 BroadcastReceiver 里添加子窗口,一定会崩溃。解决方案:要么用系统窗口类型,要么先获取一个有效的 token。
很多人直接写
params.flags = FLAG_NOT_FOCUSABLE,这会覆盖掉系统默认的 flags。正确做法是用位或运算:params.flags |= FLAG_NOT_FOCUSABLE。我刚开始也犯过这个错,结果窗口的默认行为全丢了。
如果你设了 gravity = CENTER,那 x 和 y 的偏移量是相对于中心点的。想精确控制位置?建议用 LEFT|TOP 作为 gravity,然后手动算 x 和 y。
嗯,以上就是 LayoutParams 核心参数的解析。说实话,这些参数单独看都不难,但组合起来就很容易出问题。我的建议是:写代码前先想清楚你的窗口「是什么类型」「要什么行为」「绑什么 token」「放什么位置」。想清楚了再动手,能省不少调试时间。