View的事件分发机制从dispatchTouchEvent提及

View知多少.png
事件分发机制是android中的核心知识点和难点。相信不少人也和我同样对于这点感到很是困惑。我看了不少篇博客和书面资料。今天咱们就聊聊事件的分发机制。前端

1、点击事件的传递规则

一、什么是点击事件(MotionEvent)

在了解点击事件的传递规则以前,咱们首先要弄明白什么事点击事件(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 两个事件都注册了,哪个会先执行呢?咱们用事实说话,运行程序点击按钮,咱们会发现打印结果以下:
imge1.png
这里咱们能够看到,onTouch()是优先于onClick()执行的,而且根据日志能够看到onTouch()执行了两次,一次是 ACTION_DOWN ,一次是 ACTION_UP (当你手指按下屏幕并在屏幕上滑动时,还会有屡次 ACTION_MOVE 的执行)。所以事件传递的顺序是先通过onTouch(),再传递到onClick()

有些同窗可能已经注意到,onTouch()方法是有返回值的,这里咱们返回的是 false 。若是咱们尝试把onTouch()方法里的返回值改为 true ,再运行一次,结果以下:
img2.png

咱们发现,onClick()方法再也不执行了!那为何会这样呢?具体的缘由看完这篇文章你们就明白了,这里咱们能够先理解成onTouch()方法返回 true 就认为这个事件被onTouch()消费了,于是不会再继续向下传递。

若是读到这里,以上全部的知识点你都清楚,那么说明你对 Android 事件传递算是入门了。
下面咱们继续接着往下看,咱们经过源码的角度来分析如下。

三、点击事件递

首先咱们要知道,当咱们手指触摸屏幕上的控件后,接下来确定会调用它的dispatchTouchEvent方法。咱们根据下面一张图来分析
touch.png
当咱们手指点击屏幕上的 button 时,就会去调用 button 的dispatchTouchEvent方法,这时候会发现button 里面没有这个方法,那么它就会继续向上查找它的父类 TextView 的dispatchTouchEvent方法,若是没有仍是继续向上查找,直到找到 View 中会发现这里有dispatchTouchEvent方法。

2、源码分析

下面咱们根据源码来看看,事件到底是如何传递的?首先咱们仍是来看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)方法。

二、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方法
事件传递机制.png

欢迎在评论区留下你的观点你们一块儿交流,一块儿成长。若是今天的这篇文章对你在工做和生活有所帮助,欢迎 转发分享给更多人。

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

群号:872749114

个人公众号

相关文章
相关标签/搜索