Android学习笔记之View的事件分发机制

一、点击事件的传递规则

所谓的点击事件分发过程,其实就是当我们点击屏幕,产生了一个MotionEvent之后,系统将这个事件传递给一个具体View的过程。总的来说,事件总是先传递给Activity,然后传递给Window,再传递给顶级View(Activity→Window→DecorView),最后再按照事件分发机制一层一层向下去分发事件。而这个分发过程由三个很重要的方法来共同完成:

  1. dispatchTouchEvent(MotionEvent ev):用来进行事件的分发。如果事件能够传递给当前View,那么该方法一定会被调用,返回结果受当前View的onTouchEvent方法和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
  2. onInterceptTouchEvent(MotionEvent ev):在上述方法的内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会再次调用,返回结果表示是否拦截当前事件。
  3. onTouchEvent(MotionEvent ev):在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

上述三个方法之间的关系可以通过如下伪代码表示:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){//如果拦截
        consume = onTouchEvent(ev);
    } else {//如果不拦截
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

通过上面的伪代码,我们可以大致了解点击事件的传递规则:对一个ViewGroup来说,当一个点击事件传递给它时,它的 dispatchTouchEvent 方法会被调用,如果这个ViewGroup拦截此事件,那么事件将会交由该ViewGroup来处理(即调用 onTouchEvent 方法);如果它不拦截此事件,那么事件将会向下传递给它的子元素,接着子元素的 dispatchTouchEvent 方法会被调用,如此反复直到时间被最终处理。

关于事件传递机制的一些结论:

  1. 所谓的一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 down 事件开始,以 up 事件结束,中间含有多个 move 事件。
  2. 某个 View 一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的 onInterceptTouchEvent 不会再被调用。
  3. 参考第2条,通常而言,一个事件序列只能被一个 View 拦截消耗。但是通过特殊手段,可能出现多个 View 处理的情况,比如一个 View 将本该自己处理的事件通过 onTouchEvent 强行传递给其他View处理。
  4. 某个View一旦开始处理事件(onTouchEvent 开始执行),如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回 false),那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理(即父元素的 onTouchEvent 会被调用)。在这里,如果所有 View 的 onTouchEvent 返回 false,那么最终会传递给 Activity 处理(即Activity 的 onTouchEvent 会被调用)。
  5. 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前View可以持续受到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
  6. ViewGroup 默认不拦截任何事件。
  7. View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  8. View 的 onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击的(clickable、longClickable、contextClickable都为 false)非提示框控件。View 的 longClickable、contextClickable属性默认都为false,但 clickable 属性要视情况:比如 Button 为 true,TextView 为 false。
  9. View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个View是disable状态的,只要它的 clickable、longClickable、contextClickable不同时为 false,其 onTouchEvent 就默认返回 true。
  10. onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。
  11. 事件的传递过程是由外向内的。通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

二、事件分发的源码分析 

1.点击事件向上传递至顶级View

本文最开始有提到,事件总是先传递给Activity,然后传递给Window,再传递给顶级View(Activity→Window→DecorView),最后再按照事件分发机制一层一层向下去分发事件。所以事件最开始是传递给当前Activity,由 Activity 的 dispatchTouchEvent 方法进行事件派发,如下:

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

可以看到,Activity拿到事件之后会交给它所属的 Window 进行分发,如果返回true,那么整个事件循环就结束了,如果返回false,意味着所有 View 的 onTouchEvent 返回了false,最后 Activity 的 onTouchEvent 会被调用(上述结论中的第4条)。

接下来我们看下Window是如何处理事件的。值得注意的是 Window 是一个抽象类,其 superDispatchTouchEvent 方法也是一个抽象方法,因此我们需要查看 Window 的具体实现类的源码,即 PhoneWindow 的 superDispatchTouchEvent 方法,如下:

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

可见 PhoneWindow 将事件直接传递给了 DecorView。从这里开始,事件就已经传递到顶级View了,也叫根View,通常而言顶级View都是ViewGroup。

2.顶级View对点击事件的分发过程

在上一节中,有说事件分发机制涉及到三个重要的方法,并且对于ViewGroup来说,如果它拦截该点击事件,那么事件就会交由它来处理,如果它不拦截该事件,那么则会交由它的子元素处理。我们先来看下 ViewGroup 的 dispatchTouchEvent 方法中有关是否拦截该事件的逻辑代码块:

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {//DOWN事件总会进一步判断是否拦截,除此之外的其他事件需要看mFirstTouchTarget是否为空
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {//如果没有子元素要求禁止拦截,则进一步判断是否拦截
        intercepted = onInterceptTouchEvent(ev);//判断是否拦截
        ev.setAction(action); // restore action in case it was changed
    } else {//如果子元素要求禁止拦截
        intercepted = false;
    }
} else {//如果不是DOWN事件,并且mFirstTouchTarget为空
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;//拦截事件
}

上面注释得比较清楚了,需要说明的有两点。第一点是 mFirstTouchTarget 是一个指针,指向处理了该事件(DOWN事件)的子元素,换言之如果该 ViewGroup 的所有子元素都没有处理该事件,那么 mFirstTouchTarget == null。这样就会使同一个事件序列中除DOWN以外的其他事件全部被 ViewGroup 拦截。这也就说明了上一节中的第4条结论。第二点是有关子元素请求父元素禁止拦截的,其方法为 requestDisallowInterceptTouchEvent。一旦子元素调用了 requestDisallowInterceptTouchEvent 方法,那么父元素的对应flag就会被置高,ViewGroup 将无法拦截除了DOWN以外的其他事件。这是因为ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置上述flag。如下:

if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

通过上述代码块的分析,就可以得出上一节中得出的第2条结论,以及第11条结论。

接着再看当ViewGroup不拦截此次事件时,如何分发给子元素处理,如下:

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }
    //如果子元素可以接收到点击事件(通过了上述判断)
    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//判断条件内部调用child.dispatchTouchEvent方法
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            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);//方法内部完成了mFirstTouchTarget的赋值
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
    ev.setTargetAccessibilityFocus(false);
}

上述代码首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件(判断条件:1.是否在播放动画 2.点击事件坐标是否落在子元素内),如果子元素可以接收到事件,那么将会交由它来处理,具体是在dispatchTransformedTouchEvent 方法中调用的,如下(注意此时传入的第三个参数child非空):

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

如果子元素的 dispatchTouchEvent 方法返回 true,那么 mFirstTouchTarget 就会被赋值并跳出 for 循环;如果子元素的 dispatchTouchEvent 方法返回 false,ViewGroup 就会把事件分发给下一个子元素。如果遍历所有子元素后事件都没有被合适处理,那么ViewGroup就会自己处理该事件,如下(注意此时传入的第三个参数为null):

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}

3.普通View对点击事件的处理过程

老样子,先看它的 dispatchTouchEvent 方法,如下:

public boolean dispatchTouchEvent(MotionEvent event) {

    boolean result = false;
    ......
    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)) {//如果有设置onTouchListener并且onTouch返回true
            result = true;
        }

        if (!result && onTouchEvent(event)) {//注意:如果此时result为true,onTouchEvent不执行
            result = true;
        }
    }
    ......    
    return result;
}

通过上述代码,可以看到,View对点击事件的处理首先会判断有没有设置onTouchListener,如果onTouchListener中的onTouch 方法返回 true,那么 onTouchEvent 就不会被调用,可见 onTouchListener 的优先级高于 onTouchEvent,这样的好处就是方便在外界处理点击事件。

接着再分析 onTouchEvent 的实现。先来看下上一节中的第9条结论是否正确,如下:

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;
}

不难看出,即使View是 disable 的,其返回值clickable只与三种点击状态有关,需要同时为false,才返回false。

再看一下 onTouchEvent 中对点击事件的具体处理,如下:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            ......
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                ......
                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();
                        }
                    }
                }
                ......
            }
            mIgnoreNextUpEvent = false;
            break;
    ......
    }
    return true;
}

从上述代码来看,只要 View 不是不可点击的非提示框控件,就会进入上面的 if 判断,最后默认返回 true,消耗此次事件。这就证实了上一节中的第8条结论。然后就是第10条结论,当ACTION_UP事件发生时,会触发 performClick 方法,如果 View 设置了 OnClickListener,那么 performClick 方法内部会调用它的 onClick 方法。

你可能感兴趣的:(Android)