直接用WindowManager添加的View(以下简称"WM View")的事件传递路径与Activity中的View确实存在显著差异,这主要是由于它们所处的窗口层级结构和依附关系不同所导致的。下面将从源码层面为你分析。
WindowManager添加View的事件传递机制与差异分析
1️⃣ 核心差异概览
在深入源码之前,我们先通过一个表格直观对比两种方式的事件传递差异:
| 特性维度 | Activity中的View | WindowManager直接添加的View |
|---|---|---|
| 窗口类型 | 应用窗口(Application Window) | 系统窗口/子窗口(System/Sub Window) |
| 依附关系 | 依附于Activity的Window | 直接依附于WindowManagerService |
| 事件接收起点 | ViewRootImpl → DecorView → Activity | 直接由ViewRootImpl处理 |
| Touch事件回调入口 | Activity.dispatchTouchEvent() | ViewRootImpl.WindowInputEventReceiver |
| 事件拦截与分发机制 | 经过完整的View树层级分发(ViewGroup/View) | 直接由ViewRootImpl分发给目标View |
| 默认的消费行为 | 由View树的onTouchEvent()决定 | 依赖View自身的onTouchEvent()或OnTouchListener |
| 标记位(Flags)影响 | 受Activity Window属性影响 | 独立配置的LayoutParamsflags(如NOT_FOCUSABLE) |
| 输入焦点处理 | 通过Activity窗口自动管理 | 需显式配置LayoutParamsflags |
| 层级(Z-order) | 应用层级(1-99) | 系统层级(2000-2999)或子窗口层级(1000-1999) |
| WMS中的窗口状态管理 | 通过ActivityRecord、Task等关联 | 作为独立窗口状态管理 |
2️⃣ WindowManager添加View的事件传递路径
直接用WindowManager添加的View(如悬浮窗),其事件传递路径相对直接,不经过Activity的层级结构:
InputManagerService → WindowManagerService → ViewRootImpl → 目标View
下面这张序列图描绘了WM View事件的完整传递过程:
2.1 核心源码解析
-
ViewRootImpl的setView与输入通道建立
当你调用WindowManager.addView()时,最终会调用到WindowManagerGlobal.addView(),其核心工作是创建ViewRootImpl并调用其setView()方法。// WindowManagerGlobal.java public void addView(View view, ViewGroup.LayoutParams params, ...) { ... ViewRootImpl root = new ViewRootImpl(view.getContext(), display); ... root.setView(view, wparams, panelParentView); ... }在
ViewRootImpl.setView()中,会通过WindowSession(一个Binder代理对象,指向WMS)将窗口添加到WMS,并建立输入事件接收的管道(InputChannel) :// ViewRootImpl.java public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { ... // 1. 添加窗口到WMS,并建立InputChannel res = mWindowSession.addToDisplay(..., mInputChannel); ... if (mInputChannel != null) { if (mInputQueueCallback != null) { mInputQueue = new InputQueue(); mInputQueueCallback.onInputQueueCreated(mInputQueue); } // 2. 创建WindowInputEventReceiver,用于接收输入事件 mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } }WMS通过
openInputChannel()方法创建一对关联的InputChannel。其中一个在WMS端,另一个通过Binder传递回APP进程的ViewRootImpl端。这对InputChannel构成了一个全双工的管道,是APP进程与系统服务WMS之间传递输入事件的桥梁。 -
输入事件的接收与分发
事件接收的核心是WindowInputEventReceiver。当Linux内核捕捉到触摸事件,经由InputReader、InputDispatcher处理,最终WMS找到目标窗口,通过InputChannel将事件发送到APP进程。ViewRootImpl的WindowInputEventReceiver的onInputEvent()方法会被调用:// ViewRootImpl.java - WindowInputEventReceiver @Override public void onInputEvent(InputEvent event) { // 将事件加入队列并进行分发 enqueueInputEvent(event, this, 0, true); }enqueueInputEvent()会最终调用deliverInputEvent()方法进行事件分发8。 -
ViewRootImpl中的事件处理阶段(InputStage)
ViewRootImpl中有一系列InputStage(输入事件处理阶段)构成的责任链,用于处理不同类型的事件或执行不同阶段的操作8。对于触摸事件,最重要的阶段是ViewPostImeInputStage:// ViewRootImpl.java private void deliverInputEvent(QueuedInputEvent q) { ... // 沿着InputStage责任链处理事件 if (stage != null) { stage.deliver(q); } ... }在
ViewPostImeInputStage.processPointerEvent()中,事件被直接分发给目标View的dispatchTouchEvent()方法:// ViewPostImeInputStage.java @Override protected int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; ... // 直接将事件分发给View boolean handled = mView.dispatchPointerEvent(event); ... return handled ? FINISH_HANDLED : FORWARD; }这里的
mView就是你通过WindowManager.addView()添加的View。 -
View对事件的处理
事件最终到达你添加的View的dispatchTouchEvent()方法。此后的逻辑就与常规View的事件处理一致了79:- 如果设置了
OnTouchListener且View是ENABLED状态,则优先执行OnTouchListener.onTouch()。若onTouch返回true,则表示事件被消费,流程结束。 - 否则,会调用
View自身的onTouchEvent()方法。
- 如果设置了
3️⃣ 与Activity中View事件传递的差异原因
为什么两者的事件传递路径不一样?核心原因在于窗口层级和依附关系的根本差异。
-
窗口层级与依附关系不同
- Activity的View树:依附于Activity的
PhoneWindow和DecorView,是应用窗口(类型为TYPE_APPLICATION)。它的层级在WMS中由ActivityRecord、Task等统一管理。 - WM直接添加的View:通常被创建为系统窗口(如
TYPE_APPLICATION_OVERLAY或TYPE_SYSTEM_ERROR)或子窗口。它是一个独立的窗口,直接由WMS管理,与Activity的窗口平级或有自己的层级,没有父窗口或与Activity的窗口无直接关联。
- Activity的View树:依附于Activity的
-
InputChannel的归属与事件派发起点不同
正因为窗口层级独立,WMS会为WM View单独创建InputChannel。系统服务(InputDispatcher)识别到触摸事件发生在该窗口区域后,会通过其独立的InputChannel直接将事件发送到与之关联的ViewRootImpl,而不会经过Activity所在窗口的ViewRootImpl或DecorView8。这导致了事件传递路径的根本差异。 -
缺失的中间环节:Activity与PhoneWindow
Activity的View事件传递中,DecorView接收到事件后,会先调用Activity.dispatchTouchEvent():// DecorView.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { final Callback cb = getCallback(); return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); }这里的
Callback就是Activity。因此事件会先交给Activity处理,之后才继续向下分发。
WM添加的View:它没有附加到Activity的PhoneWindow和DecorView上,因此自然不会回调Activity的任何事件分发方法。事件直接从它的ViewRootImpl就到了它自身。 -
标记位(Flags)的影响
用WindowManager添加View时,通常会配置一些特殊的LayoutParams flags,例如:FLAG_NOT_FOCUSABLE:窗口不需要焦点,按键事件会直接传递给下层窗口。FLAG_NOT_TOUCH_MODAL:窗口区域内的触摸事件自己处理,区域外的事件传递给下层窗口24。
这些Flags会直接影响WMS对触摸事件的分派决策。例如,一个FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL的悬浮窗,自身消费不掉的触摸事件(比如点在了View的空白处)会顺利传递给下层的Activity窗口;而如果设置了FLAG_WATCH_OUTSIDE_TOUCH,则还能接收到落在窗口区域外但仍在屏幕上的触摸事件。Activity的窗口通常没有这些特殊设置,其事件处理更依赖于View树自身的逻辑。
4️⃣ 关键源码定位(基于Android SDK)
-
WindowManagerGlobal.addView()
- 作用:创建ViewRootImpl,建立View与ViewRootImpl的联系。
- 位置:
frameworks/base/core/java/android/view/WindowManagerGlobal.java
-
ViewRootImpl.setView()
- 作用:通过WindowSession与WMS通信,添加窗口、建立InputChannel。
- 位置:
frameworks/base/core/java/android/view/ViewRootImpl.java
-
ViewPostImeInputStage.processPointerEvent()
- 作用:处理非输入法相关的指针(触摸)事件,直接调用View的dispatchPointerEvent。
- 位置:
frameworks/base/core/java/android/view/ViewPostImeInputStage.java(或查看ViewRootImpl中内部类)
-
View.dispatchTouchEvent()
- 作用:View处理事件的入口,判断OnTouchListener和onTouchEvent的调用。
- 位置:
frameworks/base/core/java/android/view/View.java
-
WindowManagerService.addWindow()
- 作用:WMS端创建窗口对象(WindowState),处理InputChannel的注册。
- 位置:
frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java(AOSP路径)
5️⃣ 总结与启示
直接用WindowManager添加的View因其独立的窗口层级和直接与WMS关联的特性,导致其事件传递路径绕过了Activity的DecorView和PhoneWindow,直接由自身关联的ViewRootImpl接收并分发给View自身处理。
理解这种差异对于处理悬浮窗、系统弹窗等场景的触摸交互至关重要。例如,你需要更加注意WM View的LayoutParams配置(如flags、type),因为它们直接决定了WMS如何向你的窗口派发事件。同时,由于事件不经过Activity,一些依赖于Activity事件回调的逻辑可能需要在你自定义的View内部实现。
希望这份基于源码的分析能帮助你更透彻地理解Android的事件分发机制。