1、点击事件的传递规则

点击事件,即要分析的对象是MotionEvent,所以,点击事件的分发,实际是MotionEvent 的分发过程,主要由dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。

  • public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发,如果事件能够传递到当前的View,则调用此方法,返回结果受当前View的onTouchEvent和下一级的dispatchTouchEvent方法的影响,表示是否消耗了此事件。
  • public boolean onInterceptTouchEvent(MotionEvent ev)
    在上述方法内部调用,用来判断是否拦截了某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,放回结果表示是否拦截了此事件。
  • public boolean onTouchEvent(MotionEvent ev)
    再dispatchTouchEvent中调用,用来处理点击事件返回结果表示是否消耗了当前事件,如果不消耗,则在同一事件序列中,当前view无法再次接收此事件。
    上面三个方法的关系,区别,可以用以下伪代码表示:
	public boolean dispatchTouchEvent(MotionEvent ev){
		boolean consume = false;
		if(onInterceptTouchEvent(ev)){
			consume = onTouchEvent();
		}else{
				consume = child.dispatchTouchEvent(ev);
			}
		return consume;
	}

传递规则:对一个根ViewGroup来说,点击事件产生后,首先会传给它,它会调用dispatchTouchEvent,如果这个ViewGroup的onIntercepteTouchEvent方法返回true,表示他要拦截当前事件,事件就会交给zhegeViewGroup处理,即它的onTouchEvent会调用;返回false,表示它不拦截当前事件,这个事件就会继续传递给它的子View接着子View的dispatchTouchEvent会被调用,如此反复直到事件被处理。
当一个View需要处理一个事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被调用,如果onTouch的返回值为true:onTouchEvent将不会被调用;如果返回false,则当前View的onTouch方法会被调用。由此可见onTouchListener的优先级要比onTouchEvent高,在onTouchEvent中如果设置onClickListener,那么onClick会被调用,可见onClickListener的优先级最低。
当一个点击事件产生,它的传递顺序为:Activity->Window->View,View接收到事件之后,再按照View的分发机制去分发事件。当然如果所有的View都不处理该事件,则该事件将再次”原路返回”,判断上一次是否会处理,直到分发给Activity,即Activity的onTouchEvent会被调用。
得出结论如下:

  1. 同一事件序列,是指从手指触摸屏幕,即down开始,中间含数量不定的move事件,最终以手指离开屏幕,即up事件结束。

  2. 正常情况下,一个事件只能被一个View拦截并消耗,一次同时,该序列内的所有事件都会交给它处理,因此同一事件序列中的事件不能分别由两个View同时处理。除非View将本该自己处理的事件通过onTouchEvent强行的传递给其它View处理。

  3. 某个View一旦决定拦截该事件,那该事件序列都能由它完成,并且它的onInterceptTouchEvent不会再被调用。

  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEven返回false),那么同一序列的其它事件也都不会交给它处理,该事件会交由它的父元素处理,即父元素onTouchEvent会被调用。

  5. 如果View不消除ACTION_DOWN以外的事件,那么这个点击事件会小消失,同时父元素的onTouchEvent也不会被调用,并且当前View会持续接收后续的事件,最终这些消失的点击事件都见传递给Activity处理。

  6. ViewGrop默认不拦截任何事件,源码中ViewGrop的onInterceptTouchEvent方法默认返回false。
    在这里插入图片描述

  7. View没有onInterceptTouchEvent方法,一旦有点击事件传给它,直接回被调用onTouchEvent方法。

  8. View的onTouchEvent默认都是回消耗事件(返回true)除非时不可点击的(clickable和longClicktable同时为false)。View的longClickable属性默认为false,clickable属性分两种,如Button的clickable默认为true,textViewde clickable属性默认为false。

  9. View的enable属性不受影响,onTouchEvent的默认值。哪怕View是disable状态,只要它的clickable或者longclickable有一个为true,那么它的onTouchEvent就返回true。

  10. onClick会发生的前提是当前的View是可点击的,并且接收了down和up的事件

  11. 事件传递是由外向内的,即事件是由父元素传递给子元素的,虽然通过requestDisallowInterceptTouchEvent方法可以再子元素中干预父元素的事件分发,过程,但是ACTION_DOWN事件除外。

2、事件分发源码分析

1.Activity对事件的分发过程
点击事件用MotionEvent表示,事件先传递给Activity,由它的dispatchTouchEvent进行事件分发,具体由Window来完成Window会将事件传递给decor view(当前界面底层容器,即setContextView所设置的View的父容器)。先分析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就会被调用。
**2.接下来看事件是怎么传递给Group View的
** Window的实现类是phoneWindow类,所以再看PhoneWindow如何处理事件:

public boolean superDispatchTouchEvent(MotionEvent event){
	return mDecor.superDispathTouchEvent(event);
}

可见,事件又被传到DecorView:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
private DecorView mDecor;
@override
public final View getDecorView(){
	if(mDecor == null){
		installDecor();
	}
	return mDecor;
}

通过()(ViewGroup)getWindow().getDecorview.findViewById(android.R.id.context)).getChildAt(0)获取Activity设置的View,mDecor为getWindow().getDecorView()返回的View。通过setContentView设置View是它的一个子View。所以时间首首先会传递给DecroView,由于DecorView继承自FrameLayout,且为父View,所以事件最终会传递给View。
3.顶级View对点击事件的分发过程
分发过程:首先一个事件到顶级的View(ViewGroup),会调用ViewGroup的dispatchTouchEvent,如果拦截,ViewGroup的onInterceptTouchEvent返回true,则事件有ViewGroup处理,具体会根据事件处理的优先级,消耗事件。返回false,则事件将传递给下一级View,一次类推。
3.1.ViewGroup对事件的分发

首先看ViewGroup的dispatchTouchEvent方法

final boolean intercepted;
if(actionMaxked == MotionEvetn.ACTION_DOWN ||mFirstTouchTarget != null){
	final boolean disallowIntercepte == (mGropFlags&FLAG_DISALLOW_INTERCEPTE)!=0;
	if(!disallowIntrcept){
		intercepted = onInterceptTouchEvent(ev);
		ev.setAction(action);
	}else{
		intercepted = false;
	}
}else{
	intercepted = true;
}

可见ViewGroup在ACTION_DOWN或者mFirstTouchTarget != null,其中mFirstTouchTarget != null,即,当事件由VIewGroup子控件处理时,mFirstTouchTarget会被指向子元素。
当然还有一种特殊情况:通过requestDisallowInterceptTouchEvent来设置的FLAG_DISALLOW_INTERCEPT标记位,它设置后ViewGroup无法拦截除了ACTIOND_DOWN以外的点击事件。为什么是ACTION_DOWN以外?是因为ViewGroup接收到ACTION_DOWN时,会重置次标记位,导致子View设置无效。ViewGroup对待ACTION_DOWN事件,会调用自己的onInterceptTouchEvent方法来拦截。
在这里插入图片描述上面代码可以看出,当ViewGroup决定拦截事件后,后续的事件都将默认交给它处理,并且不会再调用onInterceptTouchEvent…
参考价值有两点:
第一、onInterceptTouchEvent 不是每次都会调用,如果我们需要在前一节点处理事务,要选择dispatchTouchEvent方法,但必须续保事件可以传到ViewGroup中。
第二、FLAG_DISALLOW_INTERCEPT标记位,可以帮我们解决滑动冲突的问题。
3.2ViewGroup对事件不拦截,事件如何向下分发

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)) {
                                // 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);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

上述代码可见,首先遍历ViewGroup的每个子View,判断子view是否得到了点击事件。判断标准:1.点击事件的坐标是否落到了子元素的区域内;2.子元素是否在播动画如果满足这两个条件,则交由子元素处理。而dispatchTransformedTouchEvent实际调用了子元素的dispatchTouchEvent,这样就把事件传递给子View了,也将完成了一次循环。

if(child == null){
	handled = super.dispatchTouchEvent(event);
}else{
	handled == child.dispatchEvent(event);
}

如果子元素的dispatchTouchEvent返回true,暂不考虑子元素内部怎么分发,mFirstTouchTarget就会被赋值,同时跳出for循环。

newTouchTarget = addTouchTarget(child,idBitsToAssign);
 break;

addTouchTarget()方法如下:

private TouchTarget addTouchTarget(View child,int pointerIdBits){
	TouchTarget target = TouchTarget.obtain(child,pointerIdBits);
	target.nex = mFirstTouchTarget;
	return target;
}

当遍历完所有的子元素都没有适合的处理,包含两种情况:
第一、GroupView没有子元素
第二、子元素处理了点击事件,但dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。

//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_IN);
}

注意这里第三个参数,child为null,从前面分析可知,它会调用super.dispatuchEvent(event),显然,这里转到了View的dispatchTouchEvent方法,即点击事件交由View来处理。
4.View对点击事件的处理
View 对事件的处理,相对简单,这里值得View不包含ViewGroup,先看dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event){
	boolean result = false;
	...
	if(onFilterTouchEventForSeecurity(event)){
		//noinspection SimplifiableIfStatement
		ListenerInfo li = mListenerInfo;
		if(li != null && li.mOnTouchListener !=nulll
			&&(mVieFlags&ENABLED_MASK)==ENABLED
			&& li.MOntouchListener.onTouch(this,event)){
			result = true;
		}
		if(!result &&onTouchEvent(event)){
			result = true
		}
	}
	...
	return result;
}

可以看出,View处理点击事件,首先会判断有无OnTouchListener.,如果OnTouchListener中得onTouch返回true,那就onTouchEvent将不会被调用,可见OnTouchListener 得优先级高于onTouchEvent,这样得好处在于在外界方便处理点击事件。
然后看onTouchEvent的实现,当View不可用状态下,显然View会消耗点击事件:

在这里插入图片描述
接着,如果View设置了代理,还会执行TouchDelegate的onTouchEvent方法,onTouchEvent类似于OnTouchListener。

if(mTouchDelegate != null){
	if(mTouchDelegate.onTouchEvent(event)){
		return true;	
	}
}

再看onTouchEvent中对点击事件的处理:

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();
				if(!focusTaken){
					if(mPerformClick == null){
						mPerformClick = new PerfomrClick();
					}
					if(!post(mPerformClick)){
						performClick();
					}
				}
			}
			...
		}	
		break;
	}
	...
	return true;
}

可见,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,它就会消耗这个事件即onTouchEvent返回true,不管它是不是DISABLE状态。然后当ACTION_UP触发时,就会调用perform Click方法,如果View设置了OnClickListener,那么performClick方法就会调用onClick方法,如下:

public boolean performClick(){
	final boolean result;
	final ListernerInfo li = mListenerInfo;
	if(li != null && li.mOnClickListener != null){
		playSoundEffect(SoundEffectConstants.CLICK);
		li.mOnClickListener.onClick(this);
		result = true;
	}else{
		resule = false;
	}
	sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
	return result;
}

View 的LONG_CLICKABLE属性默认为false,而CIICKABLE 属性的状态,和View有关,可点击的View的CLICKABLE为true,不可点击的为false,比如Button是可点击的,TextView是不可点击的。课可以通过setClickable和setLongClickable设置相对应的属性,另外,setOnClickListener会自动将View的CLICKABLE设为true,LONG_CLICKABLE属性类似。如下:

public void setOnclickListener(onClickListener l){
	if(!isClickable()){
		setClickable(true);
	}
	getListenerInfo().mOnClickListener = 1;
}
public void setOnLongClickListener(onLongClickListener l){
	if(! isLongClickable()){
		setLongClickable(true);
	}
	getListenerInfo().mOnLongClickListener = 1;
}

特别声明:内容总结来源《Android开发艺术探索》,仅记录学习,如有侵权或不对之处,还请告知,定当删除或改正


版权声明:本文为qq_34629725原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_34629725/article/details/123276653