5. WindowManager.LayoutParams 详解:type、flags、token、gravity、x/y、width/height、format 等核心属性

说实话,WindowManager.LayoutParams 这个类,我每次看源码都觉得它像个「百宝箱」。它定义了窗口的几乎所有行为特征。你想想看,一个窗口在屏幕上怎么显示、能不能被触摸、要不要显示在状态栏下面……这些全由它说了算。

今天我们就把它拆开揉碎了讲。我会结合我这些年踩过的坑,帮你把每个核心属性都吃透。

5.1 type:窗口的「身份牌」

type 决定了窗口的层级和用途。说白了,就是告诉系统:「我是谁,我该待在哪一层」。

Android 把窗口分成了三大类,我习惯用一张表来记:

类型 取值范围 典型用途 层级特点
应用窗口 FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW (1~99) Activity 的主窗口 最底层,受 Activity 生命周期约束
子窗口 FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW (1000~1999) PopupWindow、Dialog 等 依附于父窗口,随父窗口移动
系统窗口 FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW (2000~2999) Toast、状态栏、输入法 最高层,需要系统权限

这里有个坑,我必须要说。我曾经在项目里直接用 TYPE_SYSTEM_OVERLAY 做悬浮窗,结果在 Android 10 以上的设备上直接崩溃。为什么?因为系统对 overlay 权限收紧了。现在你必须用 TYPE_APPLICATION_OVERLAY,并且申请 SYSTEM_ALERT_WINDOW 权限。

注意:从 Android 10(API 29)开始,TYPE_PHONETYPE_SYSTEM_ALERT 等旧类型已被废弃。请统一使用 TYPE_APPLICATION_OVERLAY

5.2 flags:窗口的「行为开关」

flags 是一个 int 型的位掩码。每个 bit 控制一个行为。我刚开始学的时候觉得这玩意儿很绕,后来发现其实就两个思路:要么控制窗口「怎么显示」,要么控制窗口「怎么交互」。

常用的 flags 我列几个:

  • FLAG_NOT_FOCUSABLE:窗口不获取焦点。触摸事件会穿透到下层窗口。做悬浮窗必备。
  • FLAG_NOT_TOUCHABLE:窗口不响应触摸。相当于一个「透明玻璃」。
  • FLAG_KEEP_SCREEN_ON:保持屏幕常亮。做视频播放器时常用。
  • FLAG_FULLSCREEN:隐藏状态栏。注意,这个和系统 UI 的隐藏逻辑不同。
  • FLAG_LAYOUT_IN_SCREEN:窗口布局时忽略状态栏区域。配合 FLAG_FULLSCREEN 使用效果更佳。

嗯,这里有个小技巧。如果你想让一个窗口既不可触摸,又不可聚焦,可以这样写:

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

用「或」运算组合多个 flag,这是 Android 的经典做法。

5.3 token:窗口的「通行证」

token 是个容易让人困惑的属性。我一开始以为它就是个标识符,后来才发现它其实是窗口和某个「宿主」绑定的凭证。

简单说,token 的作用就是:告诉 WindowManager,这个窗口属于谁

比如,你通过 Dialog 添加一个窗口,系统会自动把 Dialog 的 token 传进去。如果你自己手动添加窗口,就必须从 Activity 或 View 中获取 token:

// 从 Activity 获取 token
IBinder token = activity.getWindow().getAttributes().token;

// 或者从 View 获取
IBinder token = view.getWindowToken();
经验之谈:我遇到过不少同学在 Service 里直接 new LayoutParams 然后 addView,结果崩溃在 token 为 null 上。记住:系统窗口(type >= 2000)不需要 token,但应用窗口和子窗口必须有 token。

5.4 gravity:窗口的「定位锚点」

gravity 决定了窗口在屏幕上的位置。它和 View 的 gravity 类似,但作用范围是整个屏幕。

常用的值:

  • Gravity.TOP | Gravity.LEFT:左上角
  • Gravity.CENTER:正中间
  • Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL:底部居中

配合 x/y 使用,可以实现精确的偏移定位。比如:

params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 100;  // 距离屏幕左边 100px
params.y = 200;  // 距离屏幕顶部 200px

这里有个细节:x 和 y 的偏移方向是相对于 gravity 指定的锚点。如果 gravity 是 Gravity.CENTER,那么 x 和 y 就是从屏幕中心开始偏移。

5.5 x/y 与 width/height:尺寸与位置

这两个属性组决定了窗口的「物理存在」。

x/y:窗口左上角相对于 gravity 锚点的偏移量。单位是像素。

width/height:窗口的宽高。可以指定具体像素值,也可以用 WRAP_CONTENTMATCH_PARENT

我习惯这样理解:gravity 定「锚点」,x/y 定「偏移」,width/height 定「大小」。三者配合,就能把窗口放在屏幕的任何位置。

LayoutParams params = new LayoutParams();
params.gravity = Gravity.CENTER;
params.x = 0;
params.y = 0;
params.width = 500;
params.height = 300;
// 这样窗口就会出现在屏幕正中央,宽 500px,高 300px

5.6 format:窗口的「像素格式」

format 指定了窗口的像素格式。说白了,就是告诉系统:「我的窗口要用什么颜色深度来渲染」。

常用的值:

  • PixelFormat.TRANSPARENT:透明。适合悬浮窗、弹幕等。
  • PixelFormat.TRANSLUCENT:半透明。性能比 TRANSPARENT 好一点。
  • PixelFormat.OPAQUE:不透明。性能最好,但窗口不能有透明区域。
  • PixelFormat.RGBA_8888:32 位色深,支持透明度。最常用。
性能建议:如果你的窗口完全不透明,请使用 PixelFormat.OPAQUE。这能让 SurfaceFlinger 跳过混合步骤,提升渲染性能。我在做视频播放器时,把播放窗口从 RGBA_8888 改成 OPAQUE,帧率提升了 5% 左右。

5.7 其他重要属性

除了上面这些,还有几个属性值得提一下:

  • softInputMode:控制软键盘弹出时窗口的调整行为。比如 SOFT_INPUT_ADJUST_RESIZESOFT_INPUT_ADJUST_PAN
  • screenOrientation:强制窗口的屏幕方向。一般用在 Activity 上,但也可以对系统窗口设置。
  • alpha:窗口的整体透明度。0.0 完全透明,1.0 完全不透明。
  • dimAmount:窗口后面的变暗程度。Dialog 常用这个效果。

5.8 知识体系总览

下面这张图是我自己整理的 LayoutParams 核心属性关系图。你看一眼就能明白它们各自负责什么:

WindowManager.LayoutParams 核心属性体系 LayoutParams type 窗口身份 & 层级 flags 行为开关(位掩码) token 宿主绑定凭证 gravity 定位锚点 x / y 偏移量(像素) width / height 尺寸(WRAP/MATCH/px) format(像素格式) 每个属性控制窗口的一个维度,组合使用才能构建出符合预期的窗口

5.9 实战中的组合用法

光讲理论没意思。我分享一个我实际做过的悬浮球功能,看看这些属性怎么配合:

LayoutParams params = new LayoutParams();
params.type = LayoutParams.TYPE_APPLICATION_OVERLAY;  // 系统悬浮窗
params.flags = LayoutParams.FLAG_NOT_FOCUSABLE        // 不抢焦点
             | LayoutParams.FLAG_NOT_TOUCH_MODAL      // 事件不阻塞
             | LayoutParams.FLAG_LAYOUT_IN_SCREEN;    // 全屏布局
params.gravity = Gravity.TOP | Gravity.LEFT;          // 左上角锚点
params.x = 100;                                       // 初始位置
params.y = 200;
params.width = 80;                                    // 悬浮球大小
params.height = 80;
params.format = PixelFormat.TRANSLUCENT;              // 半透明
params.token = service.getWindowToken();              // 从 Service 获取 token

你看,每个属性都有明确的职责。type 决定能不能显示,flags 决定交互行为,gravity 和 x/y 决定位置,width/height 决定大小,format 决定渲染效果。缺一个都不行。

避坑指南:我曾经在 Android 11 上调试悬浮窗,发现 TYPE_APPLICATION_OVERLAY 的窗口默认会显示在状态栏下面。如果你想让窗口覆盖状态栏,必须加上 FLAG_LAYOUT_IN_SCREENFLAG_LAYOUT_NO_LIMITS。这个坑我花了半天才找到原因。

好了,LayoutParams 的核心属性就讲到这里。每个属性都不复杂,但组合起来能玩出很多花样。你只要记住:type 定身份,flags 定行为,gravity 定锚点,x/y 定偏移,width/height 定大小,format 定画质。下次写窗口代码时,按这个思路来,基本不会出错。

公众号:蓝海资料掘金营,微信deep3321