Android 事件分发机制-源码分析

事件分发.png

说明

  • 具体流程如图所示

  • 对于dispatchTouchEvent , onTouchEvent 返回 true 就是自己消费了,返回 false 就传到父View 的onTouchEvent方法

  • ViewGroup 想把事件分发给自己的 onTouchEvent,需要在onInterceptTouchEvent方法中返回 true 把事件拦截下来

  • ViewGroup 的 onInterceptTouchEvent 默认不拦截,所以 super.onInterceptTouchEvent() = false

  • View(这里指没有子View)没有拦截器,所以 View 的dispatchTouchEventsuper.dispatchTouchEvent(event)默认把事件分发给自己的onTouchEvent

源码解析(以下内容部分来自书籍《Android开发艺术探索》)


Activity对点击事件的分发过程

  • 当一个点击事件发生时,事件首先传递给当前 Activity ,由 Activity 的 dispatchTouchEvent() 来进行事件的分发,具体的工作是由 Activity 内部 Window来完成。Window 会将事件传递给 decor view, decor view 一般就是当前 Activity 的顶层 View, 源码如下:

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 就表示没有人要处理,交由 Activity 的onTouchEvent处理;可知 Activity 调用 getWindow().superDispatchTouchEvent(ev)把事件分发给ViewGroup, 所以我们来看

Windows#superDispatchTouchEvent

public abstract boolean superDispatchTouchEvent(MotionEvent event)
  • 可以看出 Window 是个抽象类,且 Window 唯一实现的是 PhoneWindow,所以接下来我们看看 PhoneWindow 怎么处理该方法的

PhoneWindow#superDispatchTouchEvent

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
  • 到这里清晰, 是在 PhoneWidow 将事件传递给了 DecorView, 至于什么是 DercorView ,看源码里如何解释的:
// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

这个 mDecor 显然就是getWindow().getDecorView() 所返回的 View,而我们通过 setContentView 设置的 View 就是是它的一个子 view。目前事件传递到了 DecorView 这里,由于 DecorView 继承自 FrameLayout 且又是父 View,所以最终事件会传递给 我们所设置setContentView的顶级 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;
      }
  • 可以看出 ViewGroup 在两种情况下会判断是否要拦截当前事件:

    • 事件类型 为 ACTION_DOWN ,这个很好理解
    • 或者 mFirstTouchTarget != null , 这个从后面代码可以看出,当事件由 ViewGroup 的子元素成功处理时,mFirstTouchTarget 就会被赋值并指向子元素
  • 且当ACTION_MOVEACTION_UP 事件到来时,由于actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null这个条件为 false ,将导致 ViewGroup 的 onInterceptTouchEvent 不被调用,并且同一序列中的其他事件都会默认交给他处理。

  • 但是这里有个特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记,这个标记是子 View 通过requestDisallowInterceptTouchEvent方法来设置。一旦设置了该标记,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的事件。为什么说除了ACTION_DOWN 以外的事件,这点从源码也可以看出:因为在 ViewGroup 在事件分发时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT标记,将导致子View 设置的这个标记失效。因此 当事件为ACTION_DOWN时 ViewGroup 总是会调用自己的onInterceptTouchEvent来询问是否拦截事件。
    我们看看上面代码的前一句代码就明白了:
    代码位置

     // 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 在ACTION_DOWN事件时会做重置操作:会在resetTouchState()FLAG_DISALLOW_INTERCEPT标记进行重置,因此子 View调用requestDisallowInterceptTouchEvent方法并不能影响 ViewGroup 对ACTION_DOWN事件的处理
  • 从上面分析我们可以总结出两点:

    • onInterceptTouchEvent 不是每次事件都会被调用,如果我们想在当前的 ViewGroup 处理所有的点击事件,就要选择onInterceptTouchEvent方法中处理,只有这个方法能确保每次都被调用
    • FLAG_DISALLOW_INTERCEPT 给我们提供了另一种思路去解决滑动冲突的方法:在子 View 拦截处理

接下来我们看 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 的所有子元素,然后判断是否能够接收到点击事件。是否能够接收点击事件主要有两点判断:

    • 子元素是否在播放动画
    • 点击事件的坐标是否在子元素坐标区域内
      可以看到他调用了dispatchTransformedTouchEvent方法来传递事件,所以我们来看看该方法
      ViewGroup#dispatchTransformedTouchEvent
     private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
              View child, int desiredPointerIdBits) {
          final boolean handled;
    
          // Canceling motions is a special case.  We don't need to perform any transformations
          // or filtering.  The important part is the action, not the contents.
          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;
          }
          
      ........
    }
    
    • 可以看到在该方法里如果 传递的 child 不是 null 他会直接调用的是子元素的 dispatchTouchEvent 方法来把事件传递给子元素
  • 再回到 遍历 ViewGroup 的所有子元素的方法中,可以看到在循环的最后过程中,判断如果子元素的 dispatchTouchEvent 返回 true ,那么这个 ViewGroup 就暂时不考虑事件在子元素内部是怎么分发的,而且 mFirstTouchTarget 就会被赋值同时跳出 for 循环,如下所示:
    代码位置

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
    
    • 在这里完成了mFirstTouchTarget的赋值并且终止了对子元素的遍历。其实对mFirstTouchTarget的赋值是在addTouchTarget方法里完成的:
      ViewGroup#addTouchTarget
       private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
         final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
         target.next = mFirstTouchTarget;
         mFirstTouchTarget = target;
         return target;
     }
    
    • mFirstTouchTarget 其实是一种单链表结构,他是否被赋值将直接影响到 ViewGroup 对事件的拦截策略,若果mFirstTouchTarget 为 null ,那么 ViewGroup 就默认拦截接下来同一序列中所有的点击事件
  • 如果遍历所有的子元素后事件都没有被合适处理,这里包含两种情况:

    • ViewGroup 没有子元素
    • 子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false (这一般是因为子元素在 onTouch中返回了false)
      在这两种情况下 ViewGroup 会自己处理点击事件:
    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
      // No touch targets so treat this as an ordinary view.
      handled = dispatchTransformedTouchEvent(ev, canceled, null,
      TouchTarget.ALL_POINTER_IDS);
     }
    

View对点击事件的分发过程

  • View 对点击事件的处理稍微简单点,首先看他的 dispatchTouchEvent 方法
    View#dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent event) {
      ......
      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;
      }
    }
    
    • 由于 View 不包含子元素,所以他无法传递事件只能自己处理。从上面的源码可以看出,View 对事件的处理首先会判断有没有设置 OnTouchListener如果设置了且 OnTouchListener中的 onTouch放回 true,那么 View 的 onTouchEvent 就不会被调用。由此可见 OnTouchListener 的优先级高于 onTouchEvent
  • 接着我们再看onTouchEvent的实现。先看当 View 处于不可用状态下的点击事件处理过程:
    View#onTouchEvent

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
       if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
           setPressed(false);
      }
       // A disabled view that is clickable still consumes the touch
       // events, it just doesn't respond to them.
       return (((viewFlags & CLICKABLE) == CLICKABLE ||
               (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    
    • 很明显:不可用状态下的 View 照样会消耗点击事件,尽管它看起来不可用
  • 接着onTouchEvent,如果 View 设置有代理,那么还会执行 TouchDelegateonTouchEvent方法

    if (mTouchDelegate != null) {
      if (mTouchDelegate.onTouchEvent(event)) {
          return true;
      }
    }
    
  • 再看onTouchEvent中对点击事件的具体处理:

     if (((viewFlags & CLICKABLE) == CLICKABLE ||
           (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
       switch (event.getAction()) {
           case MotionEvent.ACTION_UP:
               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) {
                       // The button is being released before we actually
                       // showed it as pressed.  Make it show the pressed
                       // state now (before scheduling the click) to ensure
                       // the user sees it.
                       setPressed(true, x, y);
                  }
    
                   if (!mHasPerformedLongPress) {
                        // 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 的 CLICKABLELONG_CLICKABLE 有一个为 true,那么他就会消耗这个事件,即 onTouchEvent 返回 true,不管他是不是 DISABLE 状态。然后在ACTION_UP发生时会触发 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);
          return result;
      }
    
    • 若果 View 设置了 OnClickListener,那么performClick() 方法内部会调用它的onClick方法

End. 到此就结束了

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

推荐阅读更多精彩内容