一 概述
从上一篇文章 Android Input系统6 事件处理全过程 中,我们知道当输入事件从 InputDispatcher 通过 server 端的 InputChannel 发送给应用程序的 client 端,其实最终事件被发送到了 java 层的 ViewRootImpl 中。
对于 java 层的事件分发从 ViewRootImpl.java 的 deliverInputEvent 开始,代码如下:
private void deliverInputEvent(QueuedInputEvent q) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
q.mEvent.getSequenceNumber());
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
}
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
if (q.mEvent instanceof KeyEvent) {
mUnhandledKeyManager.preDispatch((KeyEvent) q.mEvent);
}
if (stage != null) {
handleWindowFocusChanged();
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
ViewRootImpl 类定义了多种类型的 InputStage,以责任链模式进行处理分发输入事件。
至于事件是如何通过 InputStage 传递到应用窗口的,我们在上篇文章中并没有讨论,现在在这篇文章中我们来研究这个问题。
二 InputStage
2.1 InputStage说明
Android 应用程序对输入系统的处理分为多个阶段,我们把这些阶段称为 InputStage
ViewRootImpl 中定义了多种类型的 InputStage,如下:
final class SyntheticInputStage extends InputStage{}
final class ViewPostImeInputStage extends InputStage{}
final class NativePostImeInputStage extends AsyncInputStage{}
final class EarlyPostImeInputStage extends InputStage{}
final class ImeInputStage extends AsyncInputStage{}
final class ViewPreImeInputStage extends InputStage{}
final class NativePreImeInputStage extends AsyncInputStage{}
这里对各个 InputStage 进行简要说明:
InputStage | 说明 |
---|
NativePreImeInputStage | 分发早于 IME 的 InputEvent 到 NativeActivity 中去处理,NativeActivity 和普通 acitivty 的功能一致,不过是在 native 层实现,这样执行效率会更高,同时 NativeActivity 在游戏开发中很实用(不支持触摸事件) | ViewPreIMEInputStage | 分发早于 IME 的 InputEvent 到 View 框架处理,会调用 view(输入焦点)的 onkeyPreIme 方法,同时会给 View 在输入法处理 key 事件之前先得到消息并优先处理,View 系列控件可以直接复写 onKeyPreIme( 不支持触摸事件) | ImeInputStage | 分发 InputEvent 到 IME 处理,调用 ImeInputStage 的 onProcess,InputMethodManager 的 dispatchInputEvent 方法处理消息(不支持触摸事件) | EarlyPostImeInputStage | 与 touchmode 相关,比如你的手机有方向键,按方向键会退出 touchmode,这个事件被消费,有可能会有 view 的背景变化,但不确定(支持触摸事件) | NativePostImeInputStage | 分发 InputEvent 事件到 NativeActivity,IME 处理完消息后能先于普通 Activity 处理消息(此时支持触摸事件) | ViewPostImeInputStage | 分发 InputEvent 事件到 View 框架,view 的事件分发(支持触摸事件)。最终会调用到输入焦点的3个方法:使用 setKeyListener 注册的监听器的 onKey ,之后是 onKeyDown 和 onKeyUp,或者调用 activity 的 onKeyDown 和 onKeyUp 方法,也就是兜底处理无人处理的 key 事件 | SyntheticInputStage | 未处理 InputEvent 最后处理 |
InputStage 将输入事件的处理分成若干个阶段(Stage),如果当前有输入法窗口,则事件处理从 NativePreIme 开始,否则从 EarlyPostIme 开始,事件依次经过每个 Stage,如果该事件没有被标识为 “Finished”, 该 Stage 就会处理它,然后返回处理结果 Forward 或 Finish,Forward 运行下一个 Stage 继续处理,而 Finished 事件将会简单的 Forward 到下一级,直到最后一级 SyntheticInputStage。
这里使用一张图来描述 InputStage 的处理流程,如下所示:  责任链模式的各个 InputStage 从整体流程如上来说,简要归纳如下:
2.2 InputStage初始化
在 ViewRootImpl 的 setView 方法中完成了 InputStage 的初始化操作:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
......
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
}
}
}
在上篇文章中我们知道,最终执行的是 mFirstPostImeInputStage 的 deliver 方法。我们来看下 InputStage 的 deliver: ViewRootImpl#InputStage
2.3 InputStage.deliver
public final void deliver(QueuedInputEvent q) {
if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
forward(q);
} else if (shouldDropInputEvent(q)) {
finish(q, false);
} else {
apply(q, onProcess(q));
}
}
protected void forward(QueuedInputEvent q) {
onDeliverToNext(q);
}
protected void onDeliverToNext(QueuedInputEvent q) {
if (DEBUG_INPUT_STAGES) {
Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);
}
if (mNext != null) {
mNext.deliver(q);
} else {
finishInputEvent(q);
}
}
protected void apply(QueuedInputEvent q, int result) {
if (result == FORWARD) {
forward(q);
} else if (result == FINISH_HANDLED) {
finish(q, true);
} else if (result == FINISH_NOT_HANDLED) {
finish(q, false);
} else {
throw new IllegalArgumentException("Invalid result: " + result);
}
}
从以上代码可以看到,mFirstPostImeInputStage 的 deliver 函数,最终会执行到 ViewPostImeInputStage 的 onProcess:
2.4 ViewPostImeInputStage.onProcess
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);
}
}
}
}
onProcess 方法用于输入事件的具体处理,此方法中将按键事件和触摸事件分开,本篇文章主要研究触摸事件的流程,对于触摸事件,根据事件源分了三个方法,普通的触摸事件由 processPointerEvent 处理。
2.5 ViewPostImeInputStage.processPointerEvent
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
......
boolean handled = mView.dispatchPointerEvent(event);
......
return handled ? FINISH_HANDLED : FORWARD;
}
mView 指向当前界面的顶层布局 DecorView,handled 作为此次事件分发的结果返回,DecorView 并没有重写父类的 dispatchPointerEvent 方法,所以接着到 View 中看此方法实现。
至此事件分发中的 InputStage 阶段分析完毕。接下来就是 View 的事件分发了。
三 View事件分发
3.1 View.dispatchPointerEvent
@UnsupportedAppUsage
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
针对 Touch 事件又会调 dispatchTouchEvent 方法,此方法在 DecorView 中有重写:
3.2 DecorView.dispatchTouchEvent
DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
这里的 Window.Callback 指向当前 Activity,在 Activity 启动时会调用自己的 attach 方法,此方法中会将自己作为 callback 传给 window,
Activity.java
final void attach(......){
......
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
......
}
所以 mWindow.getCallback() 获取的就是当前的 Activity,接着会调用 Activity 的 dispatchTouchEvent 进一步分发事件:
3.3 Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
onUserInteraction 在 Activity 中是个空方法,它的作用应该是告诉开发者此 Activity 接收到事件了。
接着就会调用 PhoneWindow 的 superDispatchTouchEvent 方法,其返回值代表目标 View 对事件的处理结果,我们可以看到如果目标 View 没有消费掉此次事件(即返回值为 false)则会调用 Activity 的 onTouchEvent 来处理。
3.4 PhoneWindow.superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
3.5 DecorView.superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
这里调用 super.dispatchTouchEvent 最后到了 ViewGroup 中,此时才真正开始 View 的事件分发,我们可以看到输入事件从 native 层到 ViewRootImpl 之后会经过:DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup 的流程。
ViewGroup 可以理解为 View 事件分发的起始点,事件分发机制其实分为两部分,一部分为事件的下发,一部分为事件处理结果的上报。
事件下发的目的是找到处理该事件的目标 View,事件上报的目的是将最终的处理结果返回给 ViewPostImeInputStage:
ViewPostImeInputStage.processPointerEvent
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
......
boolean handled = mView.dispatchPointerEvent(event);
......
return handled ? FINISH_HANDLED : FORWARD;
}
即上述方法中的 handled 就是事件处理上报的最终结果,有了这个结果就可以确定此次事件是朝着 InputStage 责任链的下一环继续分发还是就此结束。
3.6 ViewGroup.dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
......
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
....
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
...
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
......
}
ViewGroup 的 dispatchTouchEvent 比较长,但是逻辑不算复杂,上述方法我们省略了鼠标相关和多点触控相关的代码,就单点触控的事件分发来看逻辑很简单,很多代码其实都是对分发的条件判断,比如某个子 View 想要接收事件,它起码得不被遮挡,然后你触摸的地方得在这个 View 的边界内吧,这个 View 还得处于 Z-order 的最顶层等,只有经过各种条件的判断这个 View 最后才有资格拿到事件进行处理。
ViewGroup.dispatchTouchEvent 总结:
- 首先会调用 onFilterTouchEventForSecurity 方法对此次事件进行初步判断,判断的是 ViewGroup 是否被遮挡或者隐藏
- 步骤 1 的条件判断通过之后,接着对于 ACTION_DOWN 事件会清空上一个事件序列(一个事件序列通常由一个 ACTION_DOWN,N 个 ACTION_MOVE,一个 ACTION_UP 组成)留下的各种状态,最主要是清空 TouchTarget 链表
- 接着会有两个条件来判断是否走 ViewGroup 的拦截机制
- 条件1:此次事件是否为 ACTION_DOWN
- 条件2:mFirstTouchTarget 是否为空
这两个条件的意思是:对于一个事件序列的 ACTION_DOWN 事件,一定会走 ViewGroup 的拦截机制,并且同一事件序列的一个事件如果被拦截了,那么后续事件默认都会被拦截而不会再走拦截方法 onInterceptTouchEvent,子 View 可以通过 requestDisallowInterceptTouchEvent 方法请求父 View 不要拦截。
- 接着又会有两个条件来判断事件是否继续分发,canceled 和 intercepted,事件被取消和被拦截,其实 canceled 多半是因为 intercepted 导致的,这个后面再说
- 对于没有被取消且没有被拦截,且是 ACTION_DOWN 的事件,就要开始遍历 View 树,寻找真正消费事件的子 View 了,这里为何会单独对 ACTION_DOWN 进行判断呢?这是因为一个事件序列可能包含多个触摸事件,而触摸事件寻找消费的子 View 是通过递归遍历 View 树,为了性能考虑,Android 的设计为:当接收到 ACTION_DOWN 时开始对 View 树进行遍历,找到最终消费事件的子 View 之后将其保存,同一事件序列的后续 ACTION_MOVE,ACTION_UP 则不再需要遍历,直接将事件发送给保存好的子 View 就行了,对子 View 的保存就用到了 TouchTarget,这是一种链表结构,后面再说
- 对于目标子 View 的寻找就比较简单了,首先将当前 ViewGroup 的所有子 View 以 Z-order 的顺序进行重建,保存在一个 list 中,然后遍历 list,从 Z-order 最大的子 View 开始,遍历条件有两个:当前遍历的子 View 可以接收事件,并且触摸区域落在当前子 View 之内则说明成功找到子 View,然后调用 dispatchTransformedTouchEvent 执行子 View 事件处理流程,如果事件成功处理则会为此子 View 构建 TouchTarget,并赋值给 mFirstTouchTarget
- 接着对于 mFirstTouchTarget 不为空的情况会遍历链表,其目的是在上一步中已经找到接收事件的目标子 View 并且保存到了 TouchTarget 链表,对于一个事件序列的后续事件只需要遍历链表分发事件就行了
- 对于没有找到消费事件的子 View 即 mFirstTouchTarget 为空,以及事件被取消的情况会做一些收尾工作
上面的总结主要是针对单点触控的情况,如果是多点触控还要复杂一些,同学们可以自行研究。
我们接着来理解 TouchTarget,这个数据结构对于事件分发非常重要,TouchTarget 在整个 ViewGroup 事件分发中是以链表的形式存在,它保存了一个 ViewGroup 中接收事件的目标子 View。
3.7 TouchTarget
private TouchTarget mFirstTouchTarget;
private static final class TouchTarget {
......
public View child;
......
public TouchTarget next;
@UnsupportedAppUsage
private TouchTarget() {
}
...
}
在 ViewGroup 中只有一个地方会构造 TouchTarget,就是在接收到 ACTION_DOWN时,此时会遍历子 View 并找到目标子 View,且目标子 View 成功处理事件之后,就会为此子 View 构建对应 TouchTarget,而多个 ViewGroup 的多个目标子 View 就构成了 TouchTarget 事件分发的链表:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
这种代码就是典型的链表,mFirstTouchTarget 作为链表的头指针,我们用图来表示,第一次构建 TouchTarget :  第二次构建 TouchTarget :  以此类推,mFirstTouchTarget 总是指向最新构建的 TouchTarget:  对于上面的布局结构,如果我们点击 Button,事件的分发就是从 DecorView->ViewGroup A->ViewGroup B->ViewGroup C->Button,mFirstTouchTarget 最终作为头指针指向事件分发流程的所有 View 所构建的 TouchTarget 链表,有了 TouchTarget 链表,只需要在事件序列开始即 ACTION_DOWN 到来时进行一次 View 树遍历,后续的 ACTION_DOWN,ACTION_UP 就可以直接通过 TouchTarget 链表进行分发。
3.8 事件拦截onInterceptTouchEvent
我们继续来看事件拦截,ViewGroup 提供了 onInterceptTouchEvent 用来对事件进行拦截,默认情况父 View 是不会拦截事件的,什么情况下需要拦截呢?  比如这种情况,一个 ListView 中嵌套了多个 Button,当我们点击 Button 进行上下滑动操作时,此时我们并不希望 Button 来处理事件,而是希望 ListView 处理,而我们如果仅仅是点击 Button,此时又希望 Button 来处理事件,这时就需要 ListView 有条件的对事件进行拦截,比如判断用户滑动的距离,速度等。
对于事件拦截的使用很简单,只需要重写 ViewGroup 的 onInterceptTouchEvent 方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {
....
return true;
}
返回 true 就能实现拦截,事件的拦截同样是针对一个事件序列来说的,当一个事件序列的事件被父 View 拦截,那么此事件序列的后续事件子 View 都收不到,这就引出一个问题,像 Button 这样的控件是需要收到 ACTION_UP 才能对事件进行处理的,因为 Button 需要判断当前事件是点击还是长按,如果在 ACTION_MOVE 时 Button 的事件被拦截掉,那么 Button 同样收不到后续的 ACTION_UP 事件,这个时候就需要引入一种新的事件用于告诉被拦截掉事件的子控件,它就是 ACTION_CANCEL,有了 ACTION_CANCEL 就可以对事件被拦截掉的情况做收尾工作。
3.9 子View事件处理dispatchTouchEvent
对于事件的具体处理就比较简单了:
public boolean dispatchTouchEvent(MotionEvent event) {
......
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
首先判断该 View 是否设置了触摸监听,即是否调用过 setOnTouchListener 方法,如果有则将事件传入其 onTouch 方法进行处理并返回结果,如果没有设置,则会调用 View 的 onTouchEvent 方法,此方法中会有条件的调用 onClick,对于事件处理的顺序就是:onTouch,onTouchEvent,onClick。
关于 View 事件分发的详细介绍还请参考 Android View事件分发机制。这篇文章详细介绍了其中的细节。
(完毕)
|