Android View 事件分发机制详解

Android开发,触控无处不在。对于一些 不咋看源码的同学来说,多少对这块都会有一些疑惑。View事件的分发机制,不仅在做业务需求中会碰到这些问题,在一些面试笔试题中也常有人问,可谓是老生常谈了。我以前也看过很多人写的这方面的文章,不是说的太戮褪翘:褂幸恍┰谙附谏闲吹囊灿姓椋试俅沃匦抡硪幌抡饪槟谌荩种尤媚愀忝靼View事件的分发机制。

说白了这些触控的事件分发机制就是弄清楚三个方法,dispatchTouchEvent(),OnInterceptTouchEvent(),onTouchEvent(),和这三个方法与n个ViewGroup和View堆叠在一起的问题,再复杂的结构都能拆分成1个ViewGroup+1个View。

其实ViewGroup和View都是大同小异,View只是没有了子容器,自然不存在拦截问题,dispatch也很简单,所以弄明白了ViewGroup其实就懂的差不多了。

三个关键方法

public boolean dispatchTouchEvent(MotionEvent ev)

View/ViewGroup处理事件分发的发起者,View/ViewGroup接收到触控事件最先调起的就是这个方法,然后在该方法中判断是否处理拦截或是将事件分发给子容器

public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup专用,通过该方法可以达到控件事件的分发方向,一般可以在该方法中判断将事件给ViewGroup独吞或是它继续传递给子容器,是处理事件冲突的最佳地点

public boolean onTouchEvent(MotionEvent event)

触控事件的真正处理者,最后每个事件都会在这里被处理

核心问题

时间分发机制的难点在哪,我觉得难的地方以下几点:三个方法调用规则,确定处理事件的对象以及事件冲突的解决方法。

事件传递规则

一般一次点击会有一系列的MotionEvent,可以简单分为:down->move->….->move->up,当一次event分发到ViewGroup时,上述三个方法之间的 ViewGroup中调用顺序可以用一段简单代码表示

MotionEvent ev;//down or move or up or others...
viewgroup.dispatchTouchEvent(ev);

public boolean dispatchTouchEvent(MotionEvent ev){
 boolean isConsumed = false;
  if(onInterceptTouchEvent(ev)){
   isCousumed = this.onTouchEvent(ev);
  }else{
   isConsumed = childView.dispatchTouchEvent(ev);
  }
  return isConsumed;
}

返回结果true表示事件被处理了,返回false表示没有处理。上面的代码通俗易懂,看起来也很简单,一句话就能概括,ViewGroup收到事件后调用dispatch,在dispatch中先检查是否要拦截,若拦截则ViewGroup吃掉事件,否则交给有处理能力的子容器处理。

不过,简单归简单,写成这样只是为了方便理解,ViewGroup的事件处理流程当然没这么简单,这里忽略了很多细节问题,接下来继续补充。回到上面说的,一系列事件我们经常处理的一般都是一个down,多个move和一个up,光靠上面的伪代码是没办法把这些问题都给完美解决,直接来看ViewGroup的dispatchTouchEvent。

onInterceptTouchEvent调用条件

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

解释一下上面的代码,看起来好像很简单,但真的很简单吗。。在解释之前先说一下intercepted代表的含义,intercepted == false表示父容器ViewGroup暂时不拦截事件,事件有机会传给子View处理,返回true表示父容器直接拦截了该系列事件,后续不会再传递给子View了。子View想获取事件只能让该值为false

onInterceptTouchEvent调用返回false(返回false才能传递给子View,对应到上面伪代码的else中的内容,叫事件传递到子容器需要满足的内容更好理解一些)需要满足两个条件中的任意一个就有可能触发(当然只是有可能):

一个是在down的时候,另一个就是mFirstTouchTarget!=null,那mFirstTouchTarget何时不为空,有兴趣的同学可以看ViewGroup中的addTouchTarget这个方法的调用时机,mFirstTouchTarget就是在这里赋值的,源码太长我就不贴了。

mFirstTouchTarget是用来保存ViewGroup中消费了ACTION_DOWN事件的子View,即在上面伪代码中child.dispatchTouchEvent(ev)在ACTION_DOWN的时候返回true的View,只要有子View的dispatch在ACTION_DOWN返回true,就不会为null(这个赋值过程只发生在ACTION_DOWN里,如果子ViewACTION__DOWN不给它赋值后面序列的事件就不会再),反之,若无子View处理,该对象即为null。当然,满足了上述两个条件还不行,必须还要满足!disallowIntercept。

disallowIntercept这个变量很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT这个标记影响,这个值可以被ViewGroup的子View设置,ViewGroup的子View如果调用了requestDisallowInterceptTouchEvent这个方法,会改变FLAG_DISALLOW_INTERCEPT,导致disallowIntercept这个值就是ture了,这种情况会跳过intercept,导致拦截失效。

但这事还没了,FLAG_DISALLOW_INTERCEPT这个标记有一个重置的机制,查看ViewGroup源码可以看到,在处理MotionEvent.ACTION_DOWN的时候会重置这个标记导致disallowIntercept失效,是不是丧心病狂,上面的一段这么简单的代码有这么多幺蛾子,这里还能得到一个结论,ACTION_DOWN的时候肯定可以执行onInterceptTouchEvent的。

所以拦截的intercepted很重要,能影响到底是让ViewGroup还子View处理这个事件。

上面的两个有可能触发拦截的条件说完了,那么当两个条件都不满足的话就不会再调用拦截了(拦截很重要,一般ViewGroup都返回false这样能把事件传递给子View,如果在ACTION_DOWN时不能走到OnInterceptTouchEvent并返回false告诉ViewGroup不要拦截,则事件再也不能传给子View了,所以拦截一般都是要走到的,而且一般都是返回false这样能让子View有机会处理),这种情况一般都是在ACTION_DOWN处理完之后没有子View当接盘侠消费ACTION_DOWN以及后续事件,从上面的伪代码可以看出来,这时候ViewGroup自己就很被动了,需要自己来调用onTouchEvent来处理,这锅就自己背了。

再继续说一下mFirstTouchTarget和intercepted是怎么影响事件方向的。看源码:

if (!canceled && !intercepted) {
....
if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 ....
 for(child : childList){
   if(!child satisfied condition....){
     continue;
   }
   newTouchTarget = addTouchTarget(child, idBitsToAssign);//在这里给mFirstTouchTarget赋值
 }

 }
}

可以在这里看到intercepted为false在ACTION_DOWN里才能给上面说过的mFirstTouchTarget赋值,只有mFirstTouchTarget不为空才能让后续事件传递给子View,否则根据上上面说的代码后续事件只能给父容器处理了。

mFirstTouchTarget就是我们后续事件传递的对象,很容易理解,如果在ACTION_DOWN中没有确定这个对象,则后续事件不知道传递给谁自然就交给父容器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为空,则ViewGroup调用super.dispatchTouchEvent(event),super是谁,ViewGroup继承自View,当然是View咯,View的dispatch调用的谁?当然是自己的onTouchEvent(后面会说),所以这个最后还是调用了ViewGroup自己的onTouchEvent。

那么当child!=null的时候呢,调用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup则继续按照上面的伪代码执行事件分发,如果也是View则调用自己的onTouchEvent。

所以,说到底事件到底给谁处理,还是和传进来的child有关,那这个方法在哪里调用的呢,继续看:

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

这就是为什么mFirstTouchTarget能影响事件分发的方向的原因。就这样,整个伪代码的流程是不是很清楚了。

这里需要多说两句,在上上面代码流程中,intercepted决定了这个事件会不会调用ViewGroup的onTouchEvent,当intercepted为true则后续流程会调用ViewGroup的onTouchEvent,仔细看上面的代码能发现,只有两种情况为ture:一是调用了InterceptTouchEvent把事件拦截下来,另一个就是没有一个子View能够消费ActionDown。只有这两种情况父容器ViewGroup才会自己处理
那么问题来了,思考一个问题:如果子View处理了ACTION_DOWN但后续事件都返回false,这些没有被处理的事件最后传给谁处理了?各位思考之,后面再说这个问题。

孩子是谁的

继续来扩展我们的伪代码,拦截条件判断完之后,决定把事件继续传递给子View的时候,会调用childView.dispatchTouchEvent(ev),问题来了,child是哪来的,继续看源码

if (!canViewReceivePointerEvents(child)
  || !isTransformedTouchPointInView(x, y, child, null)) {
   ev.setTargetAccessibilityFocus(false);
   continue;
}

ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。

再看伪代码,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,说明ViewGroup处理了这个点击事件(ViewGroup自身或者子View处理的),并且这个系列的点击事件会继续传到这个ViewGroup来处理,若isConsume == false(ACTION_DOWN时),ViewGroup没办法处理这个点击事件,那么这个系类的点击事件就和该ViewGroup无缘了。会把这个事件上抛给自己的父容器或者Activity处理。

伪代码说完了,ViewGroup的事件传递规则也就差不多说完了,这么看是不是很简单了。View相对于ViewGroup来说就更简单了,没有拦截方法,dispatch基本上是直接调用了自身的onTouchEvent,处理起来一点难度都木有呀。

一些没说到但也很重要的点

上面解释的东西都很简单,是从一个ViewGroup+一个View开始的,事件分发的执行者是ViewGroup,子容器也只有一个View,但实际开发中当然没这么简单,不过不要怕,再复杂的情况也能够拆分成这种模式的,只不过层次多了一些递归复杂了一些而已,原理还是一样的。

顺带补充几点:

从用户点击屏幕开始触发一个系列的点击事件时,事件真正的传递流程是:Activity(PhoneWindow)->DecorView->ViewGroup->View,在到达ViewGroup之前还有一个DecorView,事件是从Activity传过来的,但这些东西其实和ViewGroup的原理是一样的,Activity能看做一个大的ViewGroup,当它的DecorView包含的所有子View没有人能够消耗事件的时候(这样说有漏洞,大家懂我的意思就行了)最后还是会交给Activity处理。

事件冲突解决可以按照上面的原理在几个point中进行处理。最容易想到的处理的时机是在onInterceptTouchEvent里,比如当一个竖直方向滑动的ViewGroup里嵌套一个横向滑动的ViewGroup,可以在这里的ACTION_MOVE里来判断后续事件应该传递给谁处理,当然,也可以根据上面说的标记位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent来控制事件的流向,这都是比较容易想到的,不过看过别的大神,通过分享MotionEvent的方法来控制事件的流向,即在父容器中保存MotionEvent并在适当的时机传入子View自定义的事件处理方法来分享事件,也是可行的。

任何View只要拒绝了一系列事件中的ACTION_DOWN(返回false),则后续事件都不会再传递过来了。但如果拒绝了其他的事件,后续事件还是可以传过来的,比如View某次ACTION_MOVE没处理,这个没处理的事件最后会被Activity消耗掉(而不是View的父容器),但后续的事件还是会继续传给该View。

合理的利用ACTION_CANCEL能够控制一个系列事件的生命周期,让事件处理更加灵活。

理解事件分发的机制只要明白上面的原理基本就够用了,github上很多牛逼的大神写的各种炫酷的自定义控件的事分发根据这些也能够看明白,当然还有很多扩展的东西和更深入的内容由于篇幅的关系在这里就不罗嗦了,更重要的还是去看源码吧。
最后送各位一句经典:纸上得来终觉浅,绝知此事要躬行!

以上就是对Android View事件分发机制的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!

代码技巧

转载请关注公众号:代码技巧 回复:授权

本文链接地址:https://www.oudahe.com/p/53384/