在了解view的时间分发机制之前,我们先了解MotionEvent这个对象
MotionEvent
在手指接触屏幕后所产生的一系列事件中,典型的时间类型有如下几种:
上述三种情况是典型的事件序列,同时通过MotionEvent对象我们可以得到点击事件发生的x和y坐标,为此系统提供了两组方法,getX/getY和getRawX/getRawY。它们的区别其实很简单,getX/getY返回的是相对于当前view左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
所谓的点击事件的事件分发,其实即使对MotionEvent事件的分发过程,即当一个MotionEvent产生以后,系统需要把这个事件传递给一个具体的view,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来完成:
public boolean dispatchTouchEvent(MotionEvent ev) :用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会被调用,返回结果受当前view的ontouchEvent和下级view的dispatchTouchEvent方法的影响,表示是否消耗当前事件
public boolean onInterceptTouchEvent(MotionEvent ev):在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
上述三个方法的区别用下面的伪代码表示:
/**
* 点击事件产生后
*/
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否会消费事件
// 步骤2:判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
// a. 若拦截,则将该事件交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不拦截,则将该事件传递到下层
// 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
// 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
return consume;
}
当一个view需要处理事件时,如果它设置了onTouchListener,那么OnTouchListener中的onTouch方法会被回调,这是事件如何处理还要看onTouch的返回值,如果返回flase,则当前view的OnTouchEvent方法会被调用,如果返回true,那么此方法就不会被调用。由此可见,给view设置的OnTouchListener,其优先级比ontouchevent要高。根据源码来看,我们平时用的onClickListener,其优先级最低。
当一个点击事件产生后,它的传递顺序过程遵循如下顺序:
Activity-----》Window----》View
即事件总是先传递给activity,activity再传给window,最后window在传给顶级的view,顶级的view接收到事件后,就会按照时间分发机制去分发事件,及先后调用上面三个方法。考虑一种情况,如果一个view的onTouchEvent返回false,那么他的父容器的OnTouchEvent将会调用,以此类推。也就是说 如果各个层级都不消耗事件的话,那么从顶级view开始分发后,一直到最上面的view,这是分发,然后经过最上面view的ontouchevent方法去处理此事件,如果ontouchevent不去处理的话,就返回到他的上层的ontouchevent方法去处理,其实就是一个U型。大家慢慢去理解。
当一个点击操作发生时,事件最先传递给当前的activity,由activity的dispatchTouchEvent来进行事件的派发,具体的工作有activity内部的window来完成的。window会将事件传递给decorview,decorview一般就是当前界面的底层容器(即setcontentview 所设置的view的父容器),通过activity。getwindow。getdecorview()可以获得,接下来看activity的dispatchtouchevent。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
首先事件开始交给activity所附属的window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有view的ontouchevent都返回了false,那么activity的ontouchevent就会被调用。其实window分发最后是到了顶层的view,中间过程就不详细说了。
点击事件达到顶级view(一般是一个viewGroup)以后,会调用viewgroup的diapatchtouchevent方法,如果viewGroup拦截事件即onInterceptTouchEvent返回true,则事件由viewGroup处理,这是如果viewGroup的ontouchlistener被设置了,则onTouch会被调用,如果onTouch返回true,就会屏蔽掉onTouchEvent,如果返回false,会接着执行OnTouchEvent方法,好了 下面我们看一下dispatchtouchevent方法的源码:
// 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;
}
ViewGroup在两种情况下都会判断是否要拦截当前事件
然后接着看viewgroup遍历子view:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
final View[] children = mChildren;
//遍历所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断子View是否能接收点击事件
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);
//事件传递到子View,下面追踪该方法
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);
}
......
}
ViewGroup直接使用for遍历所有子View,对子View的各种状态进行判断,最后调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)将事件传递给子View,下面是dispatchTransformedTouchEvent()方法的部分源码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
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的dispatchTouchEvent()方法,在这里,我有一些不明白的地方,但是经过demo测试,如果嵌套了好几个viewgroup类型的view,那么就会多次执行viewgroup的dispatchTouchEvent()方法,如果最上层是一个view的话,比如textivew,那么就会走view的dispatchTouchEvent(),那么接下里就会进入view的事件分发,我自己感觉,也就是说,如果说嵌套的布局里面没有view(类似textview这一类),那么就不会走view的dispatchTouchEvent,只能走viewgroup的dispatchTouchEvent,不知道这样理解对不对,希望看到的大佬给个建议
view的dispatchTouchEvent源码分析
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;
}
从源码判断处看出,首先会判断有没有设置mOnTouchListener,如果mOnTouchListener不为空,那么onTouchEvent就不会被调用,这里可以得到一个结论,若在View中设置了OnTouchListener,那么它的优先级是高于onTouchEvent的,这样可以更好的让我们自己setOnTouchEventListener()处理点击事件
onTouchEvent源码处理事件的具体做法部分
public boolean onTouchEvent(MotionEvent event) {
......
//当View处于不可用状态下,也会消耗点击事件
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);
}
......
//对点击事件的具体处理
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
......
}
}
return true;
}
......
}
从对点击事件的具体处理中看出,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true。在ACTION_UP事件中,会触发PerformClick()方法,如果View设置了OnClickListener,那么PerformClick()方法内部会调用它的onClick()方法