Android Touch Event 分发和处理(2)

前一篇 Android Touch Event 分发和处理中介绍了 TouchEvent 是怎样在 Activity, ViewGroup 和 View 之间分发和处理的。再深入一点看 TouchEvent 事件是怎样从系统来到 Activity 的。

下面的图很清晰地展示了 TouchEvent 是怎样在 Activity, ViewGroup 和 View 之间分发和处理的。

一个很自然的疑问是 TouchEvent 事件是怎样从系统来到 Activity 的。

Activity TouchEvent 事件

从 InputEventReceiver 到 DecorView

1
2
3
4
flowchart
native_code-->|dispatchInputEvent|InputEventReceiver
InputEventReceiver-->|onInputEvent|WindowInputEventReceiver
WindowInputEventReceiver-->|enqueueInputEvent|ViewRootImpl

整理了一下主要流程:

这里附上最关键的两个代码,方便大家查看。

之后执行 ViewRootImpl.deliverInputEvent() 方法,进入 ViewRootImpl 内部的 Input Pipeline

Input Pipeline 是一条 InputStage 链(责任链模式),可以粗略分成三个阶段:

  • preIme stage
  • ime stage
  • PostIme stage

重点看 ViewPostImeInputStage。看注释容易理解这个类的作用是将 Input Event 分发到 View 树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Delivers post-ime input events to the view hierarchy.
*/
final class ViewPostImeInputStage extends InputStage {
public ViewPostImeInputStage(InputStage next) {
super(next);
}

@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}

private int processGenericMotionEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;

if (event.isFromSource(InputDevice.SOURCE_TOUCHPAD)) {
if (hasPointerCapture() && mView.dispatchCapturedPointerEvent(event)) {
return FINISH_HANDLED;
}
}

// Deliver the event to the view.
if (mView.dispatchGenericMotionEvent(event)) {
return FINISH_HANDLED;
}
return FORWARD;
}
}

最关键的地方在第35行,这里的 mView 并不是一个普通的 View,它正是 Window.mDecor

从 DecorView 到 Activity

这个流程比较简单,就直接上代码了。

DecorView.dispatchGenericMotionEvent 代码如下:

1
2
3
4
5
6
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchGenericMotionEvent(ev) : super.dispatchGenericMotionEvent(ev);
}

以上第三行的 cb 正是 Activity,因为 Activity 创建对应的 Window 对象时,将自身作为 Window.Callback 参数,见以下第9行。(注:cb 也可能是 Dialog 或其他对象,简单起见我们暂且认为肯定是 Activity)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
@UnsupportedAppUsage
private Window mWindow;

final void attach(Context context, ActivityThread aThread, ...) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}

}

我们在 Activity.dispatchTouchEvent() 方法里面打个断点,对比方法调用栈很容易验证以上代码流程。

相关代码

前一节分析了 TouchEvent 事件是怎样从系统中的 InputEventReceiver 来到 Activity 的。这有点像观察水管里的水是如何流动的。

那水管是如何修起来的呢?我们可以看 Activity 以及其关联 Window 的创建过程。但我发现这有些不必要地让事情变得复杂。

其实我们完全可以把这里的 Activity 换 Dialog。因为从窗口管理和事件分发来看,Dialog 跟 Activity 没有本质的不同,但代码要简单得多。

几句话就能描述清楚:

  • Dialog 创建 Window
  • Window 创建 DecorView
  • Dialog 请求 WindowManager.addView() 添加 DecorView
  • 调用 WindowManagerImpl 和WindowManagerGlobal 对应方法
  • WindowManagerGlobal.addView() 创建 ViewRootImpl
  • 让 ViewRootImpl 持有 DecorView 的引用
  • ViewRootImpl 为 DecorView 创建 WindowInputEventReceiver (以及对应的 InputChannel)

Dialog.java - Android Code Search

  • Dialog 创建 Window
  • Window 创建 DecorView
  • Dialog 请求 WindowManager.addView() 添加 DecorView

特别注意这里的第18行 Window 将当前的 Dialog 设置为 callback。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,
boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == Resources.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}

mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
}

public void show() {
...
mDecor = mWindow.getDecorView();
...
mWindowManager.addView(mDecor, l);
...
}

从 WindowManager.addView() 到 WindowManagerGlobal.addView() 有一些跳跃性。需要了解一些背景知识,即这几个类之间的关系以及 Window 关联的 WindowManager 实例是如何创建的。


WindowManagerGlobal.java - Android Code Search

  • WindowManagerGlobal.addView() 创建 ViewRootImpl
  • 让 ViewRootImpl 持有 DecorView 的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
if (windowlessSession == null) {
root = new ViewRootImpl(view.getContext(), display);
} else {
root = new ViewRootImpl(view.getContext(), display,
windowlessSession, new WindowlessWindowLayout());
}
...
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
// BadTokenException or InvalidDisplayException, clean up.
if (viewIndex >= 0) {
removeViewLocked(viewIndex, true);
}
throw e;
}
}
}

ViewRootImpl.java - Android Code Search

  • ViewRootImpl 为 DecorView 创建 WindowInputEventReceiver (以及对应的 InputChannel)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
inputChannel = new InputChannel();
}

// IPC with WMs
mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);

if (inputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
Looper.myLooper());
}
...
}

到这里,水管就建好了。水可以流动了,也就是说:

  • Native 代码可以通过 InputChannel 向 WindowInputEventReceiver 发送 Input 事件
  • 这些事件经过 ViewRootImpl 和 DecorView,最终会来到 Dialog 或 Activity (它们都是 Window.Callback)

参考