android点击事件的优先级,View的事件分发机制从dispatchTouchEvent说起

bVbFlf2

事件分发机制是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 两个事件都注册了,哪一个会先执行呢?我们用事实说话,运行程序点击按钮,我们会发现打印结果如下:

bVbFlf1

这里我们可以看到,onTouch()是优先于onClick()执行的,并且根据日志可以看到onTouch()执行了两次,一次是 ACTION_DOWN ,一次是 ACTION_UP (当你手指按下屏幕并在屏幕上滑动时,还会有多次 ACTION_MOVE 的执行)。因此事件传递的顺序是先经过onTouch(),再传递到onClick()。

有些同学可能已经注意到,onTouch()方法是有返回值的,这里我们返回的是 false 。如果我们尝试把onTouch()方法里的返回值改成 true ,再运行一次,结果如下:

bVbFlfX

我们发现,onClick()方法不再执行了!那为什么会这样呢?具体的原因看完这篇文章大家就明白了,这里我们可以先理解成onTouch()方法返回 true 就认为这个事件被onTouch()消费了,因而不会再继续向下传递。

如果读到这里,以上所有的知识点你都清楚,那么说明你对 Android 事件传递算是入门了。

下面我们继续接着往下看,我们通过源码的角度来分析以下。

3、点击事件递

首先我们要知道,当我们手指触摸屏幕上的控件后,接下来肯定会调用它的dispatchTouchEvent方法。我们根据下面一张图来分析

bVbFlfW

当我们手指点击屏幕上的 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方法

bVbFlfL

欢迎在评论区留下你的观点大家一起交流,一起成长。如果今天的这篇文章对你在工作和生活有所帮助,欢迎转发分享给更多人。

同时欢迎大家加入我组建的大前端学习交流群,群里大家一起学习交流 Android、Flutter等知识。从这里出发我们一起讨论,一起交流,一起提升。

群号:872749114

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值