4. WindowManager 核心接口解析:addView()、updateViewLayout()、removeView() 的调用流程与参数详解
说实话,WindowManager 这个类,你平时可能用得不多。但只要是做 UI 开发,尤其是做悬浮窗、弹窗、或者系统级界面,你就绕不开它。它的核心就三个方法:addView()、updateViewLayout()、removeView()。今天我就带你把这仨兄弟彻底搞明白。
4.1 三个方法,各自管什么?
先给个直观印象:
| 方法 | 作用 | 调用时机 |
|---|---|---|
addView() |
把一个 View 添加到屏幕上 | 首次显示时 |
updateViewLayout() |
更新已有 View 的位置、大小、参数 | 需要调整时 |
removeView() |
从屏幕上移除 View | 不再需要时 |
嗯,看起来很简单对吧?但实际用起来,坑不少。我一个个说。
4.2 addView() —— 把 View 挂到屏幕上
先看签名:
public void addView(View view, ViewGroup.LayoutParams params)
参数就两个:你要显示的 View,以及它的布局参数。但这里有个关键点——params 必须是 WindowManager.LayoutParams 类型,不能是普通的 MarginLayoutParams 或者 FrameLayout.LayoutParams。
核心要点:addView() 内部会创建一个 ViewRootImpl,然后通过它把 View 和 WindowManagerService 连接起来。说白了,就是给 View 找了个"管家"。
我个人习惯在 addView 之前,先检查一下 View 是否已经被添加过。否则会抛异常:
if (view.getParent() == null) {
windowManager.addView(myView, params);
} else {
// 已经添加过了,别重复加
}
小技巧:如果你需要频繁显示/隐藏悬浮窗,不要每次都 addView/removeView。用 updateViewLayout 把 visibility 改成 GONE/VISIBLE,性能好很多。
4.3 updateViewLayout() —— 动态调整,别重建
签名:
public void updateViewLayout(View view, ViewGroup.LayoutParams params)
这个方法不会重新创建 ViewRootImpl,只是把新的 LayoutParams 传进去,然后触发 relayout。所以效率比 removeView 再加回来高得多。
我曾经在做一个视频悬浮窗时,用户拖拽窗口位置,每秒要更新几十次。一开始我用 removeView + addView,结果卡成 PPT。后来改成 updateViewLayout,流畅多了。
注意:updateViewLayout 时,传入的 params 必须和 addView 时是同一个类型,而且不能随意改 type 字段。比如你一开始用 TYPE_APPLICATION_OVERLAY,中途改成 TYPE_SYSTEM_ALERT,会直接崩溃。
4.4 removeView() —— 优雅地收尾
签名:
public void removeView(View view)
这个方法会触发 View 的 detach 流程,然后通知 WMS 移除对应的 WindowToken。如果你不调这个方法,View 会一直留在屏幕上,即使 Activity 销毁了也一样。
嗯,这里有个坑:removeView 必须在主线程调用。否则会抛 CalledFromWrongThreadException。为什么?因为 View 的绘制和事件分发都要求在主线程。
我曾经在后台线程收到网络回调后直接调 removeView,结果 crash 了。后来加了个 Handler.post 才搞定。
4.5 调用流程全景图
下面这张图,把三个方法的调用链路画清楚了。你看一眼就明白它们是怎么跟 WMS 打交道的:
你看,三个方法最终都汇聚到 WMS。区别在于:addView 是"新建",updateViewLayout 是"修改",removeView 是"销毁"。WMS 收到请求后,会更新内部的状态机,然后通知 SurfaceFlinger 去重新合成画面。
4.6 参数详解:WindowManager.LayoutParams
这个类里字段很多,但常用的就这几个:
| 字段 | 类型 | 说明 |
|---|---|---|
type |
int | 窗口类型,如 TYPE_APPLICATION、TYPE_APPLICATION_OVERLAY |
flags |
int | 窗口行为标志,如 FLAG_NOT_FOCUSABLE、FLAG_KEEP_SCREEN_ON |
format |
int | 像素格式,如 PixelFormat.TRANSLUCENT |
gravity |
int | 对齐方式,如 Gravity.TOP | Gravity.LEFT |
x, y |
int | 窗口左上角坐标 |
width, height |
int | 窗口宽高,可用 WRAP_CONTENT 或 MATCH_PARENT |
避坑指南:type 字段在 Android 10 以后限制很严。非系统应用只能用 TYPE_APPLICATION_OVERLAY,否则会被系统拦截。我曾经在适配 Android 11 时,就因为用了 TYPE_PHONE,结果悬浮窗死活显示不出来。
4.7 一个完整的例子
最后,给你一个完整的悬浮窗示例,把三个方法串起来:
// 1. 创建参数
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
);
params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 100;
params.y = 200;
// 2. 添加
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
View floatView = LayoutInflater.from(context).inflate(R.layout.float_view, null);
wm.addView(floatView, params);
// 3. 更新位置
params.x = 300;
params.y = 400;
wm.updateViewLayout(floatView, params);
// 4. 移除
wm.removeView(floatView);
嗯,代码不多,但每一步都有讲究。尤其是 type 和 flags 的组合,稍不注意就会出问题。我建议你每次加新窗口时,先在官方文档里查一下对应 Android 版本的限制。
好了,三个核心方法就讲到这里。记住一句话:addView 是生,removeView 是死,updateViewLayout 是活着的时候调整姿势。搞明白这个,WindowManager 你就入门了。