android事件分发机制

来源:互联网 发布:商品房 网络拓扑结构图 编辑:程序博客网 时间:2024/05/22 15:37

猜想:当触摸到一个控件,首先触摸事件找到到最上层的dispatchTouchEvent方法(事件是通过dispatchTouchEvent方法分发的),然后触摸事件从上往下分发,在分发期间上层可以截断对下层的分发,如果没有截断,最下层会接受并处理触摸事件,处理完后会选择继续处理还是交给上层的处理。
当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。

一、view的事件分发
首先举一个关于ontouch()和onClick()的例子,我们如果给一个button设置这两个方法,当我们触摸的时候首先是调用该控件的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {      if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&              mOnTouchListener.onTouch(this, event)) {          return true;      }      return onTouchEvent(event);  }  

这个方法比较关键,在if语句里首先检查控件是否设置了ontouch的监听事件,判断该控件是否是enable(表示当前视图是否可用。可以调用setEnable()方法来改变视图的可用状态,传入true表示可用,传入false表示不可用。它们之间最大的区别在于,不可用的视图是无法响应onTouch事件的。),如果前两个条件成立,则会调用控件的ontouch方法(如果在ontouch方法中返回false,则不会拦截事件,Action_down之外的其他事件则不会监听到,但是在ontouchevent方法中会判断是否点击了控件,如果该控件是可以点击的则会返回true。简而言之就是可以点击的控件的ontouch方法中即使返回false,也还是会监听到一系列的动作,并且在Action_up后执行onClick方法;如果是不可点击的控件在返回false后则不会监听到一系列的动作,比如ImageView),这里的三个条件有一个是false就会调用onTouchEvent方法,如果都是true则返回true,并且触发下一个Action,onTouchEvent方法不会得到执行,当然onTouchEvent方法中的点击事件也不会执行;
还有一个很重要的知识点需要说明,就是touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

前面说道由于ImageView是默认不可点击的,所以在ontouch方法中返回false后就不会接收到其他的Action,那么有什么办法可以让他和button一样呢?
其实很简单,只需要在布局文件里面给ImageView增加一个android:clickable=”true”的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。
还有很重要的一点:给一个控件设置点击事件时,都会自动设置该控件为可点击:clickable=”true”,如果给ImageView设置了点击事件,就不需要在布局文件里面给ImageView增加一个android:clickable=”true”的属性;

二、ViewGroup的事件分发
ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是Android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。

ViewGroup中都会有一个onInterceptTouchEvent方法来决定是否拦截事件的传递,返回true表示拦截,反之不拦截,如果你重写一个布局文件的onInterceptTouchEvent方法并且返回true,则这个布局文件中的所有控件都接收不到触摸事件。
现在我们来看一下ViewGroup最重要的方法dispatchTouchEvent方法的源码:

public boolean dispatchTouchEvent(MotionEvent ev) {      final int action = ev.getAction();      final float xf = ev.getX();      final float yf = ev.getY();      final float scrolledXFloat = xf + mScrollX;      final float scrolledYFloat = yf + mScrollY;      final Rect frame = mTempRect;      boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;      if (action == MotionEvent.ACTION_DOWN) {          if (mMotionTarget != null) {              mMotionTarget = null;          }          if (disallowIntercept || !onInterceptTouchEvent(ev)) {              ev.setAction(MotionEvent.ACTION_DOWN);              final int scrolledXInt = (int) scrolledXFloat;              final int scrolledYInt = (int) scrolledYFloat;              final View[] children = mChildren;              final int count = mChildrenCount;              for (int i = count - 1; i >= 0; i--) {                  final View child = children[i];                  if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE                          || child.getAnimation() != null) {                      child.getHitRect(frame);                      if (frame.contains(scrolledXInt, scrolledYInt)) {                          final float xc = scrolledXFloat - child.mLeft;                          final float yc = scrolledYFloat - child.mTop;                          ev.setLocation(xc, yc);                          child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;                          if (child.dispatchTouchEvent(ev))  {                              mMotionTarget = child;                              return true;                          }                      }                  }              }          }      }      boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||              (action == MotionEvent.ACTION_CANCEL);      if (isUpOrCancel) {          mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;      }      final View target = mMotionTarget;      if (target == null) {          ev.setLocation(xf, yf);          if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {              ev.setAction(MotionEvent.ACTION_CANCEL);              mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;          }          return super.dispatchTouchEvent(ev);      }      if (!disallowIntercept && onInterceptTouchEvent(ev)) {          final float xc = scrolledXFloat - (float) target.mLeft;          final float yc = scrolledYFloat - (float) target.mTop;          mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;          ev.setAction(MotionEvent.ACTION_CANCEL);          ev.setLocation(xc, yc);          if (!target.dispatchTouchEvent(ev)) {          }          mMotionTarget = null;          return true;      }      if (isUpOrCancel) {          mMotionTarget = null;      }      final float xc = scrolledXFloat - (float) target.mLeft;      final float yc = scrolledYFloat - (float) target.mTop;      ev.setLocation(xc, yc);      if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {          ev.setAction(MotionEvent.ACTION_CANCEL);          target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;          mMotionTarget = null;      }      return target.dispatchTouchEvent(ev);  }  

如果我们点击了ViewGroup中的一个子view:
首先在第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。由此可以看出是否拦截触摸事件由这两个参数决定,disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。

如果进入到条件判断中就会调用子view的dispatchTouchEvent,然后就和view的事件分发一样了。

然后需要注意一下,调用子View的dispatchTouchEvent后是有返回值的。我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true。因此会导致第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,也是印证了我们前面的Demo打印的结果,如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。

那如果我们点击的不是按钮,而是空白区域呢?这种情况就一定不会在第31行返回true了,而是会继续执行后面的代码。那我们继续往后看,在第44行,如果target等于null,就会进入到该条件判断内部,这里一般情况下target都会是null,因此会在第50行调用super.dispatchTouchEvent(ev)。这句代码会调用到哪里呢?当然是View中的dispatchTouchEvent方法了,因为ViewGroup的父类就是View。之后的处理逻辑又和前面所说的是一样的了,也因此MyLayout中注册的onTouch方法会得到执行。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:
这里写图片描述

  1. Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
  2. 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
  3. 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

如果onInterceptTouchEvent返回了true,那么本次touch事件之后的所有action都不会再向深层的View传递,统统都会传给负层View的onTouchEvent,就是说父层已经截获了这次touch事件,之后的action也不必询问onInterceptTouchEvent,在这次的touch事件之后发出的action时onInterceptTouchEvent不会再次调用,知道下一次touch事件的来临。如果onInterceptTouchEvent返回false,那么本次action将发送给更深层的View,并且之后的每一次action都会询问父层的onInterceptTouchEvent需不需要截获本次touch事件。(在侧滑菜单中主布局重写onInterceptTouchEvent方法,在里面加入,如果点击按钮后移动了,就截获本次touch事件交给布局)

 @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        if(ev.getAction()==MotionEvent.ACTION_MOVE){            return true;        }        return false;    }

参考自:
http://blog.csdn.net/guolin_blog/article/details/9097463
http://blog.csdn.net/guolin_blog/article/details/9153747
http://blog.csdn.net/chaihuasong/article/details/17499799

0 0
原创粉丝点击