View的事件分发机制
1.view的点击事件原理
View的点击事件的分发,即对MotionEvent事件的分发,当一个MotionEvent产生后,系统需要把该事件传递到一个具体的View进行处理,该过程即为View事件的分发,在MotionEvent的传递过程中,需要经历三个重要的方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。
dispatchTouchEvent
该方法主要对事件进行分发,当时间传递到当前view时,该方法会被调用。
onInterceptTouchEvent
该方法用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一事件序列中,该方法不会再次被调用。
onTouchEvent
该方法用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则无法再接收后续的事件。
其中三者之间的关系如下所示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
}
即在一次事件分发的过程中,判断当前view(viewGroup)是否拦截,如果拦截,则交给onTouchEvent进行处理,如果不拦截,则交给子view的dispatchTouchEvent处理,至此完成一轮事件的分发。
注:同一事件序列事件是指从MotionEvent.ACTION_DOWN开始+中间经历的ACTION_MOVE到最后ACTION_UP的过程中的所有事件。
2.由源码验证iew的事件原理
ViewGroup对事件的分发
首先查询VIewGroup源码中对事件分发过程,我们发现这样一段代码:
// 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;
}
首先分析if里边的内容
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)
其中的mFirstTouchTarget暂且理解为当前点击的位置的子View,即能接收此点击事件的view。当子元素处理了点击事件后,就会为mFirstTouchTarget进行赋值。
当一个事件到来时,首先判断是否为ACTION_DOWN,如果为ACTION_DOWN那么肯定走if为true时的内容,当不是ACTION_DOWN时,即后续的事件到来时,就会根据第一次处理的结果进行判断,如果第一次即ACTION_DOWN事件被当前view处理了,那么mFirstTouchTarget肯定为null,则直接拦截后续的事件交给onInterceptTouchEvent进行处理,如果ACTION_DOWN事件被子元素处理了,那么mFirstTouchTarget不为null,继续进行后续的判断看是否拦截。
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
}
由disallowIntercept我们可以理解为是否禁止拦截事件,而该变量值有FLAG_DISALLOW_INTERCEPT进行控制,如果不通过FLAG_DISALLOW_INTERCEPT进行干预,那么disallowIntercept为false,然后会调用onInterceptTouchEvent看该事件是否被拦截。如果拦截,那么onInterceptTouchEvent返回true,由三者的关系图我们可以知道,当intercepted为true时,将会调用onTouchEvent对事件进行处理。如果不拦截,则intercepted为false,交给子view的dispatchTouchEvent进行处理。
也就是当ACTION_DOWN到来时,肯定会调用onInterceptTouchEvent进行判断是否拦截,如果拦截,后续的事件都会交给该viewGroup进行处理,不会再经过子元素,如果不拦截,交给子元素处理。
当后续事件到来时,如果第一次ACTION_DOWN进行了拦截,mFirstTouchTarget==null,那么之后的所有事件都会交给当前viewGroup进行处理,不会再分发到子元素。如果第一次ACTION_DOWN事件没有拦截,被子元素进行处理了,mFirstTouchTarget!=null,但是子元素只要不对父viewGroup进行干扰,即将disallowIntercept设置为true,那么这些后续事件还是会经过viewGroup的onInterceptTouchEvent询问是否拦截处理。如果不处理再交给子元素。
disallowIntercept决定了viewGroup是否拦截事件,如果在子元素中将其设置为true,那么后续的所有事件都不会再经过viewGroup中的onInterceptTouchEvent进行判断。在子元素中通过如下方法设置该值。
parent.requestDisallowInterceptTouchEvent(true);
同时由如下代码我们可以知道,当每次ACTION_DOWN到来时,会重置之前的所有状态,即将mFirstTouchTarget置为null。
// 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();
}
View对事件的分发
而view对事件的分发则相对简单一些,其中这里的view不包括viewgroup。
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
view会进行判断看是否设置了OnTouchListener,如果设置了则会返回true,onTouchEvent事件就不会被调用,如果没有设置,才会调用onTouchEvent事件。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
在onTouchEvent中,首先会判断当前view是否可用,如果不可用,也会消耗点击事件,只是看起来没效果而已。
如果可用,则会判断是否可点击,即接收CLICKABLE和LONG_CLICKABLE是否为true。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
//do something
........
if (!post(mPerformClick)) {
performClick();
}
........
}
如上边代码所示,只要CLICKABLE和LONG_CLICKABLE有一个为true,则会消耗这个事件。其中performClick()方法中会调用onClick方法。
由此可见,在事件的优先级中onTouchListener(onTouch)>onTouchEvent>onClick;
view的LONG_CLICKABLE默认为false,而CLICKABLE则跟具体的view有关,例如Button的CLICKABLE默认为true,而TextView的默认为false,即跟该view是否可点击有关,但是我们在对TextView设置点击事件时并没有专门设置该属性,为什么也能响应点击事件呢?
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
如上边所示,在view的源码中,当对view设置点击事件时,会首先判断isClickable,并将其设置为true,因此不用我们再手动进行设置。