View事件分发机制

1.事件在DecorView中的分发过程
ViewRootImpl在接收到输入事件后最终会调用DecorView的dispatchPointerEvent,由于DecorView没有重写dispatchPointerEvent,所以调用的是View的dispatchPointerEvent,而View 的dispatchPointerEvent又中会调用dispatchTouchEvent,这个方法在DecorView中被重写了,代码如下:

// DecorView
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

上述代码中的Callback即为Activity。也就是说事件分发最开始是传递给 DecorView 的,DecorView 的 dispatchTouchEvent 是传给 Window.Callback接口方法 dispatchTouchEvent,而 Activity 实现了 Window.Callback 接口。
紧接着Activity 的 dispatchTouchEvent方法里,是调到 Window 的 superDispatchTouchEvent,

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

Window 的唯一实现类 PhoneWindow 又会把这个事件回传给 DecorView,DecorView 在它的 superDispatchTouchEvent 把事件转交给了 ViewGroup。

    // PhoneWindow
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    // DecorView
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

至此,触摸事件正式进入ViewGroup中,开启事件的分发流程。总结一下事件分发的流程是:

DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup -> View

不难看出,事件的传递过程是一个典型的责任链模式。
2.事件分发涉及到的三个方法
dispatchTouchEvent 事件从Activity经Window,最终会调用到DecorView(即ViewGroup)的dispatchTouchEvent。这个方法的作用是进行事件分发,只要事件能到达ViewGroup那么dispatchTouchEvent方法必定会被调用。dispatchTouchEvent方法返回一个boolean值,代表事件是否被消费。
onInterceptTouchEvent 该方法是ViewGroup独有的方法,在ViewGroup的dispatchTouchEvent中被调用,返回一个boolean值,用来判断当前ViewGroup是否要拦截事件。如果返回true,则表示该ViewGroup要拦截事件。事件交由ViewGrroup处理。
onTouchEvent onTouchEvent方法会在dispatchTouchEvent中调用,作用是处理事件。返回结果表示是否消费当前事件。
View的dispatchTouchEvent事件
以上三个方法的关系可以用以下伪代码表示:
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event);
} else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
通过上边的伪代码想要完整的了解事件分发是不现实的。想要完整的理解事件分发就必须深入到View跟ViewGroup内部一探究竟。

3.View中对事件的处理
View中dispatchTouchEvent方法实现逻辑比较简单,简化后代码如下:

 public boolean dispatchTouchEvent(MotionEvent event) {

        //... 省略了对滑动等部分的处理逻辑代码
        boolean result = false;
        // 如果View是enable状态,并且设置了TouchListener,则调用TouchListener的onTouch,
        // 如果onTouchEvent方法返回true,则将true作为dispatchTouchEvent的返回值,表示事件被当前View消费掉了
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        // 如果View是disable状态或者没有设置TouchListener或者TouchListener的onTouch方法反回了false
        // 那么就调用View自身的onTouchEvent来处理事件,onTouchEvent返回true表示当前View消费了事件。
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    return result;
    }

View中的onTouchEvent方法会对事件进行兜底处理,比如在ACTION_UP中调用performClick方法,进而执行View的OnClick方法

public boolean onTouchEvent(MotionEvent event) {
     // 判断View是否是可以点击状态,即View设置了clickable或者longClickable或者contextClickable
     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:
           // 伪代码,这里省略了所有其他逻辑
           performClick();

           break;
        }
    }
}

public boolean performClick() {

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    // 如果设置了OnClickListener,则调用OnClickListener的onClick方法
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    return result;
}

4.ViewGroup中对事件的分发与处理

首先由于ViewGroup是继承View的,那么它的dispatchTouchEvent有两条路可以走,
1)可以调用super.dispatchTouchEvent
如果调用super.dispatchTouchEvent意味着调用了View的dispatchTouchEvent中的逻辑,即事件交由自身处理。
2)调用自身的dispatchTouchEvent。
如果调用自身的dispatchTouchEvent,即走事件的分发流程,向下分发事件。
至于这两条路是如何选择的,就要详细分析ViewGroup中的dispatchTouchEvent方法了。下面贴一下dispatchTouchEvent简化后的代码:

public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;
        // ACTION_DOWN事件认为是时间序列分发的开始
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 重置mFirstTouchTarget状态为null
            cancelAndClearTouchTargets(ev);
            // 重置是否允许该ViewGroup拦截事件的标记,意味着disallowIntercept对DOWN事件无效
            resetTouchState();
        }
    // (1)如果是DOWN事件,条件一定成立,先询问自身是否要拦截事件
    // (2)mFirstTouchTarget不为null,说明有子View消费事件
        if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
               // 是有有子View禁止当前ViewGroup拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;              
                if (!disallowIntercept) {
                    // 如果没有子View禁止当前ViewGroup拦截事件,则询问自身是否拦截事件
                    intercepted = onInterceptTouchEvent(ev);
                } else {
                    // 子View禁止了ViewGroup拦截事件,当前ViewGroup不拦截
                    intercepted = false;
                }
        } else {
            // 能走到此处说明一定不是DOWN事件。
            // 且mFirstTouchTarget为null,即没有子View消费事件
            intercepted = true;
        }

        TouchTarget newTouchTarget = null;
        // 事件是否已经分发过了的标记,用来避免后边重复分发
        boolean alreadyDispatchedToNewTouchTarget = false;

        // 注意此处的条件!!!!,此处的intercepted是理解事件分发的关键因素,根据上边分析的条件思考一下,
        // intercepted什么时候是false,什么时候是true? 可以来分情况讨论:
        // (1)如果是ACTION_DOWN事件,并且拦截了ACTION_DOWN事件,则intercepted为true,否则为false
        // (2)如果非ACTION_DOWN事件,且mFirstTouchTarget不为null,拦截了事件则为true,否则为false
        // (3)如果不是ACTION_DOWN事件,且mFirstTouchTarget为null,那么intercepted一定为true。
        //    此时会跳过此处的if语句,直接将事件交给自身处理。之所以会出现这种情况是因为前面拦截了
        //     ACTION_DOWN事件,致使mFirstTouchTarget无法被赋值。
        // 对上边的三种情况以此执行下边的代码,分析会有什么样的结果
        if (!intercepted) {
            // 别管前面的条件如何,能走到这里来,意味着当前ViewGroup不拦截事件,即走上述的第二条路,向子View分发事件  

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 注意此处,只有ACTION_DOWN事件才能进来

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {

                    final View[] children = mChildren;
                    // 遍历子View,查找消费事件的View/ViewGroup
                    for (int i = childrenCount - 1; i >= 0; i--) {
                      // 按顺序查找子View
                      final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                     // 检查子View是否能接收事件,即事件的坐标在子View内,并且View没有在执行动画
                     if (!child.canReceivePointerEvents()
                              || !isTransformedTouchPointInView(x, y, child, null)) {
                          ev.setTargetAccessibilityFocus(false);
                          // 不满足条件,则跳过该子View,继续下一个子View
                          continue;
                      }   
                      // 代码能执行到此处说明已经查找到了符合条件的子View

                      // 查找到目标View之后,通过dispatchTransformedTouchEvent开始向子View分发事件,
                      // 即可以理解为调用child的dispatchTouchEvent方法,并将child的dispatchTouchEvent返回值返回
                      // 那么dispatchTransformedTouchEvent的返回值就意味着子View是否消费了事件。                      
                      if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 走到这里说明子View中消费了事件,那么就将该View保存到mFirstTouchTarget中
                            // 该保存操作是在addTouchTarget方法中完成
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 将alreadyDispatchedToNewTouchTarget置为true,标记已经分发了事件
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            }
        }

        // mFirstTouchTarget为null,说明没有子View消费事件
        if (mFirstTouchTarget == null) {
            // 调用dispatchTransformedTouchEvent方法,注意这里child的参数是null,
            // 即走上述中的第一条路,调用super.dispatchTouchEvent方法把事件交给自身处理
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 分析什么情况下能走到此处?该ViewGroup没有拦截ACTION_DOWN事件,并且DOWN事件中遍历子View找到了消费事件的子View,
            // 那么此时会为mFirstTouchTarget赋值,并且结束ACTION_DOWN事件的分发流程(注意,此时DOWN事件不会执行到此处的代码)
            // 接下来会有一系列的MOVE事件,如果此时没有拦截ACTION_MOVE事件,那么ACTION_MOVE事件就会跳过上边的if语句,即跳过事件分发的流程
            // 直接将MOVE事件分发给mFirstTouchTarget中的child,即跳过了遍历子View查找消费事件View的流程,节省了性能。
            TouchTarget target = mFirstTouchTarget;
            // 注意到此处是一个While循环,因为TouchTarget是一个链表,由于多指触控的支持,会形成了一个TouchTarget链表
            // 多个TouchTarget代表就有多个触控点,因此,此处会循环遍历链表,然后将多个触控点分发到child View.
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    // TouchTarget中的child成员才是真正消费事件的View,调用dispatchTransformedTouchEvent将事件交给child.        
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                }
                target = next;
            }
        }
    // 如果子View或者自身消费了事件则返回true,否则返回false
    return handled;
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        // dispatchTransformedTouchEvent的大致逻辑是这样的,这里简化了代码,理解就好。
        if (child == null) {
            // 调用自身的dispatchTouchEvent方法
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            // 调用子View的dispatchTouchEvent方法
            handled = child.dispatchTouchEvent(transformedEvent);
        }
}

5. ACTION_CANCEL事件的处理

上述对于ViewGroup的dispatchTouchEvent方法的分析忽略了ACTION_CANCEL的处理。ACTION_CANCEL的调用时机是什么时候?看一个具体的例子。
在一个自定义的ViewGroup里边嵌套一个Button。自定义的ViewGroup重写onInterceptTouchEvent,并拦截一次ACTION_MOVE事件。

class CustomViewGroup @JvmOverloads constructor(
  context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
  var hasInterceptMoveEvent = false
  override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
      // 只拦截一次ACTION_MOVE事件
      if (!hasInterceptMoveEvent && ev?.action == MotionEvent.ACTION_MOVE) {
          hasInterceptMoveEvent = true
          return true
      }
      return false
  }

  override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
      // 打印事件
      Log.d("TAG", "${getAction(ev?.action)}:CustomViewGroup dispatchTouchEvent")
      return super.dispatchTouchEvent(ev)

  }
}  

接下来自定义一个Button,并在Button中的dispatchTouchEvent方法中打印事件,如下:

class CusButton @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Button(context, attrs, defStyleAttr) {
    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        Log.d("TAG", "${getAction(event?.action)}:CusButton dispatchTouchEvent")
        return true
    }
}

在布局文件中用CustomViewGroup嵌套一个CusButton,接下来,手指按住Button,然后移动,直到手指移除Button外,然后再松开。

可以看到Button收到了一个ACTION_CANCEL事件。在收到这个ACTION_CANCEL事件之后,Button就再也没有收到其它事件。

接下来修改一下上边的例子,CustomViewGroup中不再拦截ACTION_MOVE事件,其它保持不变。
即Button的ACTION_CANCEL事件没有被回调,且抬起手指时CusButton收到了ACTION_UP。

可以总结一下,如果一个子View处理了DOWN事件,那么随之而来的MOVE事件及UP事件也会交给这个View处理。但是在交给它处理之前父View是可以拦截这个事件的。如果父View拦截了这个事件,那么这个子View就会收到一个CANCEL事件。并且后续大MOVE与UP事件都不会再传递给这个View。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {  
  boolean handled = false;
  final int action = ev.getAction();
  final int actionMasked = action & MotionEvent.ACTION_MASK;

  final boolean intercepted;
  if (actionMasked == MotionEvent.ACTION_DOWN
      || mFirstTouchTarget != null
  ) {
     // 此处,如果子View没有禁止该View拦截事件,则调用onInterceptTouchEvent方法看自身是否要拦截。
      final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
      if (!disallowIntercept) {
          // 如果拦截了MOVE事件,那么intercepted为true
          intercepted = onInterceptTouchEvent(ev);
          ev.setAction(action); 
      } else {
          intercepted = false;
      }
  } else {
      intercepted = true;
  }
  final boolean canceled = resetCancelNextUpFlag(this)
          || actionMasked == MotionEvent.ACTION_CANCEL;

  TouchTarget newTouchTarget = null;
  boolean alreadyDispatchedToNewTouchTarget = false;
  // intercepted为true,则这个if语句不会被执行,直接跳过
  if (!canceled && !intercepted) {
      // ...
  }

  if (mFirstTouchTarget == null) {
      // mFirstTouchTarget不为空,所以不会走到这里来
  } else {
      // intercepted为true时会执行到这里,这里显示是对mFirstTouchTarget链表的遍历
      TouchTarget predecessor = null;
      TouchTarget target = mFirstTouchTarget;
      while (target != null) {
          final TouchTarget next = target.next;
          if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
              // 事件已经分发到了新的TouchTarget
              handled = true;
          } else {
              // 正常情况这个MOVE事件应该走到这里来,此时由于intercepted为true,那么cancelChild就一定为true
              final boolean cancelChild = resetCancelNextUpFlag(target.child)
                      || intercepted;
              // 看dispatchTransformedTouchEvent源码,此时cancelChild为true
              if (dispatchTransformedTouchEvent(
                      ev, cancelChild,
                      target.child, target.pointerIdBits
                  )
              ) {
                  handled = true;
              }

              // CANCEl事件交给子View之后,这里判断如果子View被cancel了,那么就将这个View从mFirstTouchTarget链表中移除
              // 意味着后续的MOVE事件与UP事件这个View就再也不会收到
              if (cancelChild) {
                  if (predecessor == null) {
                      mFirstTouchTarget = next;
                  } else {
                      predecessor.next = next;
                  }
                  target.recycle();
                  target = next;
                  continue;
              }
          }
          predecessor = target;
          target = next;
      }
  }

  return handled;
}  
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

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

推荐阅读更多精彩内容