运用的前提是掌握,掌握的前提是理解。只有对事件分发的原理理解了,才能在开发工程中熟练的运用事件分发机制。
1.1、两大基础控件类型
View和ViewGroup。View即普通的控件,没有子布局的,如Button、TextView. ViewGroup继承自View,表示可以有子控件,如Linearlayout、Listview等。
1.2、点击事件
Android中点击事件用MotionEvent类表示,最重要的有3个:
(1)MotionEvent.ACTION_DOWN 按下View,是所有事件的开始
(2)MotionEvent.ACTION_MOVE 滑动事件
(3)MotionEvent.ACTION_UP 与down对应,表示抬起
1.3、两个监听
事件传递机制的最终目的都是为了触发执行View的点击(onClick)监听和触摸(onTouch)监听。
View的事件分发主要涉及两个函数
1)dispatchTouchEvent():将Touch事件传递到目标View或者自己如果自己就是目标View。如果事件被该控件处理了返回true ,否则返回false。
View中dispatchTouchEvent方法将事件传递给自己的onTouch()或onTouchEvent()处理。onTouch()是View提供让用户自己处理Touch事件的接口,而onTouchEvent()是Android系统提供处理Touch事件的接口。onTouch()优先级高于onTouchEvnet()。
2)onTouchEvent():用于处理触摸事件,如果事件被处理了返回true,否则false。
接下来以一个小case来演示View事件分发并以View的源码分析其原理。
如下,在Activity中定义了一个Button按钮,并对它设置了点击监听和Touch监听:并且onTouch监听里默认return false。
mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d(TAG, "onClick execute"); } }); mButton.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d(TAG, "onTouch execute,action " + event.getAction()); return false; } });
当点击Button按钮时,查看log日志如下:
可以看到onTouch()方法优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(如果你手慢可能会有多次ACTION_MOVE的执行)。因此事件传递的顺序是先经过onTouch,再传递到onClick。
当修改onTouch()方法里的返回值为true的时候,再点击Button按钮,你会发现log日志信息如下:
发现onCLick()方法不会执行了,可以理解为onTouch()方法因为返回ture,消费了该点击事件,为了验证解释该现象,接下来会从源码来分析。
1)dispatchTouchEvent()
事件传递的入口是View的dispatchTouchEvent()函数,所以当点击button按钮后,就会去调用Button类里的dispatchTouchEvent方法,可是Button类里并没有这个方法,那么就到它的父类TextView里去找一找,而TextView里也没有这个方法,那没只好继续在TextView的父类View里找,发现View里的dispatchTouchEvent()方法源码如下:
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } // 判断View是否被屏蔽,被屏蔽意思是该View不是位于顶部,有其他View在它之上 // 被屏蔽即返回false,进不了if代码块,不会执行onTouch()和onTouchEvent() if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }
找到这个判断:
if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event))
{return true;}
里面有三个条件,如果mOnTouchListener != null且控件是enable(可用的)且onTouch()三个条件都为真,就直接返回true,否则就去执行onTouchEvent(event)方法,而且只要onTouchEvent()返回true,则dispatchTouchEvent恒返回true。
接下来分别分析下上面的三个条件:
首先看看mOnTouchListener这个变量是在哪里赋值,继续查看View的源码发现如下方法:
public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }
发现mOnTouchListener是在setOnTouchListener方法里赋值的,也就是说只要给控件注册了touch事件,mOnTouchListener就一定被赋值了。接着看第二个条件,判断当前点击的控件是否是enable(可用的)的,而针对本例来说,Button默认都是enable的,因此这个条件为true。第三个条件会回调Button注册touch事件时的onTouch方法。如果onTouch()方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果在onTouch方法里返回false,就会再去执onTouchEvent(event)方法。
那么,接下来来看看View里的onTouchEvent()的源码:
2)onTouchEvent()
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; <strong>// 第一步:判断View是否被禁用即Enable属性是false</strong> // return:如果被禁用则返回是否可点击 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)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } <strong>// 第二步:判断View是否可点击</strong> // desc:if代码块里面涉及到的主要是获取焦点,设置按下状态,触发onClick(), onLongClick()事件等等 // return:如果可点击,执行此步onTouchEvent()恒返回true,则diapatchTouchEvent()也恒返回true // 否则onTouchEvent()返回false,则diapatchTouchEvent()也恒返回false 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) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } 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(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a // scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed // feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback // right away setPressed(true, x, y); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); break; case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
代码比较长,我们看关键代码段,找到如下判断,
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
通过if的两个判断条件,我们知道只要View是可点击(CLICKABLE或者是LONG_CLICKABLE),便能进入该if语句,而且不管当前的action是什么,最终onTouchEvent()都返回true。
接着查看if里面的源码,可知如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。而且在该case语句中最终会执行到performClick()方法,那我们进入到这个方法里看看:
public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); return true; } return false; }
可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法,而mOnClickListener又在View中的setOnClickListener()方法进行赋值,如下:
public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
当我们通过调用setOnClickListener方法来给控件注册点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时,都会在performClick()方法里回调被点击控件的onClick()方法。
现在结合前面的例子来分析一下,首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的。而如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch返回true,onClick就不会再执行了。而如果onTouch()返回false,就会执行view的onTouchEvent()方法,而onClick()会在该方法里执行。
通过对View的源码分析,对View的事件分发用流程图表示如下:
对View事件分发流程图的说明:
1)事件分发是先对ACTION_DOWN进行分发的,如果其的dispatchTouchEvent返回false,后面的action如ACTION_UP将都不会执行。
2)如果ACTION_DOWN分发成功,接下来就是对ACTION_MOVE、ACTION_UP进行分发。
3)在分发过程中只要有一个action的dispatchTouchEvent返回false,后面的action都不会触发了。
现在总结下View事件分发的结论:
1)onTouch()和onTouchEvent()两个方法都是在View的dispatchTouchEvent中调用的。而onClick方法又在onTouchEvent()中调用的。
2)onTouch()优先于onTouchEvent()的执行,且当onTouch()返回true将事件消费掉,onTouchEvent将不会再执行。onTouch()能执行的前提是设置了TouchListener且该该控件是Enable,一般只要不人为修改,绝大部分View默认是Enable。
3)当前控件(或是布局)是否可点击(CLICKABLE或者是LONG_CLICKABLE)直接决定onTouchEvent方法的返回值,从而影响着dispatchTouchEvent方法的返回值。
通过对Touch事件层级传递的分析,能进一步加深对事件分发流程的理解。
我们知道MotionEvent主要包括一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。因为View不像ViewGroup,它是没有子控件的,不存在事件在子控件的传递。因此,对于View来说,事件分发的本质就是对着一系列ACTION进行传递。
通过以上对View源码分析和对View事件分发的总结,接下来我们对ACTION传递进行总结:
默认处理方式下(即按照View源码处理方式),当View被点击时,ACTION_DOWN事件就会通过Activity传递给View的dispatchTouchEvent方法。首先会调用View的dispatchTouchEvent()对ACTION_DOWN进行分发,然后View会调用onTouchEvent()对Touch事件进行处理,如果onTouchEvent方法返回false则将false返回给dispatchTouchEvent方法,此时dispatchTouchEvent()也返回false,则表示View不接受该Touch事件,事件不会继续传递,ACTION_DOWN后面的ACTION_UP等将不会触发。如果onTouchEvent方法返回true则将true返回给dispatchTouchEvent方法,dispatchTouchEvent方法也返回true,则表示View接受了该Touch事件,事件会继续传递,ACTION_DOWN后面的ACTION_UP等会触发。
用一句话总结就是dispatchTouchEvent在进行事件传递的时候,只有当前一个ACTION的dispatchTouchEvent()返回true,才会触发后面的ACTION。
聪明的你肯定发现前面的例子中,在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP也都得到执行,这岂不是和结论相矛盾,仔细分析之前的源码不难发现,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后在onTouchEvent方法中,由于我们点击了按钮,就会进入到if判断,判断是否是CLICKABLE或者是LONG_CLICKABLE,然后你会发现,只要能进入该if代码块,不管当前的action是什么,最终都返回一个true。
为了验证发现ACTION_DOWN后面一系列的action都没有再执行了,接下来我们添加一个ImageView控件,并只给它注册Touch事件返回值为false,代码如下:
mImageView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d(TAG, " ImageView onTouch ,action " + event.getAction()); return false; } });
点击ImageView控件得到log日志如下:
发现在ACTION_DOWN执行完后,后面一系列的action都没有执行。原因就是ImageView和Button不同,它默认是不可点击的,因此在onTouchEvent()的无法进入到第三个if的内部,直接跳到该方法最后一行返回了false,使disPatchTouchEvent()的ACTION_DOWN返回false,导致后面其它的action都无法执行了。
聪明的你又会发现为什么没有给ImageView设置Click事件呢?
public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
当然代码中可能会出现给该控件注册了一个onClick事件,那么就可以通过设置控件的setClickable(false)将控件的Clickable值置为false,如下:从上面代码可知,View的源码中,只要给View设置了ClickListener,就会将View设置成Clickable,那么就一定能进入到onTouchEvent()方法中的第三个if代码块内,导致onTouchEvent()恒返回true,就会使disPatchTouchEvent()的ACTION_DOWN返回true,那么ACTION_DOWN后面一些列的ACTION将会得到执行。
mImageView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d(TAG, "Button onClick execute"); } }); mImageView.setClickable(false);
综上:dispatchTouchEvent则执行ACTION传递时,只有当前一个ACTION的dispatchTouchEvent()返回true,才会触发后面的ACTION。这样你会发现在点击该控件的时候,该控件没有反应。