图解+源代码 全面理解Android View事件分发

View事件分发是Android里面非常重要的知识点,我查阅了很多资料,新建demo分析Log日志加以验证,总结成这篇博文。

宏观的角度

View 事件分发涉及到三种角色,三个方法,三种重点事件,分别是Activity,ViewGroup,View;dispatchTouchEvent, onInterceptTouchEvent,onTouchEvent和ACTION_DOWN,ACTION_MOVE,ACTION_UP。

三个方法的关系可概括为如下伪代码:

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

值得记住的几个结论:

  1. activity的dispatchTouchEvent无论返回true or false都没有意义,但当View or ViewGroup的dispatchTouchEvent返回true时,表示它要消耗了该事件,并且该事件不会再向下传递。
  2. onTouchEvent返回ture表示它要消耗该事件,并且该事件不会再往下或往上传递。同一个View or ViewGroup的dispatchTouchEvent的返回值会与onTouchEvent的返回值同步,也就是说当onTouchEvent返回true,它的dispatchTouchEvent也会返回true。
  3. 因此,第一个View or ViewGroup要消耗一个事件,除了可以在onTouchEvent中返回true,还可以在dispatchTouchEvent中返回true。而前者是后者的充分非必要条件,
  4. 一个事件序列包含若干个事件,一般地,ACTION_DOWN是一个事件序列的开头,ACTION_UP是一个事件序列的结尾。
  5. 一个事件序列只能被一个VIew拦截且消耗。同一个事件序列中的事件不能分别由两个View同时处理。某个View一旦消耗了ACTION_DOWN事件,那么这一个事件序列的事件,都会从上往下一直传递到这个View为止(但往下传递的过程中上层的onInterceptTouchEvent仍会被调用,上层仍可以拦截事件),而且它的onInterceptTouchEvent不会再被调用了(如果有的话)。
  6. 如果一个事件序列本来要交给下层某一View or ViewGroup(代号A)来处理,但某个事件从上往下传递的过程中被上层某一个ViewGroup(代号B)拦截(onInterceptTouchEvent return true),那么A会收到一个ACTION_CANCEL事件,而原事件也消失了。此事件序列以后的事件就不会再传到A而是传给B,B获得了事件序列处理权。

ACTION_DOWN的事件分发图解

事件分发图
图源:http://www.jianshu.com/p/e99b5e8bd67b#rd

  • 带箭头的虚线上的super表示在起点方法内调用父类的重写方法后事件的流向,也就是说带super的虚线标识着事件默认的流动。
  • 带箭头的虚线上的true or false标识起点方法返回值,方法返回后事件的流动,
  • 令dispatchTouchEvent 直接返回false时,系统会回调父容器的onTouchEvent,事件会回流给父容器。
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return false;
    }
  • ACTION_MOVE,ACTION_UP与ACTION_DOWN的事件分发的异同可以概括为:ACTION_MOVE,ACTION_UP跟ACTION_DOWN从上往下传递,只不过当ACTION_MOVE,ACTION_UP传到拥有事件序列接管权的那一层View or ViewGroup时,不再调用该层的onInterceptTouchEvent 了,而是直接调用该层的onTouchEvent,不管它返回true or false,事件不再往别处传递。举个例子:Activity中依次套着ViewGroup1(ViewGroup2(View)) ,ACTION_UP在ViewGroup的onTouchEvent中被消费(红线表示),那么之后同一个事件序列的ACTION_MOVE,ACTION_UP的流向都会像蓝线那样。

ACTION_MOVE,ACTION_UP
图源:http://www.jianshu.com/p/e99b5e8bd67b#rd

源代码的角度

Activity对事件的分发过程

当一个点击操作发生时,事件最先传递到当前的Activity,由Activity的dispatchTouchEvent进行事件分发。

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

事件会先传递给PhoneWindow中的DecorView,如果DecorView不能处理,则调用自己的onTouchEvent。

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

当事件由VIewGroup的子元素消耗时,mFirstTouchTarget会指向该元素。mFirstTouchTarget其实是一种单链表结构,如果mFirstTouchTarget==null,那么ViewGroup就默认拦截接下来同一事件序列的事件。

  • (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)==false时,ViewGroup的onInterceptTouchEvent不会再被调用,并且同一个事件序列的其他事件都会默认交给它处理,因为此时的传过来的事件为ACTION_MOVE or ACTION_UP,而且没有一个子元素能处理,那就这能交给自己来处理了。
  • (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)==true,这时,是否拦截事件取决于标记位FLAG_DISALLOW_INTERCEPT,它是通过requestDisallowInterceptTouchEvent方法来设置,一般用于子View请求让父容器不要拦截除了ACTION_DOWN以外的事件。可以用于解决滑动冲突。

接着根据点击坐标定位候选子元素,并将事件逐个分发给子元素来处理。

                for (...){
                    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;
                            }
                        }
                }

如果某一子元素成功处理,mFirstTouchTarget会在addTouchTarget内被赋值。

View对事件的分发过程

在View的dispatchTouchEvent方法中进行事件分发

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            //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;
            }
        }
    }

首先会判断是否设置了OnTouchListener,如果OnTouchListener#onTouch返回true,那么onTouchEvent就不会被调用了。由此可见,OnTouchListener#onTouch的优先级高于onTouchEvent。

在View中的onTouchEvent中会对点击事件做处理

        if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
          ...
                            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();
                                }
                            }

          return true;
        } else {
          return false;
        }

只要VIew的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件。在ACTION_UP事件传来时,如果VIew设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。

希望大家能对Android View事件分发有更深刻的理解,共勉。

参考资料:

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页