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,才解决问题。

⚠️ 注意:从 Android 8.0 (API 26) 开始,TYPE_PHONE、TYPE_SYSTEM_ALERT 等旧系统窗口类型已被废弃。请使用 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 经常一起用。
💡 我的习惯:写悬浮窗时,我通常这样组合 flags:
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_RESIZESOFT_INPUT_ADJUST_PAN

4.6 核心参数关系图

下面这张图是我自己整理的 LayoutParams 核心参数关系。你可以看到 type 决定层级,flags 控制行为,token 绑定身份,gravity 和 x/y 决定位置。它们共同决定了窗口的最终表现。

WindowManager.LayoutParams type 窗口类型/层级 flags 行为开关 token 身份凭证 gravity 定位规则 四个核心参数共同决定窗口的层级、行为、身份和位置

4.7 避坑指南

最后,分享几个我踩过的坑:

🔴 坑1:type 和 token 不匹配
子窗口(TYPE_APPLICATION_PANEL 等)必须绑定父窗口的 token。如果你在 Service 或 BroadcastReceiver 里添加子窗口,一定会崩溃。解决方案:要么用系统窗口类型,要么先获取一个有效的 token。
🔴 坑2:flags 的叠加问题
很多人直接写 params.flags = FLAG_NOT_FOCUSABLE,这会覆盖掉系统默认的 flags。正确做法是用位或运算:params.flags |= FLAG_NOT_FOCUSABLE。我刚开始也犯过这个错,结果窗口的默认行为全丢了。
🔴 坑3:gravity 和 x/y 的配合
如果你设了 gravity = CENTER,那 x 和 y 的偏移量是相对于中心点的。想精确控制位置?建议用 LEFT|TOP 作为 gravity,然后手动算 x 和 y。

嗯,以上就是 LayoutParams 核心参数的解析。说实话,这些参数单独看都不难,但组合起来就很容易出问题。我的建议是:写代码前先想清楚你的窗口「是什么类型」「要什么行为」「绑什么 token」「放什么位置」。想清楚了再动手,能省不少调试时间。