Android事件传递机制

  网上关于事件传递机制的文章也是多得不行了,在想写这篇的意义。写下这篇主要是方便自己,梳理完善自己对事件传递机制的整体认识。还有这篇文章编写方式会先给结论,后作出源码分析,异于源码与结论结合在一起。这样对于回顾这个知识点的时候,可以直接看结论,比较方便。

一、事件传递机制

  明确我们分析的对象是 MotionEvent ,即点击事件。所谓点击事件的分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生以后,系统需要把这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成:

  public boolean dispatchTouchEvent(MotionEvent event)

  用来进行事件的分发,如果事件传递给当前View,那么此方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。

 public boolean onInterceptTouchEvent(MotionEvent ev)

  在 dispatchTouchEvent 方法中调用,用来判断是否拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

  public boolean onTouchEvent(MotionEvent event)

  在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。

  上述三个方法的区别和联系,用伪代码来表示:

 public boolean dispatchTouchEvent(MotionEvent ev) {
         boolean comsume = false;
         if (onInterceptTouchEvent(ev)){
             comsume = onTouchEvent(ev);
         }else {
             comsume = child.dispatchTouchEvent(ev);
         }
         return comsume;
     }

  上面的伪代码,我们可以大致了解点击事件的传递规则,对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的onInterceptEvent返回 true 就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的OnTouchEvent方法机会被调用;如果这个 ViewGroup 的onInterceptEvent返回 false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。

二、总结点:

  <1> 事件传递顺序: Activity  >>>  Window  >>>  View ,即事件总是先传递给Activity,Activity 再传递给 Window(PhoneWindow) ,最后 Window 再传递给顶级 View(DecorView)。顶级 View 接收到事件后,就会按照事件分发机制去分发事件。

  <2> 事件的优先级: dispatchTouchEvent  >>>  onInterceptTouchEvent  >>>  onTouch(OnTouchListener)  >>>  onTouchEvent  >>>  onClick(OnClickListener) 。这个顺序的前提是,一个 View 需要处理事件。

  <3> 同一个事件序列是指从手指接触屏幕的那一刻起,到手离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。

  <4> 正常情况下,一个事件序列只能被一个 View 拦截且消耗。因为一旦一个元素拦截了事件,那么同一个事件序列内的所有事件都会直接交给它处理。但是通过特殊手段也可以做到,比如一个 View 将本该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理。

  <5> 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。

  <6> 如果 View 不消耗除了 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。

  <7> ViewGroup 默认不拦截任何事件, Android 源码中 ViewGroup 的 onInterceptTouchEvent 方法默认返回 false 。

  <8> View 的 onTouchEvent 默认都会消耗事件(返回 true),除非他是不可点击的(clickable 和 longClickable 同时为 false)。View 的longClickable 属性默认都为 false,clickable属性 要分情况,例如 Button的clickable属性默认为 true,而 TextView 的 clickable 属性默认为 false。

  <9> View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。   这个是区别与 ViewGroup 的,注意一下。

  <10> View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longclickable 有一个为 true,那么它的 onTouchEvent 就返回 true。

  <11> onClick 会返生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。

  <12> 事件传递时由外向内的,即事件传递给父元素,然后再由父元素分发给子 View ,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

  <13> 某个 View 一旦确定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会被 再次 调用。

三、事件分发的源码分析(SDK27)

  1. Activity 对点击事件的分发过程

   /*
     * 源码: Activity#dispatchTouchEvent*/
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction(); //点击Activity,按下时的一个回调,没有具体实现
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

  分析上面的代码,首先事件交给 Activity 所附属的 Window 进行分发,如果 getWindow().superDispatchTouchEvent(ev)返回 true,整个事件循环就结束了。如果返回 false 意味着事件没人处理,所有 View 的 onTouchEvent 都返回了 false ,那么 Activity 的 onTouchEvent 就会被调用。

2. Window对点击事件的分发过程

  接下来看 Window 是如何将事件传递给 ViewGroup 的。来看一下Window的源码:

/**
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
        public boolean superDispatchTouchEvent(MotionEvent event);
}

  通过源码可以知道,Window是一个抽象类,它的 superDispatchTouchEvent 方法也只是一个抽象抽象方法,并不能为我们找到事件的具体传递过程。那就需要找到具体的实现类,根据注释,我们可以知道 唯一的实现类就是 PhoneWindow 。那就直接看它的 superDispatchTouchEvent:

/**
*源码: PhoneWindow#superDispatchTouchEvent
*/
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) { 
  
return mDecor.superDispatchTouchEvent(event);
}

  很清晰,PhoneWindow 直接就把事件交给了 DecorView ,还不知道 DecorView 是什么,看注释说明它是 Window 最顶层的 View,它是一个FrameLayout的实现类,是一个ViewGroup ,是我们所有布局的父容器,这应该就大概明白了。所以 Window 就把事件这样分发到了 DecorView 这个 ViewGroup 里。

3. 顶级 View(ViewGroup) 对点击事件的分发过程

  关于点击事件如何在 View 中进行分发,文章第一点已经交代,重点是 View 或者 ViewGroup 中,那三个重要的方法之间的关系。顶级 View 就是一个 ViewGroup ,到这里已经是ViewGroup对事件的处理,看看事件是如何继续分发的,我们直接看源码来证明之前的描述:由于ViewGroup的 dispatchTouchEvent 太长,我们分开解读:

 // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN||mFirstTouchTarget != null) {
                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 {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

  通过代码的第一个 if 语句判断条件可以看出,ViewGroup 在如下两种情况下会判断是否要拦截当前事件: 事件类型为 ACTION_DOWN 或者 mFirstTouchTarget != null。事件类型为ACTION_DOWN 好理解,那么 mFirstTouchTarget != null 是什么意思? 这个从后面的代码逻辑可以看出,当时间由 ViewGroup 的子元素成功处理时,mFirstTouchTarget 就会被赋值并指向子元素。换种方式来说,当 ViewGroup 不拦截事件并将事件交给子元素去处理时 mFirstTouchTarget != null 。反过来,一旦事件由当亲 ViewGroup 拦截时,mFirstTouchTarget != null 不成立。那么当 ACTION_MOVE 和 ACTION_UP 事件到来时,由于 (actionMasked == MotionEvent.ACTION_DOWN||mFirstTouchTarget != null) 这个条件为 false ,将导致 ViewGroup 的 onInterceptTouchEvent 不会再被调用,并且同一序列中的其他事件都会默认交给它处理。

  从源码看到有一种特殊情况,FLAG_DISALLOW_INTERCEPT 标记位 ,这个标记位是通过 requestDisallowInterceptTouchEvent 方法来设置的,一般用于子 View 中。FLAG_DISALLOW_INTERCEPT 一旦设置后,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的其他点击事件。为什么说是除了 ACTION_DOWN 以外的其他事件呢? 这是因为 ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置的这个标记无效。因此,当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。在下面代码中,ViewGroup会在 ACTION_DOWN 事件到来时做重置状态的操作,而在 resetTouchState 方法中会对 FLAG_DISALLOW_INTERCEPT 进行重置,因此子 View 调用 requestDisallowInterceptTouchEvent 方法并不能影响 ViewGroup 对 ACTION_DOWN 事件的处理。

// Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

  从上面的分析,可以得出结论:当 ViewGroup 决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的 onInterceptTouchEvent 方法,这证实了 <总结点 -13> 。FLAG_DISALLOW_INTERCEPT 这个标志的作用是让 ViewGroup 不再拦截事件,当然前提是 ViewGroup 不拦截 ACTION_DOWN 事件,这证实了 <总结点 -12>

  这段分析总结起来有两点:第一点,onInterceptTouchEvent 不是每次事件都会被调用,如果我们想提前处理所有的点击事件,要选择 dispatchTouchEvent 方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的 ViewGroup ;另外一点,FLAG_DISALLOW_INTERCEPT 标记位的作用给我们提供了一个思路,当面对滑动冲突时,我们可以是不是考虑用这种方法去解决问题?关于滑动冲突,来看这篇(后补)。

  接着再看当 ViewGroup 不拦截事件的时候,事件会向下分发交由它的子 View 进行处理,这段源码看:

 

    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 there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        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) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        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);
    }

 

  上面这段代码逻辑也很清楚,首先遍历 ViewGroup 的所有子元素,然后判断子元素是否能够接收到点击事件,通过canViewReceivePointerEvents方法和isTransformedTouchPointInView方法中的条件来判断。是否能够接受点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。可以看到,dispatchTransformedTouchEvent 实际上调用的就是子元素的 dispatchTouchEvent  方法,在它的内部有如下一段内容:

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

  如果子元素的 dispatchTouchEvent 返回 true ,这时我们暂时不用考虑事件在子元素内部是怎么分发的,mFirstTouchTarget 就会被赋值跳出 for 循环,源码:

 

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

 

  这几行代码完成了mFirstTouchTarget 的赋值并终止了对子元素的遍历。如果子元素的 dispatchTouchEvent 返回 false , ViewGroup 就会继续遍历,直到找到并把事件分发。

  源码中对 mFirstTouchTarget 完成赋值是在 addTouchTarget 方法中完成的,通过下面 addTouchTarget 源码可以知道,mFirstTouchTarget 是一种单链表结构。mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略。

 

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

 

  如果遍历了所有子元素后事件没有被合适的处理,这包含两种情况:第一种是 ViewGroup 没有子元素;第二种是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为子元素在 onTouchEvent 中返回了 false 。在这两种情况下,ViewGroup 会自己处理点击事件,这里就证实了<总结 -5>。

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

 

   上面这段代码,dispatchTransformedTouchEvent 第三个参数为 null ,从前面的分析可以知道,它会调用 super.dispatchTouchEvent(event) 。这样ViewGroup中事件的分发机制就结束了。看看刚才

如果子元素满足接受点击事件的条件,事件将在子元素中分发,如果子元素为 ViewGroup ,那就会重复上面这个过程。如果子元素为 View , 那事件会怎么分发?

4. View 对点击事件的分发

   View 对点击事件的处理过程稍微简单一些,注意这里的 View 不包含 ViewGroup 。 先看它的 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)){
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

           ...
       
        return result;
    }    

  View(这里不包含 ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。从上面的源码可以看出 View 对点击事件的处理过程,首先会判断有没有设置 OnTouchListener,如果 OnTouchListener 中的 onTouch 方法返回 true,那么 onTouchEvent 就不会被调用,可见 OnTouchListener 的优先级高于 onTouchEvent。

  接着分析 onTouchEvent 的实现,先看当 View 处于不可用的状态下,点击事件的处理过程,如下所示,很显然,不可用状态下的 View 照样会消耗点击事件。

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 设置有代理,那么还会执行 TouchDelegate 的 onTouchEvent 方法。

 if (mTouchDelegate != null) {
      if (mTouchDelegate.onTouchEvent(event)) {
           return true;
       }
 }

  下面再看 onTouchEvent 中对点击事件的具体处理,如下所示 。

 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: ... 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) { // 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)) { performClick(); } } } ... } break; } return true; }

  从上面的代码来看,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true ,那么它就会消耗这个事件,即 onTouchEvent 返回 true,不管它是不是 DISABELE 状态,这就证实了<总结:- 8,10,11> 。然后就是当 ACTION_UP事件发生时,会触发 performClick 方法,如果 View 设置了 OnClickListener,那么 performClick 方法内部会调用它的 onClick 方法,如下所示:

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

  这可以看出 onClick的优先级时最低的,结合之前的 onTouch 优先级 ,也就验证了<总结 - 2>

  到这里,点击事件的分发机制源码也就分析完了。这一篇要是能梳理下来,感觉对事件的分发机制也是通透了许多。

 

  

posted @ 2018-04-21 20:56  Spiderman.L  阅读(781)  评论(0编辑  收藏  举报