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事件分发机制的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!

时间: 2016-08-30

Android View事件分发机制详解

准备了一阵子,一直想写一篇事件分发的文章总结一下,这个知识点实在是太重要了. 一个应用的布局是丰富的,有TextView,ImageView,Button等,这些子View的外层还有ViewGroup,如RelativeLayout,LinearLayout.作为一个开发者,我们会思考,当点击一个按钮,Android系统是怎样确定我点的就是按钮而不是TextView的?然后还正确的响应了按钮的点击事件.内部经过了一系列什么过程呢? 先铺垫一些知识能更加清晰的理解事件分发机制: 1. 通过setC

Android事件分发机制(下) View的事件处理

综述 在上篇文章Android中的事件分发机制(上)--ViewGroup的事件分发中,对ViewGroup的事件分发进行了详细的分析.在文章的最后ViewGroup的dispatchTouchEvent方法调用dispatchTransformedTouchEvent方法成功将事件传递给ViewGroup的子View.并交由子View进行处理.那么现在就来分析一下子View接收到事件以后是如何处理的. View的事件处理 对于这里描述的View,它是ViewGroup的父类,并不包含任何的子元

Android View事件分发和消费源码简单理解

Android View事件分发和消费源码简单理解 前言: 开发过程中觉得View事件这块是特别烧脑的,看了好久,才自认为看明白.中间上网查了下singwhatiwanna粉丝的读书笔记,有种茅塞顿开的感觉. 很重要的学习方法:化繁为简,只抓重点. 源码一坨,不要指望每一行代码都看懂.首先是没必要,其次大量非关键代码会让你模糊真正重要的部分. 以下也只是学姐的学习成果,各位同学要想理解深刻,还需要自己亲自去看源码. 2.源码分析 由于源码实在太长,而且也不容易看懂,学姐这里就不贴出来了,因为没必

Android Touch事件分发深入了解

本文带着大家深入学习触摸事件的分发,具体内容如下 1. 触摸动作及事件序列 (1)触摸事件的动作 触摸动作一共有三种:ACTION_DOWN.ACTION_MOVE.ACTION_UP.当用户手指接触屏幕时,便产生一个动作为ACTION_DOWN的触摸事件,此时若用户的手指立即离开屏幕,会产生一个动作为ACTION_UP的触摸事件:若用户手指接触屏幕后继续滑动,当滑动距离超过了系统中预定义的距离常数,则产生一个动作为ACTION_MOVE的触摸事件,系统中预定义的用来判断用户手指在屏幕上的滑动是

Android事件分发机制(上) ViewGroup的事件分发

综述 Android中的事件分发机制也就是View与ViewGroup的对事件的分发与处理.在ViewGroup的内部包含了许多View,而ViewGroup继承自View,所以ViewGroup本身也是一个View.对于事件可以通过ViewGroup下发到它的子View并交由子View进行处理,而ViewGroup本身也能够对事件做出处理.下面就来详细分析一下ViewGroup对时间的分发处理. MotionEvent 当手指接触到屏幕以后,所产生的一系列的事件中,都是由以下三种事件类型组成.

Android 事件分发详解及示例代码

事件分发是Android中非常重要的机制,是用户与界面交互的基础.这篇文章将通过示例打印出的Log,绘制出事件分发的流程图,让大家更容易的去理解Android的事件分发机制. 一.必要的基础知识 1.相关方法 Android中与事件分发相关的方法主要包括dispatchTouchEvent.onInterceptTouchEvent.onTouchEvent三个方法,而事件分发一般会经过三种容器,分别为Activity.ViewGroup.View.下表对这三种容器分别拥有的事件分发相关方法进行

Android View的事件分发机制

一.Android View框架提供了3个对事件的主要操作概念. 1.事件的分发机制,dispatchTouchEvent.主要是parent根据触摸事件的产生位置,以及child是否愿意负责处理该系列事件等状态,向其child分发事件的机制. 2.事件的拦截机制,onInterceptTouchEvent.主要是parent根据它内部的状态.或者child的状态,来把事件拦截下来,阻止其进一步传递到child的机制. 3.事件的处理机制,onTouchEvent.主要是事件序列的接受者(可以是

Android事件分发机制的详解

Android事件分发机制 我们只考虑最重要的四个触摸事件,即:DOWN,MOVE,UP和CANCEL.一个手势(gesture)是一个事件列,以一个DOWN事件开始(当用户触摸屏幕时产生),后跟0个或多个MOVE事件(当用户四处移动手指时产生),最后跟一个单独的UP或CANCEL事件(当用户手指离开屏幕或者系统告诉你手势(gesture)由于其他原因结束时产生).当我们说到"手势剩余部分"时指的是手势后续的MOVE事件和最后的UP或CANCEL事件. 在这里我也不考虑多点触摸手势(我

谈谈对Android View事件分发机制的理解

最近因为项目中用到类似一个LinearLayout中水平布局中,有一个TextView和Button,然后对该LinearLayout布局设置点击事件,点击TextView能够触发该点击事件,然而奇怪的是点击Button却不能触发.然后google到了解决办法(重写Button,然后重写其中的ontouchEvent方法,且返回值为false),但是不知道原因,这两天看了几位大神的博客,然后自己总结下. public class MyButton extends Button { private

android事件分发机制的实现原理

android中的事件处理,以及解决滑动冲突问题都离不开事件分发机制,android中的事件流,即MotionEvent都会经历一个从分发,拦截到处理的一个过程.即dispatchTouchEvent(),onInterceptEvent()到onTouchEvent()的一个过程,在dispatchTouchEvent()负责了事件的分发过程,在dispatchTouchEvent()中会调用onInterceptEvent()与onTouchEvent(),如果onInterceptEven

解析Android点击事件分发机制

开头说说初衷 网上关于点击事件分发的文章一搜一大堆,标题一看,不是"30分钟让你弄明白XXX"就是"这是讲解XXX最好的文章",满怀憧憬与信心,忍不住兴奋的点进去一看,发现不是代码就全是图,我基本上看完了所有相关的文章,结果硬是看了三个小时也没搞懂.所以最后还是决定自己去试一试,看一看点击事件分发到底是怎么个流程,我写的肯定不会比其他文章好多少,但是呢,带着一个初学者的心,去分析这个东西,自己能弄明白的同时,也让想学习这个的人看了之后有些许收获,那就足够了. 运行的

Android从源码的角度彻底理解事件分发机制的解析(上)

其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识.也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用ImageView?等等--对于这些问题,我并没有给出非常详细的回答,因为我知道如果想要彻底搞明白这些问题,掌握Android事件分发机

Android从源码的角度彻底理解事件分发机制的解析(下)

记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了. 还未阅读过的朋友,请先参考Android从源码的角度彻底理解事件分发机制的解析. 那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGroup的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是And