Anadorid的事件分发机制

一、点击事件的传递规则
点击事件的事件分发,其实就是MotionEvent事件的分发过程。当一个MotionEvent产生后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是分发过程。点击事件的分发过程是由三个很重要的方法来共同完成:diapatchTouchEvent、onInterceptTouchEvent和onTouchEvent
1.diapatchTouchEvent
用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent的影响,表示是否消耗当前事件。
2.onInterceptTouchEvent
在diapatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
3.onTouchEvent
在diapatchTouchEvent内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。
对于根ViewGroup,点击事件产生后,首先会传递给它,这时他的dispatchTouchEvent就会被调用,如果ViewGroup的onInterceptTouchEvent返回true,表示需要拦截事件,事件就会交给ViewGroup处理,调用他的OnTouchEvent方法。如果ViewGroup的onInterceptTouchEvent返回false,不拦截事件,子元素的dispatchTouchEvent就会被调用,如此反复,直到事件被最终处理。
设置了OnTouchListener,那么监听的onTouch会被回调,如果返回true,那么onTouchEvent方法将不会被调用。由此可见onTouchListener优先级比OnTouchEvent高。onTouchEvent中,如果设置了OnClickListener,onClick方法会被调用,其优先级最低。
点击事件产生后,遵循如下顺序:Activity->Window->View。View接收到事件后,就会按照事件分发机制去处理。
如果子View的OnTouchEvent返回false,那么它的父容器的OnTouchEvent会被调用,以此类推,最终会传递给Activity的OnTouchEvent方法。
4.一些结论
- 同一个事件序列,是手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束。这个事件序列以down事件开始,中间是数量不定的move事件,最终以up事件结束。
- 正常情况下,一个事件序列只能被一个View拦截消耗。
- 某个View一但拦截,那么这个事件序列都只能由它来处理(如果能够传递给他),并且它的onInterceptTouchEvent不会再被调用。
- 某个View开始处理事件,如果不消耗down事件(onTouchEvent返回false),那么同一事件序列不会在交给他处理,会传给父容器的OnTouchEvent
- 如果View不消耗down事件之外的其它事件,那么这个点击事件会消失,父元素的onTouchEvent并不会被调用,并且View能接受到后续的事件。
- ViewGroup不拦截任何事件,onInteceptTouchEvent默认返回false。
- View没有onInteceptTouchEvent,一但事件传递给它,直接调用其onTouchEvent方法。
- View的onTouchEvent默认消耗事件,返回true,设置clickable和longClickable、contextClickable同时为false且悬浮显示关闭,才会返回false。
- View的enable属性不影响onTouchEvent的默认返回值
- onClick的前提是,当前View是可点击的,并且它收到了down和up事件。
- 事件传递是由外向内的,事件总是先传递给父元素,然后父元素再分发给子View,通过requestDisallowIntercpetTouchEvent
方法可以干预父元素的事件分发过程,但是down事件除外。
二、事件分发的源码分析
1.activity对点击事件的分发过程
当一个点击操作发生时,事件线传递给当前的activity,由当前的activity的dispatchTouchEvent进行事件分发,具体工作是由Activity内部的Window来完成的。window会把事件传递给decorView,当前界面顶层容器。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
window是一个抽象类,superDispatchTouchEvent方法也是一个抽象方法。其唯一的实现是PhoneWindow。
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow中mDecor就是顶级的DecorView。
2.顶级View对点击事件的分发过程
顶级View一般是个GroupView,其对点击事件的分发过程,主要是在diapatchTouchEvent方法中。
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); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
这两种情况下会判断是否要拦截当前事件:事件类型是ACTION_DOWN或者mFirstTouchTarget != null。当事件由子元素成功处理,mFirstTouchTarget 就会指向子元素,就不为null了。
所以当ViewGroup进行拦截时,mFirstTouchTarget != null不成立,当ACTION_MOVE或者ACTION_UP事件到来,将会导致ViewGroup的onIntercpetTouchEvent不会再被调用。
FLAG_DISALLOW_INTERCEPT这个标记为是通过requestDisallowInterceptTouchEvent方法来设置,设置后,ViewGroup无法拦截ACTION_DOWN之外的事件。
ViewGroup会在ACTION_DOWN事件到来时,重置状态,所以View调用requestDisAllowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。
ViewGroup不拦截事件的时候,事件会分发给子View进行处理。
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);
// 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();
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 there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
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);
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
遍历所有子元素,再判断子元素是否能够接收到点击事件。点击事件是否落在子元素的区域内和子元素是否在播动画,只要同时满足这两个条件,那么事件就会传递给它来处理。
dispatchTransformedTouchEvent调用的就是子元素的dispatchTouchEvent方法。
如果子元素的dispatchTouchEvent返回true,这时就不需要考虑在子元素里边是怎么分发的,mFirstTouchTarget就会被赋值,同时跳出for循环。
3.View对点击事件的处理
View对点击事件的处理过程简单一些,这里的View不包括ViewGroup。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
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;
}
}
...
return result;
}
首先会判断有没有设置OnTouchListener,如果onTouch方法返回true那么onTouchEvent方法不会被调用。OnTouchListener优先级高级onTouchEvent,这样做的好处是方便在外边处理点击事件。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
}
break;
}
...
reurn true;
}
只要View的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE、TOOLTIP是开启的,就会消耗掉事件。
设置了setOnClickListener会自动设置View的CLICKABLE设为true,
设置了setLongClickListener则会自动将View的CONTEXT_CLICKABLE设为true,
设置了 setOnContextClickListener则会自动将View的CONTEXT_CLICKABLE设为true。
当ACTION_UP事件发生时,会触发performClick方法。perform会调用设置的OnClickListener。