1、概述
View的事件分发机制是指点击事件MotionEvent的分发过程,一旦一个MotionEvent产生,必须要传递给一个具体的View去处理,这个分发过程就是View的事件分发机制,与下面三个函数密切相关
public boolean dispatchTouchEvent(MotionEvent ev); //用来分发事件,当前view一定会调用,返回结果受当前view的onTouchEvent和下级view的dispatchTouchEvent影响
public boolean onInterceptTouchEvent(MotionEvent ev); // 判断是否拦截某个事件,返回结果表示是否拦截,如果当前view拦截了某个事件,那么同一个事件序列中,此方法不会被调用,即只调用一次
public boolean onTouchEvent(MotionEvent ev); // 处理点击事件,返回结果表示是都消耗此事件,如果不消耗,则在同一个事件序列中,当前view无法再接受到事件
三个方法之间可以表示为
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
}
一个点击事件首先会传递给一个ViewGroup,它的dispatchTouchEvent会调用,如果这个ViewGroup拦截此事件,那么它的onTouchEvent会调用处理点击事件,否则事件传递给它的子View,调用子Viewd的dispatchTouchEvent,如此反复,直到事件被最终处理。
2、(原理)源码分析
(1)、Activity事件的分发过程
点击事件产生后,首先会传给Activity,调用Activity.dispatchTouchEvent,里面调用了Window.superDispatchTouchEvent,如果返回了true,整个事件循环就结束了,如果返回false,表示没有View消耗该事件,Activity的onTouchEvent就会被调用,Window是一个抽象类,它的唯一子类是android.policy.PhoneWindow,Window可以控制顶级View的外观和行为策略,
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public boolean superDispatchTouchEvent(MotionEvent ev) {
return mDecor.superDispatchTouchEvent(ev);
}
PhoneWindow将MotionEvent传递给了DecorView,它是一个FrameLayout(ViewGroup),setContentView设置它的一个子View
(2)、DecorView(ViewGroup)对点击事件的分发过程
看一下ViewGroup.dispatchTouchEvent,当action==DOWN事件时,会将mFirstTouchTarget设置为null,标记位FLAG_DISALLOW_ONTERCEPT被重置,从代码中知道,两种情况下ViewGroup会判断是否拦截事件:事件action==DOWN和mFirstTouchTarget!=null,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会指向该子元素,即如果ViewGroup拦截当前事件时,mFirstTouchTarget==null,当MOVE和UP事件到来时,onInterceptTouchEvent不再被调用
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
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();
}
// Check for interception.
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;
}
一种特殊情况是通过requestDisallowInterceptTouchEvent设置标记位FLAG_DISALLOW_ONTERCEPT,这时除了DOWN事件外,所有的事件到来时都不再调用onInterceptTouchEvent,DOWN事件会重置标记位FLAG_DISALLOW_ONTERCEPT
当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理,并不会调用onInterceptTouchEvent,FLAG_DISALLOW_ONTERCEPT的作用是让ViewGroup不再拦截事件,前提是不拦截ACTION_DOWN事件,onInterceptTouchEvent不是每次都被调用,如果想提前处理所有的点击事件,要选择dispatchTouchEvent,只有这个方法每次都会被调用
所以有以下结论:
ACTION_DOWN事件到来时,ViewGroup一定会调用onInterceptTouchEvent判断是都拦截事件
ACTION_MOVE和ACTION_UP事件到来时,如果mFirstTouchTarget!=null(ViewGroup决定拦截事件),ViewGroup会调用onInterceptTouchEvent判断是都拦截事件
特殊情况是子View调用了requestDisallowInterceptTouchEvent,这个时候,除了DOWN事件,ViewGroup均不拦截,不再调用onInterceptTouchEvent
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
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();
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 (!canViewReceivePointerEvents(child)
|| !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)) { // 调用子元素的dispatchTouchEvent,如果返回false,mFirstTouchTarget被赋值跳出循环
// 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); //mFirstTouchTarget被赋值
alreadyDispatchedToNewTouchTarget = true;
break;
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) { // dispatchTransformedTouchEvent返回了false或没有子元素,ViewGroup自己处理点击事件
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null, // 内部调用了super.dispatchTouchEvent(ev),开始交给view处理
TouchTarget.ALL_POINTER_IDS);
然后遍历所有的子元素,判断子元素是否能接收点击事件,事件交给满足子元素是否播放动画和点击事件是否落在子元素区域内
这两个条件的子元素去处理,调用子元素的dispatchTouchEvent,如果返回true,跳出for循环,赋值mFirstTouchTarget,mFirstTouchTarget是否赋值直接影响ViewGroup的拦截策略
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
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(); return handled;
如果遍历所有的子元素没有子元素,或则子元素在dispatchTouchEvent中返回了false,一般是子元素在onTouchEvent中返回了false,这两种情况下ViewGroup会自己处理点击事件
(3)、View对点击事件的处理
因为View(不包括ViewGroup)是一个单一的元素,因此无法再向下传递事假,只能自己处理,View没有onInterceptTouchEvent方法,从源码中可以看出,OnTouchListener.onTouch优先级最高,如果返回true,onTouchEvent不会被调用,否则调用onTouchEvent,在onTouchEvent,只要View的属性CLICKABLE、LONG_CLICKABLE有一个为true,那么它就消耗事件,即返回true,不管它是不是DISABLE状态,setLongClickListener和setClickListener都会设置属性为true
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;
}
}
一些结论:
1.同一个事件序列:ACTION_DOWN-MOVE。。。。。MOVE-UP
2.正常情况下,一个事件序列只能被一个View拦截消耗
3.一旦一个View决定拦截,那么这个事件序列只能由它处理,并且他的onIntercept不会再被调用
4.某个View一旦开始处理事件,但是不消耗DOWN事件,那么同一个事件序列中的其他事件都不再交给它处理,重新交给它的父元素去处理,即父元素的onTouchEvent会被调用,
4.如果VIEW不消耗DOWN以外的其他事件,父元素的onTouchEvent不会被调用,并且当前View还可以收到后续事件,最终这些事件会传递给Activity处理
6.ViewGroup默认不拦截任何事件,ViewGroup源码中onInteceptTouchEvent默认返回false
7.View没有onInteceptTouchEvent方法,一旦传递给它, 他的onTouch或者onTouchEvent会被调用
8.View的onTouchEvent默认会消耗事件,除非它是不可点击的,enable属性不会影响onTouchEvent默认返回值