事件分发机制是android中的核心知识点和难点。相信不少人也和我同样对于这点感到很是困惑。我看了不少篇博客和书面资料。今天咱们就聊聊事件的分发机制。前端
在了解点击事件的传递规则以前,咱们首先要弄明白什么事点击事件(MotionEvent),所谓MotionEvent是指手指接触屏幕后所产生的一系列事件。android
ACTION_DOWN————手指刚接触屏幕。
ACTION_MOVE————手指在屏幕上移动。
ACYION_UP————手指从屏幕上松开的一瞬间。ios
点击事件的分发过程就是MotionEvent
的分发过程,该过程主要由如下三个函数来完成:app
public boolean dispatchTouchEvent(MotionEvent ev)
ide
功能:用来进行事件的分发函数
public boolean onInterceptTouchEvent(MotionEvent ev)
源码分析
功能:用来判断是否拦截某个事件。post
public boolean onTouchEvent(MotionEvent ev)
学习
功能:处理点击事件,在dispatchTouchEvent
中调用。返回结果表示是否消耗当前点击事件。this
先不急咱们从最简单的OnClickListener
来看,OnClickListener
的优先级最低,处于事件传递的尾端。
咱们首先简单建立一个Android 项目,只有一个 Activity ,而且 Activity 中有一个按钮。若是咱们想要给这个按钮注册一个点击事件,只须要调用以下的代码:
button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.e("TAG_紫雾凌寒","执行了onClick"); } });
这样在onClick()
方法里面写咱们须要处理的业务逻辑,就能够在按钮被点击的时候执行。再若是想给这个按钮再添加一个 touch 事件,只须要调用以下所示的代码:
button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.e("TAG_紫雾凌寒","执行了onTouch==Action="+event.getAction()); return false; } });
咱们仅仅凭 Touch[触摸] 和 Click[点击] 就可以猜测到onTouch()
方法里能作的事情比onClick()
要多一些,好比判断手指按下、抬起、移动等事件。那么我同时给 button 两个事件都注册了,哪个会先执行呢?咱们用事实说话,运行程序点击按钮,咱们会发现打印结果以下:
这里咱们能够看到,onTouch()
是优先于onClick()
执行的,而且根据日志能够看到onTouch()
执行了两次,一次是 ACTION_DOWN ,一次是 ACTION_UP (当你手指按下屏幕并在屏幕上滑动时,还会有屡次 ACTION_MOVE 的执行)。所以事件传递的顺序是先通过onTouch()
,再传递到onClick()
。
有些同窗可能已经注意到,onTouch()
方法是有返回值的,这里咱们返回的是 false 。若是咱们尝试把onTouch()
方法里的返回值改为 true ,再运行一次,结果以下:
咱们发现,onClick()
方法再也不执行了!那为何会这样呢?具体的缘由看完这篇文章你们就明白了,这里咱们能够先理解成onTouch()
方法返回 true 就认为这个事件被onTouch()
消费了,于是不会再继续向下传递。
若是读到这里,以上全部的知识点你都清楚,那么说明你对 Android 事件传递算是入门了。
下面咱们继续接着往下看,咱们经过源码的角度来分析如下。
首先咱们要知道,当咱们手指触摸屏幕上的控件后,接下来确定会调用它的dispatchTouchEvent
方法。咱们根据下面一张图来分析
当咱们手指点击屏幕上的 button 时,就会去调用 button 的dispatchTouchEvent
方法,这时候会发现button 里面没有这个方法,那么它就会继续向上查找它的父类 TextView 的dispatchTouchEvent
方法,若是没有仍是继续向上查找,直到找到 View 中会发现这里有dispatchTouchEvent
方法。
下面咱们根据源码来看看,事件到底是如何传递的?首先咱们仍是来看dispatchTouchEvent
方法。
public boolean dispatchTouchEvent(MotionEvent event) { /***********省略部分代码******************/ boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // 若是是Down中止滚动 stopNestedScroll(); } //重要的代码就是这里 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; }
这里咱们首先看到它定义了一个变量 result
,它的默认值是false,仅接着就去调用了onFilterTouchEventForSecurity(event)
这个方法,这个方法主要做用就是判断该触摸事件要不要分发,咱们下面来看下这个方法。
public boolean onFilterTouchEventForSecurity(MotionEvent event) { if (// 先检查View有没有设置被遮挡时不处理触摸事件的flag (mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 // 再检查受到该事件的窗口是否被其它窗口遮挡 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { // Window is obscured, drop this touch. return false; } return true; }
这个方法的代码很少几行就是判断当前 View 有没有被遮挡,还有 View 对应的窗口有没有被遮挡。
Tips:既然判断事件要不要被分发,有一条是根据mViewFlags
标志的,那咱们彻底能够经过设置或是清楚FILTER_TOUCHES_WHEN_OBSCURED
标志位,这样就能够控制触摸事件在弹出窗口后,后续的事件可否继续处理。
看完onFilterTouchEventForSecurity
方法咱们继续回到前面的dispatchTouchEvent
中。咱们看到若是前面是true
,那么接下来会判断 view 的mOnTouchListener
是否是空,而且这个View是否是能够点击的,若是能够点击而且mOnTouchListener
不为空的话,就会继续调用mOnTouchListener.onTouch(this.event)
,它若是也是 true 的话,就给result
赋值为 true ,后面就再也不调用view的点击事件了。这就是咱们前面说的onTouch()
的方法改成 true 后就不会再执行onClik
的缘由。
Tips:也就是说咱们调用setOnTouchListener
设置的 OnTouchListener 的onTouch()
优先级比onTouchEvent(event)
高。
若是前面不知足result
为false,那么就会继续调用onTouchEvent(event)
方法。
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //判断View是否是可点击 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; /***********省略部分代码******************/ if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } /***********省略部分代码******************/ // 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)) { performClickInternal(); } } } /***********省略部分代码******************/ break; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } /***********省略部分代码******************/ break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } /***********省略部分代码******************/ break; } return true; } return false; }
咱们看到这个方法很是的长,咱们注意下面几点就OK。
1.clickable
判断 View 是否是可点击的。
2.若是是的话会根据手势的 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL。来执行不一样的代码。
3.咱们主要看按下手势 ACTION_DOWN 和抬起手势 ACTION_UP。
下面咱们首先看 ACTION_DOWN ,若是是不可点击的那么就会执行checkForLongClick(0, x, y)
判断是否是长按。
private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }
咱们看到这里主要是若是是长按的话会,延迟发送消息执行一个Runable-CheckForLongPress
,下面咱们看下,这个 Runable 的run()
方法:
@Override public void run() { if ((mOriginalPressedState == isPressed()) && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } }
这里咱们看到其实就作了一件事调用了(performLongClick(mX, mY)
咱们继续跟这个方法,发现它最后调用performLongClickInternal
执行了长按的操做。这里就很少作深刻了。
咱们回到 ACTION_DOWN ,继续往下看,咱们会发现紧接着就调用了performButtonActionOnTouchDown(event)
,这个方法就是判断是否是鼠标右键,弹出菜单之类的,下面会判断是否是滚动视图之类的。咱们这里了解一下就好。
下面咱们看当咱们抬起手指的时候,执行了那些操做呢?
case MotionEvent.ACTION_UP: /***********省略部分代码******************/ // 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)) { performClickInternal(); } } /***********省略部分代码******************/
这里咱们主要看核心代码,那就这里执行了 performClickInternal()
,咱们来看看它作了哪些?
private boolean performClickInternal() { // Must notify autofill manager before performing the click actions to avoid scenarios where // the app has a click listener that changes the state of views the autofill service might // be interested on. notifyAutofillManagerOnClick(); return performClick(); }
咱们看到这个方法很简单直接 return 了performClick()
,咱们接下来继续看这个方法。
public boolean performClick() { // We still need to call this method to handle the cases where performClick() was called // externally, instead of through performClickInternal() notifyAutofillManagerOnClick(); 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); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
在这个方法中咱们会看到,这里它仍是获取了mListenerInfo
,而且判断了它的OnClickListener
是否是为空,若是不为空则执行mOclickListener.onClick()
方法。
看到这里,你们是否是明白为何,View 的onClick
方法会在最后执行了。
这一篇文章咱们首先介绍了事件的传递机制,再经过源码分析了 View 的onTouch
方法为何比onClick
方法优先执行。咱们学习了 View,那咱们还知道 Activity 是一个 ViewGroup ,下篇文章咱们来分析下手指从触摸屏幕到 Activity 再到 ViewGroup 的传递。
下面咱们经过一张图来总结如下dispatchTouchEvent
方法
欢迎在评论区留下你的观点你们一块儿交流,一块儿成长。若是今天的这篇文章对你在工做和生活有所帮助,欢迎 转发分享给更多人。同时欢迎你们加入我组建的大前端学习交流群,群里你们一块儿学习交流 Android、Flutter等知识。从这里出发咱们一块儿讨论,一块儿交流,一块儿提高。
群号:872749114