在移动设备上,我们去做一些操作,无论是 Android 还是 IOS 其实在系统中是根据事件驱动的,用户通过屏幕与手机交互的时候,每一次的点击,长按,移动等都是一个事件。
而事件分发机制呢?他其实是因为 Android 每一个页面都是基于 Activity 进行实现的,一个 Activity 里面有若干个 View 以及若干个 ViewGroup 组成的,而事件分发机制就是某一个事件从屏幕传递给各个 View,由这个 View 来消费这个事件或者忽略这个事件,交与其他 View 进行消费的这个过程的控制。
事件分发的对象是什么呢?系统会把整个事件封装为 MotionEvent 对象,事件分发的过程就是 MotionEvent 对象分发的过程
结合我们的人为操作过程,事件的类型有下面四种:
所以说一个完整的事件序列是从手指按下屏幕开始,到手指离开屏幕为止所产生的一系列事件。也就是说一个完整的事件序列是以一个 ACTION_DOWN 事件开始,到一个 ACTION_UP 事件为止,中间有若干个 ACTION_MOVE 事件(当然可以没有)。
在同一个事件序列中,如果子 View / ViewGroup 没有消费该事件,那同一事件序列的后续事件就不会传递到该子 View / ViewGroup 中去。
那 Android 中是怎样传递事件的呢?
其实主要的对象就是 Activity,ViewGroup 以及 View。事件的分发就是对这一系列的传递的操作。接下来我们就围绕这三种主要的对象的事件分发来进行理解。
我们从流程图中可以知道当事件开始触发的时候会调用 dispatchTouchEvent
方法,那我们来看下对应的源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从源码中我们可以知道当事件的类型是 DOWN 的时候,会执行 onUserInteraction
方法
public void onUserInteraction() {
}
然后进入这个方法,我们可以发现在源码中该方法为空方法。所以说当我们需要监听按下手势的时候,重写 onUserInteraction
方法就可以达到监听的效果。
然后接着往下面看,我们就会发现会调用 Window 的 superDispatchTouchEvent
方法。假如消费了该事件的话,就会返回 true ,代表事件已被消费,否则调用 onTouchEvent
方法消费该事件。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
观看源码后我们发现其实 superDispatchTouchEvent
方法是一个抽象方法。我们看 Window 类的注释会发现
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
其实他有唯一的实现,就是 PhoneWindow 类,然后我们看下对应的实现类的方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
其实可以发现调用的是 DecorView 的 superDispatchTouchEvent
方法,而 DecorView 呢?其实就是 Activity 顶层的View,也我们 setContentView 方法传递进来的 layout 就是添加到了这个 View 上面。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
然后我们就发现他其实调用了他的父类的 dispatchTouchEvent
方法,也就是 ViewGroup 的 dispatchTouchEvent
方法,那这个方法就一起在后面的 ViewGroup 里面记录。
接下来就是说假如 getWindow().superDispatchTouchEvent(ev)
返回了 true ,那就什么该事件已经被消费了,直接返回就行,如果返回的是 false ,就说明当前任何视图都没有处理这个事件,那我们就是要调用 Activity 的 onTouchEvent
方法去消费该事件,并且直接返回 onTouchEvent
方法的结果。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
如果 mWindow.shouldCloseOnTouch(this, event)
返回结果为 true ,就将该 Activity finish掉,并且返回true,否则为 false。
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
mCloseOnTouchOutside 只有当 Activity 是以 Dialog 方式进行实现的时候才会为 true 否则为 false
然后怎么确保有 View 呢?其实 peekDecorView
方法就是用来获取当前 Activity 的 DecorView 的
最后怎么保证是点击在 View 外部的呢,其实就是靠 isOutside 变量。所以我们就可以理解为什么之前会调用 finish
方法了。所以说只有在当 Activity 是以 Dialog 方式进行实现的时候,并且点击了 View 外部的空白才会将该 Activity 关闭,否则不做任何处理直接消费事件。
上面说到了之后就会进入 ViewGroup 的 dispatchTouchEvent
方法,这个方法就是标志着事件已经到了 ViewGroup 这一层。然后呢?就是 onInterceptTouchEvent
方法,这个方法的意义就是是否拦截事件,假如返回结果为 true 的话,则代码该事件,当前的 ViewGroup 会拦截该事件,事件就不会再向下传递了。最后就是 onTouchEvent
方法了。这个方法在 ViewGroup 中没有实现,而是在 View 中进行实现的。这个方法就是用来当我们把事件拦截了以后,自己来处理这个事件重写的。
下面就是 ViewGroup 的事件分发流程图:
我们首先就来看一下 ViewGroup 的 dispatchTouchEvent
方法,源码的行数较多,就不贴上来了,其实这个方法主要就是做了三件事:
根据流程图我们可以发现从 onFilterTouchEventForSecurity
方法开始进入事件分发的过程
onFilterTouchEventForSecurity
方法做的就是一些安全策略的操作,主要的用处就是去判断这个 View 是不是可以被触摸,假如这个视图被其他视图遮挡,那就不会去处理这个事件。
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
如果这个方法返回了 false,那就说明安全策略不通过,所以直接返回 false,否则的话再进行后面的事件分发。
然后我们就会对这个事件的类型进行判断,假如这个事件的类型是一个按下的操作的时候,就回去做一些初始化的操作,因为按下是一个事件系列的开始。
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
cancelAndClearTouchTargets
方法是用于取消和清除所有的触摸目标,然后通过 resetTouchState
方法来重置触摸状态。
然后就要开始检测当前的事件是否是需要拦截的,就是靠 intercepted 这个变量去记录的。
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); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
如果当前事件为按下事件或者是已经有处理改事件的子 View 的时候就要进入后面的判断,否则就为 true,表示的是事件被拦截,事件就不会再向下传递。disallowIntercept 变量的意义就是判断当前事件是否可以拦截,如果为 true 的话,就代表当前事件在这个 ViewGroup 是不允许被拦截的,如果为 false 代表这个事件是可以被拦截的,然后就要通过 onInterceptTouchEvent
方法来判断是否对事件拦截。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
这几个判断条件都是什么呢?
只有这四个条件都满足的话,我们才会去拦截这个事件。所以说一般情况下 onInterceptTouchEvent
方法都是返回 false,不去拦截该事件的。
接下来就是判断改事件是不是一个取消事件:
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
然后再去判断该事件是不是作用与多个视图:
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
最后如果即通过了安全判断也不是取消事件以后就是 开始进入事件分发的逻辑。
if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)
当事件的类型是按下或者是移动的时候进入事件的分发,首先系统就会清除之前触摸点的信息,然后判断当前触摸点是否大于 0,之后就去获取当前触摸点的坐标。并且获取到可以接受到该触摸事件的子 View 的集合 preorderedList,以及判断是否对自定义 View 绘制顺序有要求。
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
然后就开始对这个子 View 列表进行遍历。然后通过索引获取到每一个子 View
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
getAndVerifyPreorderedIndex
方法里面就利用到了我们之前的 customOrder 变量,如果当前的 View 的绘制是有自定义顺序的话就要通过 getChildDrawingOrder
方法去获取,这个方法就是在我们自定义绘制顺序的时候需要重写的方法,否则索引就是列表的下标。
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
final int childIndex;
if (customOrder) {
final int childIndex1 = getChildDrawingOrder(childrenCount, i);
if (childIndex1 >= childrenCount) {
throw new IndexOutOfBoundsException("getChildDrawingOrder() "
+ "returned invalid index " + childIndex1
+ " (child count is " + childrenCount + ")");
}
childIndex = childIndex1;
} else {
childIndex = i;
}
return childIndex;
}
然后就是通过 getAndVerifyPreorderedView
方法去获取对应的 View。
private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children, int childIndex) {
final View child;
if (preorderedList != null) {
child = preorderedList.get(childIndex);
if (child == null) {
throw new RuntimeException("Invalid preorderedList contained null child at index "
+ childIndex);
}
} else {
child = children[childIndex];
}
return child;
}
然后就是判断这个 View 能否接受到触摸事件以及当前的触摸事件是不是在这个 View 范围之内。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
如果两个方法都返回了 true,就说明这个 View 就可以处理该事件。然后就要获取当前 View 的触摸对象。
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
如果该 View 已经有对应的触摸对象的话直接退出循环即可,否则的话就表示该 View 还没有对应的触摸事件,然后就要去判断,该 View 有没有设置不接收触摸事件的标志位,如果有的话就清除这个标志。
然后就是最主要的一个方法了,dispatchTransformedTouchEvent
方法里面讲述了一个事件是如何从一个 ViewGroup 传递到一个具体的 View 中是如何过度的。
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
首先我们会判断该事件是否为一个取消事件,如果是取消事件的话,就要去判断是否有 View 处理,如果 child 为 null 则直接用 ViewGroup 父类的 dispatchTouchEvent
方法处理,否则调用 View 的方法,最后直接返回结果。
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
如果不是取消事件的话,就会去获取一个新的指针位,如果指针位为 0 的话,直接返回 false
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
然后呢就要去判断原有的指针位和新的指针位是不是一样的,如果是一样的就要去判断子视图是否存在,如果没有的话还是调用父类的 dispatchTouchEvent
方法处理,否则就要去计算子 View 的偏移量,然后调用子 View 的 dispatchTouchEvent
方法处理。
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
如果前面的两个条件都不满足,没有返回值的话,就要创建出一个 MotionEvent 类,然后再去判断子 View 是否为空,如果为空的话还是调用父类的 dispatchTouchEvent
方法处理,否则就要去计算子 View 的偏移量,然后调用子 View 的 dispatchTouchEvent
方法处理。最后释放相关资源。
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
然后如果 dispatchTransformedTouchEvent
方法返回了 true 的话,就代表了已经传递到了子 View 的 dispatchTouchEvent
方法了,也就代表了该事件已经被消费了,所以就可以直接结束。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
这样我们就解决了当前事件不为取消事件以及子 View 允许事件传递的情况。然后就要看通用的情况了。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
在这个时候 mFirstTouchTarget 还是为 null 的话,就说明没有子 View 去执行这个事件,就要通过 dispatchTransformedTouchEvent
方法去消费事件获取返回值,我们就可以发现他的 View 的传值为 null,在之前讲的 dispatchTransformedTouchEvent
方法里面,就会交与他的父类去执行 dispatchTouchEvent
方法。
如果不为空的话就会遍历整个链表,假如在之前已经处理了的话,他就会直接返回 true,否则的话就会去重新分发事件。
else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
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;
}
}
最后就到了最后一步 View 了。Android 中事件在 View 里面会怎么进行处理呢?首先和前面是一样的,就是 dispatchTouchEvent
方法,这个方法就是标志着事件已经到了 View 这一层。然后呢?就是 onTouchEvent
方法了。这个方法里面就是 Android 系统处理触摸事件的相关逻辑。
下面就是 View 的事件分发流程图:
首先判断该 View 是否有可相应焦点,如果没有的话直接返回 false。
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
然后对事件的类型进行判断,如果当前的事件为按下事件的话,如果存在视图滚动效果的话就要立刻停止滚动
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
之后就开始进入真正的事件分发的过程了,首先和 ViewGroup 是一样的,通过 onFilterTouchEventForSecurity
方法来进行事件的安全判断。这块的逻辑和 View Group 中是一样的,如果不符合的话就返回 false ,如果符合的话就要接下来进行事件的处理了。
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
首先就是判断当前的操控方式是不是为鼠标操作,如果为鼠标操作的话,返回值就可以直接为 ture 了,表示消费了该事件。然后就要检查是否有触摸事件的监听,然后就要调用 listener 的 onTouch
方法,如果结果返回的是 true,则我们的返回结果也为 true。如果还是不去消费该事件的话,就要调用 View 的 onTouchEvent
方法,根据返回的结果来进行返回。
然后就要进入 View 的 onTouchEvent
方法里面来看了
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
首先获取到点击的位置坐标,View 的标志位,事件类型以及可点击状态,然后去判断该 View 是否处于一个禁用状态的话,返回结果就为可点击的状态,这样就可以说明,当 View 是处于一个禁用状态的话,如果是可点击的,也会去消费这个事件,但是因为是直接返回的,所以说不会去多事件有所响应。
然后就会去判断有没有设置触摸的代理。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
如果有的话就要通过代理的 onTouchEvent
方法去获取结果,如果能够消费事件的话就直接返回 true,否则继续后面的事件处理。
最后就是去判断是否为一个可点击的状态或者在标记位上 TOOLTIP 位为 1 的话,就会开始根据事件类型的不同做不同的处理,然后返回 true,表示事件已经被消费,否则返回结构为 false。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
case MotionEvent.ACTION_DOWN:
...
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
}
return true;
}
return false;
}