18、窗口焦点管理:WMS 的焦点窗口分配策略、focusChanged() 回调、触摸事件的分发与焦点关系
各位同学,今天我们来聊聊窗口焦点管理。这个话题,说白了就是「当前哪个窗口说了算」。你想想看,手机屏幕上那么多窗口,用户按一下返回键,到底该通知谁?触摸事件又该发给谁?这些问题的答案,都指向同一个核心——焦点窗口。
我个人习惯把焦点管理比作「舞台追光灯」。灯光打在谁身上,谁就是主角。在 Android 系统中,这个「主角」就是当前接收键盘事件和某些触摸事件的窗口。嗯,这里要注意,触摸事件的分发其实比键盘事件复杂得多,我们后面会细说。
18.1 焦点窗口的分配策略
WMS 维护了一个全局变量 mCurrentFocus,指向当前拥有焦点的窗口。这个变量在 updateFocusedWindowLocked() 方法中更新。我刚开始看这部分代码时,觉得逻辑挺绕的,后来画了个流程图才理清楚。
分配策略大致如下:
- 优先给输入法窗口:如果输入法窗口(IME)正在显示且需要焦点,WMS 会优先把焦点给它。因为用户正在打字,键盘事件必须准确送达。
- 其次给最顶层的应用窗口:如果没有输入法窗口,或者输入法不需要焦点,WMS 会找 Z-order 最高的可聚焦窗口。
- 特殊窗口类型:系统窗口(如状态栏、导航栏)通常不参与焦点竞争,除非它们明确设置了
FLAG_NOT_FOCUSABLE为 false。
我在项目中遇到过一个问题:某个悬浮窗抢了焦点,导致底层 Activity 收不到返回键事件。排查后发现,那个悬浮窗没有设置 FLAG_NOT_FOCUSABLE,WMS 自然就把焦点给它了。解决方案很简单,加上这个 flag 就行。
18.2 focusChanged() 回调
当焦点窗口发生变化时,WMS 会调用 focusChanged() 方法。这个方法会通知所有注册了 WindowManagerInternal.FocusObserver 的监听者。嗯,这里要注意,这个回调是系统内部使用的,普通应用开发者接触不到。
回调的流程是这样的:
- WMS 计算出新的焦点窗口
newFocus。 - 如果
newFocus != mCurrentFocus,则调用focusChanged()。 - 通知所有
FocusObserver,参数包括旧焦点窗口和新焦点窗口的 token。 - 更新
mCurrentFocus为newFocus。
我曾经在调试一个输入法闪烁问题时,发现 focusChanged() 被频繁调用。原因是某个窗口在 onResume() 中不断请求焦点,导致 WMS 反复切换焦点。这种问题很难复现,但一旦出现,用户体验极差。我的建议是:应用层不要主动调用 requestFocus(),除非你真的需要。
18.3 触摸事件的分发与焦点关系
这里有个容易混淆的点:触摸事件的分发,并不完全依赖焦点窗口。你想想看,用户点击了一个悬浮窗,这个悬浮窗可能没有焦点,但它依然能收到触摸事件。为什么?
因为触摸事件的分发是基于「命中测试」(hit test)的。WMS 会遍历窗口列表,找到第一个包含触摸点坐标的窗口,然后把事件发给它。这个过程和焦点无关。
但键盘事件就不一样了。键盘事件(比如物理键盘输入、返回键、菜单键)必须发给焦点窗口。所以,焦点窗口主要影响的是键盘事件和某些系统按键事件。
为了更直观地理解焦点管理流程,我画了一张图:
从图中可以看得很清楚:触摸事件和键盘事件走的是两条不同的路径。触摸事件靠「命中测试」找到目标,键盘事件则依赖「焦点窗口」。这也是为什么有时候一个窗口没有焦点,却依然能响应触摸事件的原因。
18.4 焦点切换的时机
焦点切换发生在以下场景:
- 窗口添加或移除:新窗口出现,或者旧窗口消失,WMS 会重新计算焦点。
- 窗口可见性变化:比如一个窗口从不可见变为可见,或者反过来。
- 窗口 Z-order 变化:窗口层级调整后,最顶层的可聚焦窗口可能改变。
- 输入法窗口状态变化:输入法显示或隐藏时,焦点可能被抢走或归还。
我记得有一次调试一个多窗口模式下的焦点问题。用户在分屏模式下操作,点击左边窗口,右边窗口却收到了按键事件。排查后发现,分屏模式下 WMS 的焦点分配逻辑有个特殊处理:它会优先给最近交互过的窗口。这个逻辑在 FocusedStackFrame 中实现,有兴趣的同学可以翻翻源码。
18.5 总结
焦点管理是 WMS 中一个看似简单、实则暗藏玄机的模块。核心要点就三个:
- 焦点窗口由
updateFocusedWindowLocked()决定,遵循「可聚焦、可见、最顶层」原则。 focusChanged()回调通知系统内部监听者,应用层一般不需要关心。- 触摸事件走命中测试,键盘事件走焦点窗口——这两条路径是独立的。
最后送大家一句话:别让你的窗口乱抢焦点。该聚焦时聚焦,不该聚焦时让开,这才是优雅的窗口行为。