18、窗口焦点管理:WMS 的焦点窗口分配策略、focusChanged() 回调、触摸事件的分发与焦点关系

各位同学,今天我们来聊聊窗口焦点管理。这个话题,说白了就是「当前哪个窗口说了算」。你想想看,手机屏幕上那么多窗口,用户按一下返回键,到底该通知谁?触摸事件又该发给谁?这些问题的答案,都指向同一个核心——焦点窗口。

我个人习惯把焦点管理比作「舞台追光灯」。灯光打在谁身上,谁就是主角。在 Android 系统中,这个「主角」就是当前接收键盘事件和某些触摸事件的窗口。嗯,这里要注意,触摸事件的分发其实比键盘事件复杂得多,我们后面会细说。

18.1 焦点窗口的分配策略

WMS 维护了一个全局变量 mCurrentFocus,指向当前拥有焦点的窗口。这个变量在 updateFocusedWindowLocked() 方法中更新。我刚开始看这部分代码时,觉得逻辑挺绕的,后来画了个流程图才理清楚。

核心原则: 焦点窗口必须是可聚焦的(focusable)、可见的(visible),并且位于当前输入法窗口之上(如果输入法窗口需要焦点)。

分配策略大致如下:

  • 优先给输入法窗口:如果输入法窗口(IME)正在显示且需要焦点,WMS 会优先把焦点给它。因为用户正在打字,键盘事件必须准确送达。
  • 其次给最顶层的应用窗口:如果没有输入法窗口,或者输入法不需要焦点,WMS 会找 Z-order 最高的可聚焦窗口。
  • 特殊窗口类型:系统窗口(如状态栏、导航栏)通常不参与焦点竞争,除非它们明确设置了 FLAG_NOT_FOCUSABLE 为 false。

我在项目中遇到过一个问题:某个悬浮窗抢了焦点,导致底层 Activity 收不到返回键事件。排查后发现,那个悬浮窗没有设置 FLAG_NOT_FOCUSABLE,WMS 自然就把焦点给它了。解决方案很简单,加上这个 flag 就行。

18.2 focusChanged() 回调

当焦点窗口发生变化时,WMS 会调用 focusChanged() 方法。这个方法会通知所有注册了 WindowManagerInternal.FocusObserver 的监听者。嗯,这里要注意,这个回调是系统内部使用的,普通应用开发者接触不到。

回调的流程是这样的:

  1. WMS 计算出新的焦点窗口 newFocus
  2. 如果 newFocus != mCurrentFocus,则调用 focusChanged()
  3. 通知所有 FocusObserver,参数包括旧焦点窗口和新焦点窗口的 token。
  4. 更新 mCurrentFocusnewFocus

我曾经在调试一个输入法闪烁问题时,发现 focusChanged() 被频繁调用。原因是某个窗口在 onResume() 中不断请求焦点,导致 WMS 反复切换焦点。这种问题很难复现,但一旦出现,用户体验极差。我的建议是:应用层不要主动调用 requestFocus(),除非你真的需要。

18.3 触摸事件的分发与焦点关系

这里有个容易混淆的点:触摸事件的分发,并不完全依赖焦点窗口。你想想看,用户点击了一个悬浮窗,这个悬浮窗可能没有焦点,但它依然能收到触摸事件。为什么?

因为触摸事件的分发是基于「命中测试」(hit test)的。WMS 会遍历窗口列表,找到第一个包含触摸点坐标的窗口,然后把事件发给它。这个过程和焦点无关。

但键盘事件就不一样了。键盘事件(比如物理键盘输入、返回键、菜单键)必须发给焦点窗口。所以,焦点窗口主要影响的是键盘事件和某些系统按键事件。

避坑指南: 如果你的应用需要处理返回键,请确保你的窗口在需要时能获得焦点。我曾经见过一个 bug:某个 Dialog 弹出后,返回键无法关闭它,就是因为 Dialog 没有正确请求焦点。

为了更直观地理解焦点管理流程,我画了一张图:

窗口焦点管理流程 输入事件到达 判断事件类型 触摸事件 键盘/按键事件 命中测试 → 找到目标窗口 查找焦点窗口 mCurrentFocus 分发到命中窗口 分发到焦点窗口 触摸事件走命中测试,键盘事件走焦点窗口

从图中可以看得很清楚:触摸事件和键盘事件走的是两条不同的路径。触摸事件靠「命中测试」找到目标,键盘事件则依赖「焦点窗口」。这也是为什么有时候一个窗口没有焦点,却依然能响应触摸事件的原因。

注意事项: 虽然触摸事件不依赖焦点,但某些系统手势(如三指截图、边缘手势)可能会拦截触摸事件。这些手势通常由 SystemUI 处理,与普通窗口的焦点无关。

18.4 焦点切换的时机

焦点切换发生在以下场景:

  • 窗口添加或移除:新窗口出现,或者旧窗口消失,WMS 会重新计算焦点。
  • 窗口可见性变化:比如一个窗口从不可见变为可见,或者反过来。
  • 窗口 Z-order 变化:窗口层级调整后,最顶层的可聚焦窗口可能改变。
  • 输入法窗口状态变化:输入法显示或隐藏时,焦点可能被抢走或归还。

我记得有一次调试一个多窗口模式下的焦点问题。用户在分屏模式下操作,点击左边窗口,右边窗口却收到了按键事件。排查后发现,分屏模式下 WMS 的焦点分配逻辑有个特殊处理:它会优先给最近交互过的窗口。这个逻辑在 FocusedStackFrame 中实现,有兴趣的同学可以翻翻源码。

18.5 总结

焦点管理是 WMS 中一个看似简单、实则暗藏玄机的模块。核心要点就三个:

  1. 焦点窗口由 updateFocusedWindowLocked() 决定,遵循「可聚焦、可见、最顶层」原则。
  2. focusChanged() 回调通知系统内部监听者,应用层一般不需要关心。
  3. 触摸事件走命中测试,键盘事件走焦点窗口——这两条路径是独立的。

最后送大家一句话:别让你的窗口乱抢焦点。该聚焦时聚焦,不该聚焦时让开,这才是优雅的窗口行为。


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