Android:View事件分发机制详解

来源:互联网 发布:矩阵有什么用 编辑:程序博客网 时间:2024/06/09 19:45

Android事件传递机制绝对不是三言两语就能说得清的,在网上查了相关资料,觉得大部分都没有讲的很清楚透彻,写本文的目的就是让更多的开发者进从FrameWork层到Application层一步步深入Android事件传递机制的原理,今天先小小试牛刀,主要是讲View的事件传递机制原理,下一篇会将更复杂的控件ViewGroup事件的传递机制。

Android事件构成

在Android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了Android中的事件响应。总的来说,所有的事件都由如下三个部分作为基础:

  • 按下(ACTION_DOWN)
  • 移动(ACTION_MOVE)
  • 抬起(ACTION_UP)

所有的操作事件首先必须执行的是按下操作(ACTIONDOWN),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(ACTIONMOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。这一系列的动作在Android中都可以进行控制。

我们知道,所有的事件操作都发生在触摸屏上,而在屏幕上与我们交互的就是各种各样的视图组件(View),在Android中,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。所以,我们的事件操作主要就是发生在View和ViewGroup之间,那么View和ViewGroup中主要有哪些方法来对这些事件进行响应呢?记住如下3个方法,我们通过查看View和ViewGroup的源码可以看到:

View.java

  • public boolean dispatchTouchEvent(MotionEvent event)
  • public boolean onTouchEvent(MotionEvent event)

ViewGroup.java

  • public boolean dispatchTouchEvent(MotionEvent event)
  • public boolean onTouchEvent(MotionEvent event)
  • public boolean onInterceptTouchEvent(MotionEvent event)

在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,但是在ViewGroup中还有一个onInterceptTouchEvent方法,那这些方法都是干嘛的呢?别急,我们先看看他们的返回值。这些方法的返回值全部都是boolean型,为什么是boolean型呢,看看本文的标题,“事件传递”,传递的过程就是一个接一个,那到了某一个点后是否要继续往下传递呢?你发现了吗,“是否”二字就决定了这些方法应该用boolean来作为返回值。没错,这些方法都返回true或者是false。在Android中,所有的事件都是从开始经过传递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。

接下来就是这些方法的参数,都接受了一个MotionEvent类型的参数,MotionEvent继承于InputEvent,用于标记各种动作事件。之前提到的ACTIONDOWN、ACTIONMOVE、ACTION_UP都是MotinEvent中定义的常量。我们通过MotionEvent传进来的事件类型来判断接收的是哪一种类型的事件。到现在,这三个方法的返回值和参数你应该都明白了,接下来就解释一下这三个方法分别在什么时候处理事件。

  • dispatchTouchEvent方法用于事件的分发,Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件没有被消费。返回false则继续往下分发,如果是ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。
  • onTouchEvent方法用于事件的处理,返回true表示消费处理当前事件,返回false则不处理,交给子控件进行继续分发。
  • onInterceptTouchEvent是ViewGroup中才有的方法,View中没有,它的作用是负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的onTouchEvent进行处理。返回false则不拦截,继续往下传。这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而在Android中View中是不能再包含子View的(iOS可以)。

Android事件处理

比如一个Activity页面有一个Button 按钮,要想为该按钮设置onClick事件,只需简单的使用下面几句代码即可:

mTestButton.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View view) {                 Log.d(TAG, "onClick execute");             }         });  

有了上面click事件,点击Button时就会执行上述onClick方法中的具体实现,这个我们都知道,但是如果我再为button添加一个OnTouchListener,代码实现也很简单,如下:

mTestButton.setOnTouchListener(new View.OnTouchListener() {             @Override             public boolean onTouch(View view, MotionEvent motionEvent) {                 Log.d(TAG, "onTouch execute, action event " + motionEvent.getAction());                 return false;             }         }); 

此时,我们现在分析一下,是onTouch先执行,还是onClick执行,我想一般人都能立即回答出,肯定是onTouch事件先执行,但是为什么会这样呢?

其中的原理是什么,接下来我从FrameWork 源码去探寻一下整个事件的执行流程和原理:
我们知道Button ,TextView等基础控件的基类都是View,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类(实际上是基类View)里的dispatchTouchEvent方法,所以接下来看View源码中dispatchTouchEvent()方法的具体实现

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

分析上述代码,第2行如果三个条件都为真的话,就返回true,否则执行onTouchEvent:
第一个条件mOnTouchListener!=null,这个条件就是如果设置了OnTouchListener就会为true,否则是false;
第二个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true;
第三个条件就比较复杂了,mOnTouchListener.onTouch(this, event),这个其实就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。

在 onTouch(View v, MotionEvent event)中会处理一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP.
该onTouch()方法返回true表示事件已经消耗,返回false表示事件未消耗.就会再去执行onTouchEvent(event)方法。
比如在处理ACTION_DOWN时返回true才会继续分发ACTION_MOVE事件
比如在处理ACTION_MOVE时返回true才会继续分发ACTION_UP事件
比如在处理ACTION_DOWN时返回false,那么后续的ACTION_MOVE,ACTION_UP就不会再继续分发.
我们在代码中也就无法捕捉到ACTION_MOVE,ACTION_UP这两个Action了.

从该dispatchTouchEvent()的源码也可以看出
onTouch(this,event)和 onTouchEvent(event)的区别和关系:
1 先调用onTouch()后调用onTouchEvent()
2 在onTouch()方法中处理了Touch事件,即处理一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP事件
, 返回false时表示事件(每个单独的ACTION_DOWN,ACTION_MOVE,ACTION_UP都叫一个事件,并不是说这三者联系在一起才是一个事件)
,未被消耗才会调用onTouchEvent(event).
3 在onTouchEvent(event)中的ACTION_UP事件里会调用performClick()处理OnClick点击事件!!!!

4 所以可知:
4.1 Touch事件先于Click事件发生和处理,且注意onTouch()方法默认返回为false.
4.2 只有在onTouch()返回false时(即事件未被消耗)才会调用onTouchEvent()
4.3 在onTouchEvent()中的ACTION_UP事件会调用performClick()处理OnClick点击事件.

5 参见下面的onTouchEvent()源码,请注意第三个if判断,这个if判断很重要!!!!!!!
5.1 在该if条件中判断该控件是否是可点击的(CLICKABLE)或者是否是可以长按的(LONG_CLICKABLE).
5.2 如果满足CLICKABLE和LONG_CLICKABLE中任一条件则始终会返回true给onTouchEvent()方法
5.3 如果CLICKABLE和LONG_CLICKABLE这两个条件都不满足则返回false给onTouchEvent()方

接下来我们结合上面的具体例子,来分析一下这个过程,首先会执行dispatchTouchEvent(MotionEvent event) ,所以onTouch方法肯定是早于onClick方法的,如果在onTouch里返回false,就会出现下面的现象:

04-19 17:33:20.846 7795-7795/? D/MainActivity: onTouch execute, action event 004-19 17:33:20.856 7795-7795/? D/MainActivity: onTouch execute, action event 204-19 17:33:20.866 7795-7795/? D/MainActivity: onTouch execute, action event 104-19 17:33:20.866 7795-7795/? D/MainActivity: onClick execute
public static final int ACTION_DOWN             = 0;单点触摸动作public static final int ACTION_UP               = 1;单点触摸离开动作public static final int ACTION_MOVE             = 2;触摸点移动动作public static final int ACTION_CANCEL           = 3;触摸动作取消public static final int ACTION_OUTSIDE          = 4;触摸动作超出边界public static final int ACTION_POINTER_DOWN     = 5;多点触摸动作public static final int ACTION_POINTER_UP       = 6;多点离开动作

即先执行了onTouch,再执行了onClick事件,而且onTouch执行了三次,一个是action_down,一个是action_up事件;

如果onTouch里返回true,则出现下面的现象:

04-19 17:39:00.127 10574-10574/? D/MainActivity: onTouch execute, action event 004-19 17:39:00.207 10574-10574/? D/MainActivity: onTouch execute, action event 204-19 17:39:00.207 10574-10574/? D/MainActivity: onTouch execute, action event 1

结果是onClick事件没有执行了,原因是如果onTouch返回true的话,则dispatchEvent(MotionEvent event)方法直接返回true了,相当于不往下传递事件了,所以onClick不会执行,相反如果onTouch返回false的话(此时会执行onClick方法),则会执行 onTouchEvent(MotionEvent event)方法,由此可以得出这样一个结论,onClick事件的具体调用执行肯定是在onTouchEvent(MotionEvent event)方法源码中,接下来分析一下该函数的源码:

public boolean onTouchEvent(MotionEvent event) {      final int viewFlags = mViewFlags;      if ((viewFlags & ENABLED_MASK) == DISABLED) {          // 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));      }      if (mTouchDelegate != null) {          if (mTouchDelegate.onTouchEvent(event)) {              return true;          }      }      if (((viewFlags & CLICKABLE) == CLICKABLE ||              (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {          switch (event.getAction()) {              case MotionEvent.ACTION_UP:                  boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;                  if ((mPrivateFlags & 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 (!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();                              }                          }                      }                      if (mUnsetPressedState == null) {                          mUnsetPressedState = new UnsetPressedState();                      }                      if (prepressed) {                          mPrivateFlags |= PRESSED;                          refreshDrawableState();                          postDelayed(mUnsetPressedState,                                  ViewConfiguration.getPressedStateDuration());                      } else if (!post(mUnsetPressedState)) {                          // If the post failed, unpress right now                          mUnsetPressedState.run();                      }                      removeTapCallback();                  }                  break;              case MotionEvent.ACTION_DOWN:                  if (mPendingCheckForTap == null) {                      mPendingCheckForTap = new CheckForTap();                  }                  mPrivateFlags |= PREPRESSED;                  mHasPerformedLongPress = false;                  postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                  break;              case MotionEvent.ACTION_CANCEL:                  mPrivateFlags &= ~PRESSED;                  refreshDrawableState();                  removeTapCallback();                  break;              case MotionEvent.ACTION_MOVE:                  final int x = (int) event.getX();                  final int y = (int) event.getY();                  // Be lenient about moving outside of buttons                  int slop = mTouchSlop;                  if ((x < 0 - slop) || (x >= getWidth() + slop) ||                          (y < 0 - slop) || (y >= getHeight() + slop)) {                      // Outside button                      removeTapCallback();                      if ((mPrivateFlags & PRESSED) != 0) {                          // Remove any future long press/tap checks                          removeLongPressCallback();                          // Need to switch from pressed to not pressed                          mPrivateFlags &= ~PRESSED;                          refreshDrawableState();                      }                  }                  break;          }          return true;      }      return false;  }  

虽然源码有点多,但是我们只重点关注关键代码,在38行我们看到了代码:performClick();这个方法从名字表义来看就是OnClick方法的调用,我们进入到该方法中去看一探究竟,是否执行了OnClick方法呢?

public boolean performClick() {      sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);      if (mOnClickListener != null) {          playSoundEffect(SoundEffectConstants.CLICK);          mOnClickListener.onClick(this);          return true;      }      return false;  }  

从上述代码可以看到,只要mOnClickListener不是null,就会去调用它的onClick方法,那mOnClickListener又是在哪里赋值的呢?经过分析后找到如下方法:

public void setOnClickListener(OnClickListener l) {      if (!isClickable()) {          setClickable(true);      }      mOnClickListener = l;  }  

而上述这个方法就是我们在Application层经常使用的方法,即我们给button 设置点击事件的时候就会调用该方法了,分析到这了,我们知道了OnClick方法确实是在OnTouchEvent方法中,那么除了要设置 OnClickListener,调用onClick的条件又是什么呢?我们从38行代码往前推,从第14行可以分析出:
只要该控件是可点击的或者是长按类型的,则会进入到MotionEvent.ACTION_UP这个分支当中 ,然后经过各种条件判断,则会进入到38行的performClick()方法中。

至此,一切都清晰明白了!当我们通过调用setOnClickListener方法来给控件注册一个点击事件时,就会给mOnClickListener赋值。然后每当控件被点击时或者长按时,都会在performClick()方法里回调被点击控件的onClick方法。

经验之谈:
关于OnTouchEvent(MotionEvent事件)事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

那我们可以换一个控件,将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。如下所示:

imageView.setOnTouchListener(new OnTouchListener() {    @Override    public boolean onTouch(View v, MotionEvent event) {        Log.d("TAG", "onTouch execute, action " + event.getAction());        return false;    }});

运行一下程序,点击ImageView,你会发现结果如下:
这里写图片描述

在ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。这又是为什么呢?因为ImageView和按钮不同,它是默认不可点击的,因此在onTouchEvent的第14行判断时无法进入到if的内部,直接跳到第91行返回了false,也就导致后面其它的action都无法执行了。

Button默认情况下就是CLICKABLE和LONG_CLICKABLE的,但是ImageView在 默认情况下CLICKABLE和LONG_CLICKABL均为不可用的.
所以在用Button和ImageView分别实验OnTouchListener和OnClickListener是有区别的.
再次提醒注意:onTouch()方法默认返回为false.
1 Button做实验分析dispatchTouchEvent().mOnTouchListener.onTouch()返回false(默认值),所以dispatchTouchEvent()
如上源码中的if不满足,于是继续调用onTouchEvent(event)时由于Button满足CLICKABLE和LONG_CLICKABLE
所以最后返回给dispatchTouchEvent()的是true,即继续事件的分发.
所以可以捕获到一系列的:ACTION_DOWN,ACTION_MOVE,ACTION_UP.
这里就解释了为什么在Button中虽然onTouch()返回false(默认值)但是事件分发还在继续!!!!!!!!!!!!!

2 用ImageView做实验分析dispatchTouchEvent().
mOnTouchListener.onTouch()返回false(默认值),所以dispatchTouchEvent()
如上源码中的if不满足,在调用onTouchEvent(event)时由于ImageView不满足CLICKABLE和LONG_CLICKABLE
中任何一个所以最后返回给dispatchTouchEvent()的是false,即终止事件的分发.所以对于ImageView只有
ACTION_DOWN没有ACTION_MOVE和ACTION_UP
这里就解释了为什么在ImageView中在onTouch()返回里false(默认值)就终止了事件分发!!!!!!!!!!!!!

如何才可以使ImageView像Button那样”正规的”事件分发,有如下两个方法:
1 为ImageView设置setOnTouchListener,且在其onTouch()方法中返回true而不是默认的false.
2 为ImageView设置android:clickable=”true”或者ImageView设置OnClickListener.就是说让ImageView变得可点击.

好了,关于View的事件分发,我想讲的东西全都在这里了。现在我们再来回顾一下开篇时提到的那三个问题,相信每个人都会有更深一层的理解。

1. onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

2. 为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
如果你阅读了Android滑动框架完全解析,教你如何一分钟实现滑动菜单特效 这篇文章,你应该会知道滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。

3. 为什么图片轮播器里的图片使用Button而不用ImageView?
提这个问题的朋友是看过了Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用Button,主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable=”true”的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

资料

http://blog.csdn.net/guolin_blog/article/details/9097463
http://blog.csdn.net/bigconvience/article/details/26611003
http://www.infoq.com/cn/articles/android-event-delivery-mechanism/
http://blog.csdn.net/lmj623565791/article/details/38960443

0 0
原创粉丝点击