事件分发机制是android中的核心知识点和难点。相信很多人也和我一样对于这点感到非常困惑。我看了很多篇博客和书面资料。今天我们就聊聊事件的分发机制。
一、点击事件的传递规则
1、什么是点击事件(MotionEvent)
在了解点击事件的传递规则之前,我们首先要弄明白什么事点击事件(MotionEvent),所谓MotionEvent是指手指接触屏幕后所产生的一系列事件。
ACTION_DOWN————手指刚接触屏幕。
ACTION_MOVE————手指在屏幕上移动。
ACYION_UP————手指从屏幕上松开的一瞬间。
2、点击事件分发过程
点击事件的分发过程就是MotionEvent
的分发过程,该过程主要由以下三个函数来完成:
public boolean dispatchTouchEvent(MotionEvent ev)
功能:用来进行事件的分发
public boolean onInterceptTouchEvent(MotionEvent ev)
功能:用来判断是否拦截某个事件。
public boolean onTouchEvent(MotionEvent ev)
功能:处理点击事件,在dispatchTouchEvent
中调用。返回结果表示是否消耗当前点击事件。
先不急我们从最简单的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 事件传递算是入门了。
下面我们继续接着往下看,我们通过源码的角度来分析以下。
3、点击事件递
首先我们要知道,当我们手指触摸屏幕上的控件后,接下来肯定会调用它的dispatchTouchEvent
方法。我们根据下面一张图来分析
当我们手指点击屏幕上的 button 时,就会去调用 button 的dispatchTouchEvent
方法,这时候会发现button 里面没有这个方法,那么它就会继续向上查找它的父类 TextView 的dispatchTouchEvent
方法,如果没有还是继续向上查找,直到找到 View 中会发现这里有dispatchTouchEvent
方法。
二、源码分析
下面我们根据源码来看看,事件究竟是如何传递的?首先我们还是来看dispatchTouchEvent
方法。
1.View.dispatchEvent(event)
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)
这个方法,这个方法主要作用就是判断该触摸事件要不要分发,我们下面来看下这个方法。
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)
方法。
2、View.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。
I.MotionEvent.ACTION_DOWN
下面我们首先看 ACTION_DOWN ,如果是不可点击的那么就会执行checkForLongClick(0, x, y)
判断是不是长按。
a.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)
,这个方法就是判断是不是鼠标右键,弹出菜单之类的,下面会判断是不是滚动视图之类的。我们这里了解一下就好。
I.MotionEvent.ACTION_UP
下面我们看当我们抬起手指的时候,执行了那些操作呢?
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()
,我们接下来继续看这个方法。
View.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源码分析——View是如何被添加到屏幕的?
Android热修复——深入剖析AndFix热修复及自己动手实现
深入理解HashMap原理(一)——HashMap源码解析(JDK 1.8)
深入理解HashMap原理(二)——手写HashMap
欢迎在评论区留下你的观点大家一起交流,一起成长。如果今天的这篇文章对你在工作和生活有所帮助,欢迎转发分享给更多人。
同时欢迎大家扫描左侧边栏的二维码关注我的公众号和加入我组建的大前端学习交流群,群里大家一起学习交流 Android、Flutter等知识。从这里出发我们一起讨论,一起交流,一起提升。