View的事件分发机制

前言

在 Android 中,View 是十分庞大的一个体系。对于一个 APP 来说,与用户产生交互的各种控件都是通过最为基本的 View 扩展而来的,例如 TextViewButton 等。而对 View 的学习,我会从两个方面入手,一个是事件分发机制,另一个是 View 的绘制流程。本着先易后难的原则,这一篇我们首先从源码的角度介绍 View 的事件分发机制。

进行事件分发机制的意义

对于这个问题,先举一个例子:

假设我们的手机屏幕现在有如上布局(并不标准,仅用于问题叙述),这个布局非常简单,仅有一个 ViewGroup,它里面包含了一个子 View。然后我们分别在两个黑点所示的位置产生点击事件,命名为点击1和点击2。可以看到,点击1产生的点击事件必然是由 ViewGroup 来处理这的;但是我们看到点击2,它点击的位置是在 View 里面,那么这个点击事件应当由谁来处理呢?在这里我们可能就有比较大的疑惑了,所以它是需要一个规则来进行这类情况的处理的。

可能你也已经猜到了,这类情况就是按着 View 的事件分发机制来进行处理的,这也就是事件分发机制存在的意义了,即由它来决定谁来干这个活(处理点击事件)。

准备工作

在开始真正分析源码之前,有几个概念我们先来理清楚,这有助于我们后面对源码进行理解。

点击事件

当我们点击手机屏幕的时候,就会产生点击事件,它被封装成了一个类:MotionEvent。这个类里面有许多的点击事件类型,例如 ACTION_DOWNACTION_UPACTION_MOVEACTION_CANCEL 等,表示按下、移动、抬起和非人为因素的关闭等操作。我们产生的点击事件类型基本上可以由这上面3个组成。

例如我们点击一个按钮,就是先按下然后抬起手指,即 ACTION_DOWN >> ACTION_UP;而拖动一个列表时,就是按下,移动,最后抬起,即 ACTION_DOWN >> ACTION_MOVE... >> ACTION_UP。这里需要注意的是 ACTION_MOVE 不止产生了一次,它会产生若干个 ACTION_MOVE

然后我们从这两个点击的例子中还可以注意到一个共同点,就是点击事件永远是以 ACTION_DOWN 为开始,以 ACTION_UP 为结束的。这其实也非常好理解,我们按下的时候,点击事件就开始了,直到我们抬手离开屏幕,这个点击事件也就结束了。

责任链模式

分发事件机制是一种比较典型的责任链模式,以上面展示的图为例,当 ViewGroup 收到这个点击事件以后,在条件允许的情况下,它会先向 View 分发这个事件,如果 View 处理了这个点击事件,那么这个分发就结束了;如果 View 不愿意处理这个事件,它会就将事件回退给 ViewGroup,交由 ViewGroup 来处理这个点击事件。

也就是说,分发事件机制不外乎就是找到一个“责任人”来对这个点击事件进行处理,将责任落实到实处。刚才说到,只有在条件允许的情况下,ViewGroup 才会向 View 分发这个事件,那么这个条件是什么呢?其实就是我们接下来要介绍的拦截机制了。

拦截机制

在事件分发机制中,是依据拦截与否来决定是否由该视图来处理这个点击事件的。如果这个视图拦截了这一点击事件,表示它不再将事件继续向子 View 传递,而是自己处理掉(消费掉这个事件);如果不进行拦截,则表示它会先将这个点击事件向子 View 传递(分发这个事件),而后再决定是否由自己处理这个点击事件。

在源代码中,我们会有两个重要的 boolean 型变量 disallowInterceptintercepted

disallowIntercept 的作用是判断是否允许拦截,不允许拦截的话,事件就会直接分发给子 View ;而如果允许拦截的话,就有了选择拦截的权利。这里要特别注意一点,允许拦截不代表一定会拦截!这里相当于提供给了我们的视图一个可以拦截的权利。也就是说,disallowIntercept 的值为 true 时,我们可以选择拦截下这个事件,自己消费掉,也可以不拦截,向子视图分发这个事件;而当值为 false 时,只能选择向子视图分发这个事件了。

而拦截与否的决定权是交由 intercepted 的,当 intercepted 值为 true 时,表示拦截了这个点击事件,也就表示要自己处理了!而为 false 时,顾名思义就是不拦截,果断向子视图分发事件。要注意到当 disallowIntercept 值为 false 时,intercepted 的值必定为 false,这也很好理解,不准进行拦截,那就只能选择分发啦。

Activity 的构成

要理解事件分发机制的源码,还是需要知道 Activity 的构成是什么样子的,限于篇幅,这里就不再做赘述,如果你还不知道的话,可以先看下笔者的这篇文章 Activity 的构成,里面对于 Activity 的构成做了介绍和分析。

好了,准备工作做完之后,我们就来开始分析源码吧!首先申明一点,笔者在这里进行源码分析时,只会截取出与该主体有关的关键代码进行分析,不会讲解整片代码,如果想要研究整片代码的话也可以在看完笔者的分析后再去自行研究。

源码分析

Acitvity 层

当我们的点击事件产生之后,点击事件首先会传入我们的 Activity,调用的方法是 Activity 的 dispatchTouchEvent 方法,我们来看看这个方法里面是做什么的:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

看到第一个 if 语句,如果为 ACTION_DOWN 的话,就会调用 onUserInteraction 方法。我们前面分析过,每一个点击事件的开头都是 ACTION_DOWN,所以必然会进入到这个 if 语句里面去调用 onUserInteraction 方法,那么我们就来看看这个方法做了什么:

	/**
     * Called whenever a key, touch, or trackball event is dispatched to the
     * activity.  Implement this method if you wish to know that the user has
     * interacted with the device in some way while your activity is running.
     * This callback and {@link #onUserLeaveHint} are intended to help
     * activities manage status bar notifications intelligently; specifically,
     * for helping activities determine the proper time to cancel a notfication.
     *
     * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
     * be accompanied by calls to {@link #onUserInteraction}.  This
     * ensures that your activity will be told of relevant user activity such
     * as pulling down the notification pane and touching an item there.
     *
     * <p>Note that this callback will be invoked for the touch down action
     * that begins a touch gesture, but may not be invoked for the touch-moved
     * and touch-up actions that follow.
     *
     * @see #onUserLeaveHint()
     */
    public void onUserInteraction() {
    }

可以看到这居然是一个空方法,从注释中可以得知当点击 home,back,menu 键等都会调用此方法,如果希望知道用户在 Acitvity 运行时以何种方式与设备进行了交互,那么可以实现这个方法,这个方法不是我们这一篇的重点,所以我们跳过,直接看第二个 if 语句。

第二个 if 语句里面首先调用了 getWindow 方法,这个方法会返回 mWindow 。我们知道 PhoneWindowWindow 这个抽象类的唯一实现类,换言之 mWindowPhoneWindow 类型的,所以调用的 superDispatchTouchEvent 方法也就是 PhoneWindow 这个类下的了,我们来看看它做了什么:

	@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到,这个方法直接返回了 mDecorsuperDispatchTouchEvent 方法,mDecorDecorView 类型的变量,它是 PhoneWindow 的一个子类。我们点进去这个类 superDispatchTouchEvent 的看看里面有什么:

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}

从这里我们可以看到,它返回的是父类的 dispatchTouchEvent 方法,而 DecorView 是继承自 FrameLayout 的,而 FrameLayout 又是继承自 ViewGroup, 所以也就相当于我们返回的 super.dispatchTouchEvent(event) 实际上返回的是 ViewGroupdispatchTouchEvent 方法。

好了,分析完了这段代码之后,我们先回到 ActivitydispatchTouchEvent 方法,为了方便观看我重新贴出来:

	public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 实际调用的是 ViewGroup 的 dispatchTouchEvent 方法
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

分析完了第2个 if 语句里面的方法,我们看到如果 superDispatchTouchEvent 返回 true,就意味着这个点击事件被处理了,那么 ActivitydispatchTouchEvent 方法也就会返回 true。换句话说也就是如果 ViewGroupdispatchTouchEvent 方法返回 true 就返回 true,否则它就会调用 ActivityonTouchEvent 方法。既然如此,我们就来看看 onTouchEvent 方法里面做了什么吧:

	public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

这段代码我就直接解释作用了,首先 if 语句判断是否应该结束掉这次点击事件,如果为 true 就结束 Activity 并返回 true,否则就返回 false。需要注意的是这个方法一般情况下都不会被执行,因为它的执行情况是这个 Activity 下的所有子 View 都不会处理这个点击事件,所以才会将这个点击事件回退到这个方法进行处理,而一般情况下我们总会有一个 View 愿意处理这个点击事件的。

分析完了 Activity 的 dispatchTouchEvent 方法,我们来对它做一个小结:

  • Acitvity 是分发事件的起点,由它开始向下经过一系列的处理,最终交由 ViewGroupdispathcTouchEvent 方法进行分发。
  • 如果 ViewGroupdispatchTouchEvent 返回 true,表示这个点击事件已经被处理了,ActivitydispatchTouchEvent 就会直接返回 true,否则它就会执行 onTouchEvent 方法,因为 ViewGroup 的点击事件被回退回来了,所以只能由 Activity 自己来处理。
  • 从第2条的结论我们可以知道,ActivityonTouchEvent 方法也就是回退点击事件的终点了。

为了将结论更清晰的表达出来,上面所说的过程可以用下图进行表示:
在这里插入图片描述

ViewGroup 层

好了,在我们理清了 ActivitydispatchTouchEvent 方法就是要调用 ViewGroup 方法之后,我们接下来就来对 ViewGroupdispatchTouchEvent 方法进行一个分析吧!

第一部分

这个方法的代码非常的长,我们只从中截取出了关键部分,并分为两部分来进行分析。首先来看看第一部分的代码:

	public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
        // 第一部分
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;// 1 

            // 关键点1:如果点击事件为 ACTION_DOWN,就将进行初始化
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                // 将 mFirstTouchTarget 设置为 null
                resetTouchState();
            }

            // 关键点2
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {  // IF1
                // 是否允许进行拦截
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // 如果允许拦截
                if (!disallowIntercept) {
                	// 关键点3:对 intercepted 进行赋值,onInterceptTouchEvent 方法
                	// 默认会返回 false,即不进行拦截
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); 
                } else { // 不允许拦截的话就直接放行,不拦截
                    intercepted = false;
                }
            } else {
                // 关键点4:当 mFirstTouchEvent == null && 非 ACTION_DOWN 事件
                intercepted = true;
            }
			......
    }

在关键点 1 处,actionMask 先获取了点击的类型,如果为 ACTION_DOWN 的话,说明产生了一个新的点击事件,所以我们要进行初始化操作,以免上一次遗留下来的东西影响了这次的点击事件。那么我们的初始化操作做什么呢?

我们在关键点2处下面的 if 语句中会看到一个变量 mFirstTouchTarget,这个变量非常关键,它在初始化操作的时候首先会被置空。然后接下来我们说说它的作用:mFirstTouchTarget 为空时,表示当前的 ViewGroup 拦截了这个事件或者 ViewGroup 下的子 View 没有一个可以处理这个事件;而当 ViewGroup 下的子 View 处理了这个点击事件时,当前 mFirstTouchTarget 就为非空。所以我们的初始化会将 mFirstTouchTarget 置空,以免之前的事件影响到了这次点击事件的处理。

接下来我们回到关键点2处的开头,可以看到有一个 boolean 型的变量 intercepted,它用于表示是否要对点击事件进行拦截。然后我们先看到 IF1 处的条件判断,我们刚刚把 mFirstTouchTarget 置空,所以要进入这个 if,只有可能为 ACTION_DOWN 了。这个 if 这么判断是为了屏蔽除 ACTION_DOWN 以外的所有事件,因为我们的事件序列开端只需要关注 DOWN 事件就够了。

我们接着往下看到关键点3处的 if 语句,它会先会判断是否允许拦截,disallowIntercept 变量可以通过该类的对象直接调用方法 requestDisallowInterceptTouchEvent 进行赋值。如果允许拦截的话,intercepted 就会通过 onInterceptTouchEvent 进行赋值,这个方法的源代码如下:

	public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

这个方法的 if 里面的条件在默认情况下是不成立的,所以这个方法在默认情况下会返回 false,如果我们在自定义的控件中希望拦截下这个事件自己处理,那么就可以重写这个方法返回 true

我们继续回到 dispatchTouchEvent 方法的关键点4处,如果为其他非 DOWN 事件并且 mFirstTouchTarget == null,说明这个事件被 ViewGroup 拦截下来自行处理了,也就是说 ViewGroup 自己会去处理这个点击事件,所以将 intercepted 设置为 true

总结一下

  • 这部分代码是通过获取到的 MotionEvent 的类型,如果为 DOWN 事件,代表一个点击事件序列的开端,就会执行初始化,将变量 mFirstTouchTarget 置空,然后通过两个 boolean 型变量 disallowInterceptintercepted 进行判断是否拦截该点击事件。
  • disallowIntercept 变量可以通过外部调用 requestDisallowInterceptTouchEvent 方法进行赋值,intercepted 可以通过重写 onInterceptTouchEvent 方法进行赋值。

第二部分

在判断完是否需要拦截之后,我们来继续阅读第二部分的源码吧。

	public boolean dispatchTouchEvent(MotionEvent ev) {
			......
			// 关键点1
            if (!canceled && !intercepted) {
						......
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            ......
                            // 关键点2
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            
							......
							
							// 关键点3
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // 关键点4
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
    }

我们先看到关键点1处,如果 ViewGroup 不进行拦截并且点击事件没有被取消的话,就会执行下面 if 中的语句。里面使用了一个 for 循环来遍历 ViewGroup 中的子 View,值得一提的是这个 for 循环是按照倒序遍历的,也就是从最外层的子 View 往内层遍历。

接下来看到关键点2处,这个 if 主要是判断点击事件位置是否在子 View 范围内或者子 View 是否在播放动画。如果均不符合说明这个子 View 不是我们要寻找的子 View,直接 continue 跳过。

我们继续看到关键点3处的 if 语句,我们看到 if 语句中有一个方法 dispatchTransformedTouchEvent,我们来看看它做了什么:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event); 
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

可以看到在这个方法的 if 语句中,如果传进来的 View 类型参数 child 为空,也就说明这个点击事件的位置下 ViewGroup 是没有子 View 的,那么 ViewGroup 就只能选择自己处理这个事件,所以调用的是父类 ViewdispatchTouchEvent 方法,如果 child 非空,那么就会调用 childdispatchTouchEvent 方法了。

View 层

假设我们的 child 就是 View 类型的,我们来看看 View 下面的 dispatchTouchEvent 方法做了什么吧:

public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        boolean result = false;
        ......
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            // 关键点1
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

			// 关键点2
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ......

        return result;
    }

在关键点1处,我们可以看到这个 if 语句满足的条件是:

  • 设置了外部监听 ListenerInfo
  • View 必须是 ENABLE
  • 监听的 onTouch 方法返回 true

如果均满足的话,返回值 result 就会被设置为 true,其中 onTouch 方法是个接口方法,如果我们没有实现这个方法的话,即默认情况下,第一个 if 语句的条件不会成立。如果第一个 if 条件不被满足的话,onTouchEvent 方法就会得到执行,因为此时 !result == true,所以第二个 if 会执行这个方法查看它的返回值。我们来看看这个方法里面的代码是什么:

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
		// 关键点1:判断是否是可点击的
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { // 关键点2
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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) { 
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();  // 关键点3
                                }
                            }
                        }

                        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();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

					......
            }

            return true;
        }

        return false;
    }

这个方法也是非常的长,我们主要看两个点,一个是关键点1,在这里,我们定义了一个 boolean 型的变量 clickable 判断 View 是否是可点击的,可以看到 CLICKABLELONG_CLICKABLE 都属于可点击类型,它们分别对应点击事件的点击和长按。

接下来看到关键点2,clickabletrue 时,进入到这个 if 语句,在这个 if 语句里面会用到 switch 对多种点击事件类型进行不同的处理,这里我们只关注 UP 事件,因为这是点击事件序列结束的标志,之后它就会执行相应的点击处理了。

最后经过一系列的执行,我们会来到关键点3处,执行 performClick 方法,这个方法的源代码如下所示:

	public boolean performClick() {
        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;
    }

从它的 if 语句可以看出,如果 View 设置了点击事件 onClickListener,那么它就会返回 true,它的 onClick 方法就会得到执行。这个 onClick 方法就是我们平常经常使用到的那个按钮监听方法了。所以 onTouchEvent 方法对应的就是我们设置的 onClick 方法。而且有一点需要特别注意的是,只要 View 是可点击的,onTouchEvent 方法就会返回 true,换言之 ViewdispatchTouchEvent 方法也会返回 true,也就是说 View 会消费掉这个点击事件。

总结一下:

  • 默认情况下 View 处理事件的方法是 onTouchEvent,除非 onTouch 方法被实现并返回 true
  • 只要 View 是可点击的,onTouchEvent 方法就会返回 true
  • ViewdispatchTouchEvent 方法可能处理事件的地方有两处,分别是调用 onTouch 方法和 onTouchEvent 方法。
  • 可以看出,如果我们在设置了 onTouch 监听并返回 true 的话,那么 result 在第一个 if 语句中就会被设置为 true,所以第二个 if 语句的 onTouchEvent 方法也就不会被执行。onTouch 先于 onClick 获得点击事件

分析到这里,假设我们子 ViewdispatchTouchEvent 方法返回了 true,说明这个点击事件被消费了,那么它的父视图 ViewGroupdispatchTouchEvent 方法也会返回 true,继续向上通知 ActivitydispatchTouchEvent 方法不用执行 ActivityonTouchEvent 方法了,这个事件已经被消费掉了!因为前面我们分析过,只有 ViewGroupdispatchTouchEvent 方法也会返回 falseActivityonTouchEvent 方法才会得到执行。

回退

从我们刚才分析完整个事件,一切看起来都是那么的和谐,但是我们没有考虑到一种情况:如果子 View 拒绝处理事件,即 ViewdispatchTouchEvent 返回 false 时,该怎么处理呢?讲到这里,我们就要返回到我们 ViewGroupdispatchTouchEvent 方法的第二部分了,第二部分剩下的代码如下所示:

	public boolean dispatchTouchEvent(MotionEvent ev) {
			......
            // 关键点4
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
    }

我们看到关键点4处,假设我们的子 ViewdispatchTouchEvent 方法返回 false 的话,根据我们之前的分析,ViewGroupmFirstTouchEvent 就会被设置为空,那么这个 if 里面的语句就会得到执行,这里面调用了 dispatchTransformedTouchEvent 方法,注意到它的参数 child 直接传入了 null,也就是自己执行 super.dispatchTouchEvent 方法,这也就是所谓的回退,因为子 View 拒绝处理点击事件,所以会回退给 ViewGroup 自行处理,而假设 ViewGroupdispatchTouchEvent 也返回 false 的话,它也会继续回退,依次类推,可以一直回退到 Activity 处,Activity 发现没有任何 View 愿意处理这个事件,就只能调用 onTouchEvent 方法自行处理了。

流程图

所以完整的事件分发的完整流程图应当是这样子的:
在这里插入图片描述
这个图应该就很清晰了吧,从最左边看起,向上的箭头代表的是底层的 View 一直向上对自己的父视图进行通知是否事件分发完成了。

然后左二一路向下的箭头就是父视图向子 View 分发事件的过程,可以看到只有 ViewGroup 需要判断是否需要进行拦截。

然后就是最右边看到我们 onTouchEvent 方法这一列,说明的就是我们上面所说的回退进制了。这个图其实说的不准确,因为通过源码我们也知道,父视图的 dispatchTouchEvent 方法只会根据子 ViewdispatchTouchEvent 的返回值来进行判断,而 onTouchEvent 方法在默认的情况下,其实是在 dispatchTouchEvent 内部被调用的,所以实际情况应当是子 ViewViewGouponTouchEvent 方法之间是个间接影响的过程,并不是直接影响,这是上面这个图不准确的地方。要注意:当我们实现了 onTouch 方法的时候我们应当根据我们的实际情况进行判断!因为 onTouch 的优先级是高于 onTouchEvent 的。

好了,本篇事件分发机制就介绍完了,祝大家学习愉快!

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值