☰
11、事件分发目标查找:findFocusedWindowTargets()与findTouchedWindowTargets()
2026/10/8 7:25:00 网站建设 项目流程

11.1 为什么需要两个查找方法?

你想想看,键盘事件和触摸事件的性质完全不同。

  • 键盘事件:用户按下一个键,系统得知道当前哪个窗口获得了焦点。焦点窗口只有一个,所以查找逻辑相对简单。
  • 触摸事件:用户手指点下去,系统得知道手指落在哪个窗口上。可能有多个窗口重叠,还得考虑窗口的可见区域、是否可点击等等。

所以Android把这两种查找逻辑分开了。一个管焦点,一个管触摸。嗯,这里要注意,触摸事件的分发远比键盘事件复杂,我在项目中遇到过好几次因为触摸目标找错导致的诡异Bug,后面会细说。

11.2 findFocusedWindowTargets():焦点窗口的查找

这个方法的核心任务:找到当前拥有输入焦点的窗口,然后把事件发过去。

我们先看它的简化流程:

// 伪代码,展示核心逻辑 void findFocusedWindowTargets() { // 1. 获取当前焦点窗口 sp<InputWindowHandle> focusedWindow = findFocusedWindow(); // 2. 检查焦点窗口是否有效 if (focusedWindow == nullptr) { // 没有焦点窗口,丢弃事件 return; } // 3. 检查窗口是否已死亡 if (focusedWindow->getInputChannel() == nullptr) { // 窗口已销毁,丢弃事件 return; } // 4. 检查窗口是否可接收事件 if (!focusedWindow->canReceiveInput()) { // 窗口不可接收输入,丢弃事件 return; } // 5. 添加到目标列表 mTargets.add(focusedWindow); }

看起来很简单对吧?但实际源码里要考虑的细节远不止这些。我记得有一次调试一个第三方输入法的问题,发现按键事件总是丢失,最后定位到就是findFocusedWindow()返回了nullptr——因为焦点窗口在事件到达前刚好被销毁了。

关键点:焦点窗口的查找依赖于WindowManagerService维护的焦点栈。每次窗口焦点变化时,WMS都会通过InputManagerService通知InputDispatcher更新焦点信息。

11.3 findTouchedWindowTargets():触摸窗口的查找

这个方法就复杂多了。触摸事件的目标查找,说白了就是根据触摸点的坐标,找到最合适的那个窗口。

我们来看核心流程:

// 伪代码,展示核心逻辑 void findTouchedWindowTargets() { // 1. 获取所有可见窗口 Vector<sp<InputWindowHandle>> windows = getVisibleWindows(); // 2. 按Z-order排序(从上到下) sortWindowsByZOrder(windows); // 3. 遍历窗口,找到第一个包含触摸点的窗口 for (auto& window : windows) { // 3.1 检查窗口是否可触摸 if (!window->canReceiveTouch()) continue; // 3.2 检查触摸点是否在窗口区域内 if (!window->containsPoint(touchX, touchY)) continue; // 3.3 检查窗口是否被遮挡 if (isWindowObscured(window)) continue; // 3.4 找到了!添加到目标列表 mTargets.add(window); break; } // 4. 如果没找到,检查是否有壁纸窗口 if (mTargets.isEmpty()) { handleTouchOnWallpaper(); } }

这里有几个坑,我一个个说。

11.3.1 窗口遮挡检测

你想想看,如果两个窗口重叠了,上面的窗口是透明的,那触摸事件应该发给谁?

Android的处理方式是:默认发给最上面的窗口。但如果上面的窗口设置了FLAG_NOT_TOUCHABLE或者FLAG_NOT_TOUCH_MODAL,那事件就会穿透到下面的窗口。

避坑指南:我曾经遇到过一个Bug,弹窗挡住了底部的按钮,但弹窗本身设置了透明背景且没有消费触摸事件。结果用户点击弹窗区域,事件穿透到了底部的按钮上。解决方案就是给弹窗设置FLAG_NOT_TOUCH_MODAL并正确处理事件拦截。

11.3.2 触摸区域的计算

窗口的触摸区域不仅仅是它的布局边界。还要考虑:

  • 裁剪区域:父窗口可能对子窗口进行裁剪
  • 圆角裁剪:某些窗口有圆角效果
  • 安全区域:刘海屏、挖孔屏的避让区域

我记得有一次调试一个全面屏手机的触摸问题,用户点击屏幕边缘总是没反应。查了半天,发现是安全区域的计算出了问题,触摸点被判定为在安全区域之外,导致事件被丢弃。

11.4 两个方法的协作关系

你可能要问了:这两个方法会不会同时被调用?

答案是:会,但分场景。

事件类型调用的方法说明
按键事件(KeyEvent)findFocusedWindowTargets()发给焦点窗口
触摸事件(MotionEvent)findTouchedWindowTargets()发给触摸点下的窗口
轨迹球事件两者都可能取决于具体实现

嗯,这里要注意,触摸事件的分发优先级高于焦点事件。什么意思呢?比如用户正在触摸一个窗口,这时候来了一个按键事件,按键事件还是会发给焦点窗口,而不是触摸窗口。两者互不干扰。

11.5 源码中的关键数据结构

这两个方法操作的核心数据结构是InputTarget。每个InputTarget代表一个事件的目标窗口,包含:

struct InputTarget { // 目标窗口的输入通道 sp<InputChannel> inputChannel; // 标志位:FLAG_FOREGROUND, FLAG_WALLPAPER等 uint32_t flags; // 全局缩放比例(用于多窗口模式) float globalScaleFactor; // 窗口的变换矩阵 const InputWindowHandle* windowHandle; };

我个人习惯在调试时重点关注flags字段。它决定了事件是以什么方式发送给窗口的——是前台分发还是后台分发,是否需要壁纸处理等等。

11.6 实战经验总结

最后,分享几个我在项目中踩过的坑:

  1. 焦点窗口丢失:快速切换Activity时,焦点窗口可能短暂为null。解决方案是在findFocusedWindowTargets()里加一个重试机制。
  2. 触摸穿透:Dialog或PopupWindow没有正确设置FLAG_NOT_TOUCH_MODAL,导致事件穿透到下层窗口。
  3. 多指触摸:多点触控时,每个手指的触摸点可能落在不同窗口上。Android的处理方式是:第一个手指决定目标窗口,后续手指都发给同一个窗口。

调试技巧:如果你想观察事件分发的目标窗口,可以在InputDispatcher的日志里搜索findFocusedWindow或findTouchedWindow。开启adb shell dumpsys input也能看到当前窗口的焦点状态和触摸区域信息。

好了,这一章我们详细拆解了findFocusedWindowTargets()和findTouchedWindowTargets()的内部逻辑。下一章,我们会继续深入,看看事件找到目标窗口之后,是怎么通过InputChannel发送过去的。到时候我会重点讲一讲跨进程通信的细节,那也是很多性能问题的根源所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询