七、事件分发

1.事件分发的总结

  • 事件分发顺序:Activity(Window) -> ViewGroup -> View
  • 事件分发过程由dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()三个方法协助完成。
    三个方法之间的关系,伪代码如下:
    //点击事件产生后,会直接调用dispatchTouchEvent()方法
    public  boolean dispatchTouchEvent(MotionEvent event){
        //代表是否消耗事件
        boolean consume = false;
        if (onInterceptTouchEvent(event)){
            //如果onInterceptTouchEvent()返回为true,则代表当前View拦截的点击事件
            //事件会被当前View进行处理
            //即调用onTouchEvent()方法去处理点击事件
            consume = onTouchEvent(event);
        }else{
            //不拦截,交由子元素处理,重复上述过程
            //直到点击事件被处理为止。
            consume = child.dispatchTouchEvent(event);
        }
        return consume;
    }

2.事件分发的源码分析

事件分发顺序:Activity(Window) -> ViewGroup -> View

1. Activity中的事件分发

当一个点击事件发生时,事件最先传递到Activity中的dispatchTouchEvent()进行事件分发

具体是由Activity的window来完成。

Activity中的dispatchTouchEvent源码:

  public boolean dispatchTouchEvent(MotionEvent ev) {
        //一般事件最开始都是down,所以这里返回true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          //注意1
            onUserInteraction();
        }
        //注意2
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
  • 注意1:onUserInteraction(),方法为空方法,当该Activity处于栈顶的时候,触屏点击home,back,menu键等都会触发此方法。所以onUserInteraction()方法主要用于屏保
  • 注意2:Window类是抽象类,且PhoneWindow是其唯一实现类。
    superDispatchTouchEvent()是也是抽象方法。
    PhoneWindow类中的superDispatchTouchEvent()方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
    //mDecor是DecorView的实例
    //DecorView是视图的顶层view, 继承自FrameLayout, 是所有界面的父类
}

mDecor.superDispatchTouchEvent(event)方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
    //DecorView继承自FrameLayout
    //那么它的父类就是ViewGroup
    //而super.dispatchTouchEvent(event)方法, 其实就应该是      
    //ViewGroup的dispatchTouchEvent()
}

综上:执行getWindow().superDispatchTouchEvent(ev)实际上是执行了ViewGroup.superDispatchTouchEvent(ev)。
activity中的superDispatchTouchEvent实际上就是在执行ViewGroup.superDispatchTouchEvent()方法。这样,事件就从Activity传递到了ViewGroup。
总结:
当一个点击事件发生时,调用顺序如下,

  1. 事件最先传递到Activity的dispatchTouchEvent进行分发。
  2. 调用Window的实现类PhoneWindow的superDisPatchTouchEvent(),
  3. 调用DecorView的superDispatchTouchEvent()。即父类ViewGroup的dispatchTouchEvent()方法。
2. ViewGroup中的事件分发

ViewGroup的dispatchTouchEvent()方法:

...
          // Check for interception.主要是拦截器的处理
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
        //是否拦截
//可以在子View通过调用
//requestDisallowInterceptTouchEvent方法对这个值进行修改, 不让该View拦截事件
                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 {
                // 当没有触摸targets, 且不是down事件时, 开始持续拦截触摸。
                intercepted = true;
            }
...  

该代码主要是判断是否拦截。
a.如果当前事件的motionEvent.action_down,进入判断,
b.如果mFirstTouchTarget != null,即已经发生过motionEvent.action_down,并且该事件已经有ViewGroup的子view来处理了,那么也进行判断。调用viewgroup.onInterceptTouchEvent(),判断是否拦截。如果不是以上两种情况,即已经是move或者up事件,并且之前的事件没有对象处理,则设置为true,开始拦截接下来所有的事件。这也就解释了如果view的onTuchEvent()方法返回false,那么接下来的的一系列事件都不会交由它处理。如果ViewGroup的onInterceptTouchEvent(),第一次执行为true,则mFirstTouchTarget = null,则也会使得接下来不会调用onInterceptTouchEvent(),直接将拦截设置为true。
当viewgroup不拦截事件的时候,事件会向下分发交由它的子View或者ViewGroup进行处理。

/**从底层的父视图开始遍历,寻找newTouchTarget,即上面的mFirstTouchTarget ,如果已经存在寻找的newTouchTarget,说明正在接收触摸事件,则跳出循环。
*/
...
                    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;
                            }
//如果view不可见或者触摸的焦点不在view的范围内,跳过本次循环
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            newTouchTarget = getTouchTarget(child);
//视图已经接收触摸事件,退出整个循环
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
//重置取消或抬起标志位
//如果触摸位置在child的区域内,则把事件分发给view或者ViewGroup
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 获取TouchDown的时间点
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    //获取touchDown的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;
                            }
  • Android事件分发是先传递到ViewGroup,再由ViewGroup传递到view
  • 在ViewGroup中通过onInterceptTouchEvent()

1.onInterceptTouchEvent方法返回true代表拦截事件,即不允许事件继续向子View传递;
2.返回false代表不拦截事件,即允许事件继续向子View传递;(默认返回false)
3.子View中如果将传递的事件消费掉,ViewGroup中将无法接收任何事件。


3. View中的事件分发

View.dispatchTouchEvent源码分析:

//过滤触摸事件以应用安全策略。
     if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            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;
            }
        }
  1. 条件 li != null ,直接实例化,不为空
  2. 条件 li.mOnTouchListener != null

//mOnTouchListener是在View类下setOnTouchListener方法里赋值的
public void setOnTouchListener(OnTouchListener l) {
// getListenerInfo() 返回的是静态类ListenerInfo
//即只要我们给控件注册了Touch事件, mOnTouchListener就一定
//被赋值( 不为空)
getListenerInfo().mOnTouchListener = l;
}

  1. 条件 (mViewFlags & ENABLED_MASK) == ENABLED
  • 该条件是判断当前点击的控件是否enable
  • 由于很多view默认是enable的,因此该条件恒定为true。
  1. 条件 li.mOnTouchListener.onTouch(this, event)
  • 回调控件注册touch事件时的onTouch方法
          view.setOnTouchListener(new View.OnTouchListener() {
              @Override
              public boolean onTouch(View v, MotionEvent event) {
                  return false;
              }
          });
    
  • 如果在onTouch方法返回true,就会让条件成立,整个方法返回为true
  • 如果在onTouch方法返回false,就会继续后面的判断,执行onTouchEvent(event)方法。
    onTouchEvent(event):
public boolean onTouchEvent(MotionEvent event) {
    ...
       if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
           switch (action) {
               case MotionEvent.ACTION_UP:
                   mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                   if ((viewFlags & TOOLTIP) == TOOLTIP) {
                       handleTooltipUp();
                   }
              
                   boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0
                       if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                           // This is a tap, so remove the longpress check
                           removeLongPressCallback();
                           if (!focusTaken) {
                       // 注意: 经历了种种判断进入performClickInternal()方法
                               if (mPerformClick == null) {
                                   mPerformClick = new PerformClick();
                               }
                               if (!post(mPerformClick)) {
                                   performClickInternal();
                               }
                           }
                       }

                       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;

注意:当手指抬起的时候触发点击事件,经过一系列的判断,调用 performClickInternal(),该方法里面又调用了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;
  }
  • 只要li.mOnClickListener 不为空,就会调用onclick方法。

     public void setOnClickListener(@Nullable OnClickListener l) {
             if (!isClickable()) {
              setClickable(true);
          }
          getListenerInfo().mOnClickListener = l;
      }
    

    mOnClickListener 是在View类下setOnClickListener方法里赋值的,而我们设置按钮点击的事件的时候,会调用该方法,自动赋值,最后回调onClick()方法。

结论:

  1. onTouch() 的执行高于onClick()。

  2. 每当控件被点击的时候:

    • 如果在回调onTouch()里面返回false,就会让dispatchTouchEvent()返回false,那么就会执行onTouchEvent()方法;如果回调了setOnclickListener()来给控件注册点击事件的话,最后会在通过performClickInternal(),调用performCik()方法里面调用onClick()方法。

    onTouch()返回false(该事件没有被onTouch()消费掉) = 执行onTouchEvent() = 执行Onclick()。

    • 如果在回调onTouch()里返回为true,就会让dispatchTouchEvent方法返回true,那么将不会执行onTouchEvent(),即onClick()也不会执行。

    onTouch()返回true(该事件被onTouch()消费掉) =dispatchTouchEvent()返回true(不会再继续向下传递) = 不会执行onTouchEvent() = 不会执行onClick().

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容