1.点击事件的传递规则
所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的view,这个过程就是分发的过程。事件的分发过程是由三个方法共同完成的:
DispathTouchEvent,onInterceptTouchEvent和onTouchEvent。
1. public boolean dispatchTouchEvent(MotionEvent ev)
用于进行事件的分发。如果事件能够传递给当前的view,那么此方法一定会被调用。返回结果受当前view的onTouchEvent和子view的DispathTouchEvent方法的影响
2. public boolean onInterceptTouchEvent(MotionEvent ev)
这个方法是在dispatchTouchEvent中调用,用来判断是否拦截某个事件,
如果当前view拦截了某个事件,那么在同一个事件序列当中,onInterceptTouchEvent这个方法不会再被调用,返回结果表示是否拦截当前事件
3. public boolean onTouchEvent(MotionEvent event)
此方法也是在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,
如果不消耗的话,则在同一个事件序列中,当前view无法再次接受到事件
这里我们用一段伪代码表示一下上面3个方法之间的关系
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent()) {// 当前拦截了此事件
consume = onTouchEvent();// 执行此view的onTouchEvent()方法
} else {// 没有拦截
consume = child.dispatchTouchEvent(MotionEvent ev);// 调用子view的dispatchTouchEvent()方法
}
return consume;
}
通过上面的伪代码,我们可以了解一下传递规则:对于一个viewgroup来说,点击事件发生后,首先会传递给dispatchTouchEvent方法,然后判断这个
viewgroup
的onInterceptTouchEvent是否拦截,返回true就表示它拦截了当前的事件,那么这个事件就会交给这个
viewgroup处理,既是它的onTouchEvent会被调用。如果返回false表示不拦截,那么这个事件就会传递给它的子view的dispatchTouchEvent,然后一直到这个事件被最终处理。
当一个view(这里是view)需要处理事件时,如果这个view设置了onTouchListener,那么
onTouchListener的onTouch方法就会被调用,如果onTouch返回false,那么这个view的onTouchEvent方法才会被调用,如果返回true,
onTouchEvent就不会被调用。由此可见,
onTouchListener的优先级比
onTouchEvent要高,然后onClickListener中的onclick方法是在
onTouchEvent中调用,onclick方法的优先级是最低的,处于事件传递的末尾(这个结论在下面分析源码中可以得到)。
当一个点击事件产生后,它的传递顺序是activity -> window -> view,顶级的view接受到事件后,就会按照事件分发机制去分发事件。这里考虑一个情况,如果一个view的onTouchEvent方法返回了false,那么事件会传递到view的父容器的
onTouchEvent将会被调用,然后依次类推。如果所有的元素都不处理这个事件的话,那么最总会传递给activity处理,即activity的
onTouchEvent会被调用
(这个结论在下面分析源码中可以得到)
关于事件传递的机制,这里给出一些结论,这些结论可以在下面的源码分析中得到证实:
1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕那一刻结束,在这个过程中所产生的一系列的事件,这个事件序列已down事件开始,中间含有数量不定的move事件,最终以up事件结束
2. 正常情况下,一个事件序列只能被一个view拦截且消耗,因为一旦一个袁术拦截了某次事件,那么同一个事件序列内所有事件都会直接交给它处理,但是通过特殊手段可以做到,比如一个view将本来应该由自己处理的事件通过onTouchEvent强行传递给其他view处理。3. 某个view一旦决定拦截,那么这一个事件序列都只能由它处理,并且它的onInterceptTouchEvent不会再被访问到,也就是说一旦这个view拦截了一个事件后,那么系统就会把同一个事件序列内的其他方法都直接交给这个view处理了,因此就不会再调用这个view的onInterceptTouchEvent去询问是否还要拦截了
4. 某个view一旦开始处理事件,如果它不消耗action_down这个事件(onTouchEvent 里面返回了false),那么同一事件序列的其它事件都不会交给它来处理,并且会把这个事件交给父元素处理,既是父元素的onTouchEvent会被调用。意思就是,事件一旦交给一个view处理,那么它就必须消耗掉,否则同一事件序列的其他事件就不会交给它处理了。
5. 如果view不消耗action_down以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前view可以持续收到后续的事件,最终这些消失的点击事件都会传递给activity处理。
6. viewgroup默认不拦截任何事件,默认返回false
7. view没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
8. view的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longclickable同时为false)。view的longclickable默认是false,而clickable要分情况,比如button是true,textview是false。9. view的enable属性不影响onTouchEvent的默认返回值。哪怕一个view是disable状态,只要它的clickable或者longclickable有一个为true,那么它的返回值就是true
10. onclick发生的前提是,view是可以点击的,并且它收到了up和down的事件
11.事件的传递是由外向内的,即事件总是先传递给父元素,然后再由父元素分发到子元素,但是可以通过requestDisallowInterceptTouchEvent方法在子元素中干预父元素的事件分发过程,但是在action_down事件除外上面的这些结论可以在下面的源码分析得到证实
2.通过研究源码证实结论
2.1 activity对点击事件的分发过程
当一个点击操作发生时,事件最先传递当前的activity,由activity的DispathTouchEvent来进行事件的分发,具体工作是由activity内部的window来完成的。window会将事件传递给decor view,decor view 一般就是当前界面的底层容器(既是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);
}
首先事件会交给window进行分发,如果返回的是true,那么整个事件循环就结束了,如果返回false就意味着没人处理,那么就会交给activity的onTouchev处理
接下来我们来看,
window是如何把事件传递给viewgroup的。
通过源码我们知道window是一个抽象类,而superDispatchTouchEvent是一个抽象方法,所以我们要找到window的实现类。然而window的实现类是phonewindow,我们查看phonewindow的superDispatchTouchEvent方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
这里的mDecor是一个DecorView
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
我们知道((ViewGroup)(getWindow().getDecorView().findViewById(android.R.id.content))).getChildAt(0);可以获取到setContentView设置的view,然而这个mDecor就是getWindow().getDecorView()返回的view,所以我们setContentView设置view的父容器就是mDecor。DecorView继承至framelayout且是父view,所以最终事件都会传递给view
2.2 顶级view对点击事件的分发过程
这里大致回顾一下事件分发:点击事件到达了顶部的view(一般是一个viewgroup)以后,会调用这个viewgroup的DispathTouchEvent,然后如果这个viewgroup拦截了这个事件(即onInterceptTouchEvent返回true),则事件由viewgroup处理,这时如果viewgroup的onTouchListener被设置了,则onTouch会被调用,那么这时需要看onTouch返回值来判断onTouchEvent是否会被执行。如果viewgroup不拦截这个事件,那么会传递给子view,调用子view的DispathTouchEvent,然后子view的执行和父view是一样的。
我们先看
viewgroup(注意这里是viewgroup)的的dispathTouchEvent方法的一个片段
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要想走到拦截方法onInterceptTouchEvent那里,要经过两个判断,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null),actionMasked == MotionEvent.ACTION_DOWN好理解,这个mFirstTouchTarget != null是什么意思呢?这个可以在dispathTouchEvent后面的代码找到作用,现在来说就是,
当事件被viewgroup拦击了,mFirstTouchTarget为null,当viewgroup不拦截,mFirstTouchTarget不为null。然后当action_move和action_up事件到来时,由于if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,将导致viewg的onInterceptTouchEvent并不会被调用到,并且同一序列的其他事件都会交给它处理的。
当然这里面还有一种特殊情况,那就是
FLAG_DISALLOW_INTERCEPT,这个标记为是通过
requestDisallowInterceptTouchEvent方法来设置的,一般用于子view。这个标记一旦被子view设置了后,viewgroup就无法再拦截除了action_down以外的事件了。为什么除了action_down事件呢,因为如果是action_down事件的话,viewgroup会重置FLAG_DISALLOW_INTERCEPT的值。
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();
}
在resetTouchState中改变了重置了这个值。因此,当面对action_down时,viewgroup总会询问是否拦截这个事件。
我们在这里可以得到上面
第3条的结论:当viewgroup决定拦截事件后,那么系统就会把同一个事件序列内的其他方法都直接交给这个view处理了,就不会再调用这个view的onInterceptTouchEvent去询问是否还要拦截了。还有可以证实
第11条结论:requestDisallowInterceptTouchEvent方法在子元素中干预父元素的事件分发过程,但是在action_down事件除外。
如果当前这个viewgroup拦截了action_down事件,那么mFirstTouchTarget为null,接下来的move和up事件再来传递,if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,就不用再走onInterceptTouchEvent方法了,直接在viewgroup的onTouchEvent中执行了。
FLAG_DISALLOW_INTERCEPT这个标签起作用的前提是,拦截了action_down这个事件
接下来我们看viewgroup不拦截的情况下,事件会向下分发事件,分发给它的ziview
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
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;
}
}
首先for循环遍历了所有的子元素,然后判断子元素是否能接受到点击事件,如果能接受到点击事件,那么这个事件就会传递给它。dispatchTransformedTouchEvent方法实际上就是调用子view的dispatchTouchEvent,dispatchTransformedTouchEvent内部有一段代码:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果child不为null,就调用子view的dispatchTouchEvent方法。如果子view的dispatchTouchEvent返回true的话,mFirstTouchTarget会被赋值(在addTouchTarget中赋值),然后跳出循环。
如果mFirstTouchTarget为null,那么viewgroup就会默认拦截同一序列中所有的点击事件(if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) )。
如果遍历了所有子元素后,事件都没有被处理,那么就分两种情况:第一种没有子元素,第二种子元素处理了点击事件,但是dispatchTouchEvent返回了false,这种情况一般是onTouchEvent返回false,
在这两种情况下,viewgroup会自己处理点击事件,这里证实了
第4条结论
// 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);
}
从上面代码看到,如果子view都不处理调事件,那么mFirstTouchTarget为null,然后dispatchTransformedTouchEvent方法执行,
注意第三个参数传入的是null,如果是null的话,就会转到view的dispatchTouchEvent方法中,点击事件就交给了view处理(
这里是view,不是viewgroup)
2.3 view对点击事件的处理过程
先看一下view的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
.....
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;
}
}
.....
return result;
}
因为view是一个单独的元素,没有子元素,没有onInterceptTouchEvent方法。系统会先判断有没有设置onTouchListener,如果onTouch方法返回true,那么onTouchEvent不会被调用。
然后我们再看onTouchEvent方法,先看一个片段:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
不可用状态下的view照样会消耗点击事件
继续看源码
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress) {
// 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();
}
}
}
...
removeTapCallback();
}
break;
}
...
return true;
}
从代码看出来,只要view的clickable和longclickable中有一个为true,就会消耗此事件,最后会看到return true,代表消耗事件,这里可以证实了
8、9、10结论。然后如果view设置OnClickListener,那么performClick方法内部就会调用它的onclick方法。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
然后通过setOnclickListener方法可以将view的clickable设置为true,textview默认是false,button默认是true
到这里,点击事件的分发机制的源码就分析完了,上面的结论也得到了证实