上一篇我们主要主要是从ViewGroup分发的角度测试了下事件分发机制,但没有涉足多少View的事件分发,也就是说我们没有为MyRelativeLayout、MyLinearLayout、以及MyButton设置Touch和Click监听事件,这一篇将来测试下View的事件分发过程,为了比较简洁的显示打印信息,我简化了布局文件,具体的布局文件代码如下:
<com.hzw.eventtest.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/myRelativeLayout" android:layout_width="match_parent" android:layout_height="match_parent" > <com.hzw.eventtest.MyButton android:id="@+id/myButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="我的按钮" /> </com.hzw.eventtest.MyRelativeLayout>
也即布局文件图是酱紫的:
具体的测试代码就是在MainActivity和MyButton中的dispatchTouchEvent以及onTouchEvent方法中打印Log,以及在MyRelativeLayout的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent中打印Log,并且为MyRelativeLayout、MyButton设置了onTouchListener、onLongClickListener、以及onClickListener事件监听器;
我们点击MyButton按钮,查看Logcat输出结果如下:
06-30 10:27:37.127: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN 06-30 10:27:37.127: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN 06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN 06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN--->false 06-30 10:27:37.132: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN 06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouch--->DOWN 06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN 06-30 10:27:37.144: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.144: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.144: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.144: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.689: I/System.out(2705): MyButton--->onLongClick 06-30 10:27:37.832: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP--->false 06-30 10:27:37.832: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouch--->UP 06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP 06-30 10:27:37.842: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP--->true 06-30 10:27:37.842: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP--->true 06-30 10:27:37.842: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP--->true 06-30 10:27:37.842: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP--->true 06-30 10:27:37.852: I/System.out(2705): MyButton--->OnClick
如果你仔细查看输出的话,有一部分会让你觉得很奇怪的,就是输出的第12行的onLongClick方法和第24行的onClick方法是在dispatchTouchEvent方法执行结束之后才开始执行的,这一点让我感到很诧异,所以专门写了这篇博客来试着从代码层面解释下这种现象的原因,因为网上看别人的分析过程均没有涉足到我想要的部分,所以打算自己分析一次View分发过程的源码,有什么错误还请指正,源码分析结束之后我们再来看看Logcat输出或许你会明白点了;
一个事件传递到View上面首先执行的就是他的dispatchTouchEvent方法,那么很自然首先应该从View的dispatchTouchEvent开始分析:
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement 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; }
第6行判断是否过滤掉当前事件系列,什么情况下会被过滤呢?View在被遮盖的时候,onFilterTouchEventForSecurity方法会返回true,进而直接在第22行dispatchTouchEvent返回了false;如果当前View没有被遮盖的话,执行7--16行的if语句块,首先获取到ListenerInfo对象,他是View的静态内部类,这个对象主要存储的就是一些我们所设置的事件监听器了,稍微看看里面的几个属性字段:
static class ListenerInfo { public OnClickListener mOnClickListener; protected OnLongClickListener mOnLongClickListener; private OnTouchListener mOnTouchListener; }
接着走到第9行的if判断语句处,这个地方有四个判断条件,第1个li指的就是ListenerInfo对象,第2个li.mOnTouchListener其实是在判断是否设置Touch事件监听器,具体li.mOnTouchListener的值等于什么呢?从下面代码中可以看出来:
public void setOnTouchListener(OnTouchListener l) { getListenerInfo().mOnTouchListener = l; }
这是一个public类型的方法,我们通常在程序中为某个控件设置Touch监听器就是调用的这个方法,那么其实第2个判断条件是在查看我们是否有设置Touch事件监听器,第三个条件是在查看我们当前的View是否是enable的,也就是说当前View本身是否能够接受触摸事件,第4个就是onTouch方法的返回值了,这个方法可以被重写,默认情况下是返回false的;如果这个if判断的四个条件都满足的话,执行11行,直接返回,也就是当前事件已经分发结束了,从这里可以看出View事件分发首先执行的是onTouch(当然你必须设置Touch事件监听器);如果if的四个条件中有一个是false,就会执行第14行的if语句,调用onTouchEvent来处理事件,这里我们有必要来看看onTouchEvent方法了;
该方法是public修饰的,所以你可以在子类中重写它,方法比较长,我们截段分析:
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语句块,第2行判断当前事件是UP并且设置了PFLAG_PRESSED标志的话,则调用setPressed将标志置位,因为整个事件的最后一步就是UP了,所以我们必须在事件结束之前将设置的标志还原;
public void setPressed(boolean pressed) { final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED); if (pressed) { mPrivateFlags |= PFLAG_PRESSED; } else { mPrivateFlags &= ~PFLAG_PRESSED; } if (needsRefresh) { refreshDrawableState(); } dispatchSetPressed(pressed); }
这里传给setPressed的参数是false,所以执行7行代码取反还原;回到onTouchEvent方法中,第7行查看View是否有设置点击和长点击,有的话返回true,没有返回false,从这里可以看出onTouchEvent的返回值是跟你View是enable还是disable没有多大关系,只要你设置了clickable或者longClickable,那么他就会返回true;
接着分析onTouchEvent下面代码:
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
查看是否有设置事件代理,有的话,则将事件交给代理处理,根据代理事件onTouchEvent方法来判断是否返回true;
接下来的onTouchEvent代码比较长,我们先来整理一个大体框架:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { ............... ............... ............... return true; } return false;
可以看到只要clickable和longClickable有一个被设置就会返回true,只有在两者都没设置的情况下才会返回false,这也更加印证了onTouchEvent方法的返回值只和你有没有设置clickable和longClickable有关,和View的enable和disable没什么关系;
如果clickable和longClickable有一个被设置,那么进入if语句块中,该语句块是一个switch语句,我们按照事件的触发顺序来进行分析,即DOWN--->MOVE--->UP:
先来看DOWN部分:
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(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true); checkForLongClick(0); } break;
刚进来首先设置mHasPerformedLongPress的值为false,这个比较关键了,用来表示是否有执行长点击事件,如果有设置长点击事件并且onLongClick方法返回true的话,这个值是会被改变成true的,等会你就看到什么原因啦,接着第9行判断当前View是否在正在滚动的控件中,在的话就满足第13行的if条件语句调用postDelay方法来延期press的反馈,为什么要这么做呢?从第11行的注释看出来是为了防止当前事件是一个滚动事件,进入if语句块之后执行第14行,设置PREPRESSED标志,这个标志表示的是prepressed状态,这个状态存在于ACTION_DOWN和真正意识到是press之间,用于识别是不是tap事件,接着第18行执行了postDelayed方法,参数ViewConfiguration.getTapTimeout()的值是150ms,这个方法的代码如下:
public boolean postDelayed(Runnable action, long delayMillis) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.postDelayed(action, delayMillis); } // Assume that post will succeed later ViewRootImpl.getRunQueue().postDelayed(action, delayMillis); return true; }
可以看到这个方法其实是调用了handler的postDelayed方法,将action加入到了MessageQueue消息队列中,熟悉handler机制的应该知道随后调用的将是action的run方法了,也就是mPendingCheckForTap的run方法了,mPendingCheckForTap是CheckForTap类型的对象,具体定义如下:
private final class CheckForTap implements Runnable { public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; setPressed(true); checkForLongClick(ViewConfiguration.getTapTimeout()); } }
run方法首先是将PREPRESSED标志置位,接着执行setPressed方法,设置PRESSED标志,这个方法在前面又出现过,只不过前面调用的是setPressed(false)而已;接着便调用checkForLongClick来查看是否有长点击事件了,传入的参数是150ms;如果当前View不在滚动的控件中的话,则直接执行第19行的else语句,接着调用setPressed以及checkForLongClick方法,这里执行的内容就和CheckForTap的run方法一致了,只不过传入的checkForLongClick参数值不同而已,那么我们就该看看checkForLongClick方法了:
private void checkForLongClick(int delayOffset) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }
该方法第2行判断你有没有设置longClick事件,有的话进入if语句块,首先还是将mHasPerformedLongPress设置为false,接着第9行同样调用了postDelayed方法,传入的第二个参数是ViewConfiguration.getLongPressTimeout() - delayOffset,ViewConfiguration.getLongPressTimeout()的默认值是500ms,从这句话我们可以看出来不管你是通过checkForLongClick(0)还是checkForLongClick(delayOffset)其中delayOffset大于0,调用checkForLongClick方法,其实检测你是不是长点击的时间是一致的,都是500ms,你点击的时间超过500ms的话,会认为是长点击,很自然调用的是mPendingCheckForLongPress的run方法:
public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } }
注意到这个方法第2行会判断isPressed(),什么意思呢?就是说如果你500ms之后还是处于点击状态,那么你就是长点击了,执行if语句块中的内容,第4行执行的是performLongClick方法,而这个方法就主要是执行的我们的OnLongClickListener监听方法了,来看看里面的代码:
public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { handled = showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
同样OnLongClickListener存储在ListenerInfo对象里面,第7行执行了onLongClick方法,并且获取到返回值,回到前面的run方法第4行会判断这个返回值是true的话,执行mHasPerformedLongPress = true语句,我们有必要说明下mHasPerformedLongPress
的作用,他是用于表示你的onLongClick是否返回true的,如果没有设置LongClick监听事件或者设置了LongClick监听事件但是onLongClick方法返回false,那么mHasPerformedLongPress的值将是false,随后在UP事件判断中才会执行接下来的click点击事件,如果设置了LongClick监听事件并且onLongClick方法返回true,那么在随后的UP事件判断中将不再会执行click点击事件,从这里我们可以看出其实onLongClick方法是优先于onClick执行的,这也就解释了我们平常使用onLongClick方法有返回值而onClick方法没有返回值的问题了;这样的话DOWN事件处理结束了;
从DOWN事件的处理中,我们可以知道longclick是在它里面进行检测的,并且如果500ms之后还处于press状态的话会调用它的performLongClick,而这个方法是在子线程中调用的,所以就出现了我们上面Log输出第12行在dispatchTouchEvent返回之后才执行的结果;
接下来分析的是MOVE事件:
case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // 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;
MOVE相对来说比较简单,首先是获取你当前触摸处的位置,接着第6行判断你触摸的地方是否处于当前View的边界内,不处于的话会执行7--15行代码,处于的话不做任何事,我们来看看不处于情况下做了些什么,首先执行第8行的removeTapCallback方法,这个方法:
private void removeTapCallback() { if (mPendingCheckForTap != null) { mPrivateFlags &= ~PFLAG_PREPRESSED; removeCallbacks(mPendingCheckForTap); } }
主要是置位PREPRESSED标志,并且从当前的MessageQueue消息队列中移出封装有mPendingCheckForTap这个线程的消息,为什么要这么做呢?因为你都已经不在我当前View的控制范围内了,我也没必要看你接下来的一些操作了;第9行如果我们设置了PRESSED标志的话,说明在DOWN事件中也在MessageQueue里面添加了封装有监听长点击事件的Message,那么就需要调用第11行的removeLongPressCallback方法,将该Message从MessageQueue中移出,并且13行调用setPressed方法将PRESSED标志置位;
接下来就是UP事件了:
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); } 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;
UP事件代码相对来说比较多,第3行的判断条件说明只要你在MOVE的过程中没有移出边界都会满足,接着第19行会判断mHasPerformedLongPress的值,这个值只有在你设置了longclick监听事件,并且在onLongClick方法中返回true的情况下才会是true,否则均是false,这个在上面已经说过了,我们假定这里mHasPerformedLongPress的值是false,进入if语句块,首先调用removeLongPressCallback,从MessageQueue中移出长点击监听Message,接着第28--33行的代码比较关键,这里将是解释我们上面Log输出的重要部分,如果没有PerformClick对象则创建,并在第31行通过post方法将PerformClick对象添加到MessageQueue消息队列中,接下来将是执行PerformClick的run方法了:
private final class PerformClick implements Runnable { public void run() { performClick(); } }
很明显PerformClick是一个线程,在他的run方法里面也会执行performClick,也就是说不管第31行post方法有没有执行成功都会执行performClick方法的,那么这里为什么要用到post通过子线程来执行performClick而不是直接执行performClick呢?根据官方的注释看到这样做的目的是为了在click执行之前让view上面的其他visual
状态能够更新,来看看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; }
可以看到如果我们设置了OnClickListener监听器的话,会执行第7行代码,也就执行了我们的onclick方法了;因为如果我们调用post的话,performClick是执行在PerformClick类型的子线程中的,所以我们上面的Log输出会出现onClick方法在dispatchTouchEvent事件返回之后才执行的情况了;UP事件后面的一些操作是用于状态置位的,我们再次不做过多牵涉;
这样的话,View的事件分发源码分析完毕了,我们做个小结以此来解释上面的Log输出:
(1)View中如果我们设置了onTouchListener、onLongClickListener以及onClickListener的话,三者的执行顺序是onTouch--->onLongClick--->onClick;
(2)如果我们在onLongClick方法中返回true的话,那么随后的onClick方法将不再会执行;
(3)我们的onLongClick方法以及onClick方法可能会在dispatchTouchEvent方法返回之后才去执行,原因在于onLongClick方法是在CheckForLongPress类型的子线程中执行的,onClick是在PerformClick类型的子线程中执行的,也即解释了上面Log输出第12行出现在第9行之后,以及第24行出现在第21行之后的问题;
好了,这篇先到这里了,下篇从实例测试的角度进行不同情况下的分析;