开发文章

Android的事件分发详解

一、Touch事件和绘制事件的异同之处

Touch事件和绘制事件很类似,都是由ViewRoot派发下来的,但是不同之处在绘制事件是由应用中的某个View发起请求,一层一层上传到ViewRoot,再有ViewRoot下发绘制,传递canvas给所有子View让其绘制自身,绘制好后,再通知WMS进行画到屏幕上。而Touch事件是由硬件捕获到触摸后由系统传递给应用的ViewRoot,再由ViewRoot往下一层一层传递。

他们的处理过程都是自上而下的分发,但是绘制多了一层自下往上的请求。

事件存在消耗,事件的处理方法都会返回一个boolean值,如果该值为true,则本次事件下发将会终止。

二、MotionEvent

1、MotionEvent对象的产生

系统有一个线程在循环收集屏幕硬件信息,当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。

系统的另一个线程循环的读取消息队列中的MotionEvent,然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity。

这就是一个先进先出的消费者和生产者的模板,一个线程不停的创建MotionEvent对象放入队列中,另一个线程不断的从队列中取出MotionEvent对象进行分发。

当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件,在该事件中,系统会不断收集事件信息封装成MotionEvent对象。收集的间隔时间取决于硬件设备,例如屏幕的灵敏度以及cpu的计算能力。目前的手机一般在20毫秒左右。

MotionEventCompat.getActionMasked()

2、MotionEvent对象详解

MotionEvent对象包含了触摸事件的时间、位置、面积、压力、以及本次事件的Dwon发生的时间。

MotionEvent常用的Action分为5种:Down 、Up、Move、Cancel、OutSide

MotionEvent中我们常用的方法就是获取点击的坐标,因为这是与我们操作息息相关的。获取坐标有两种方式:

  • getX和getY用于获取以该View左上角为坐标原点的坐标
  • getRowX和getRowY用于获取以屏幕左上角为坐标原点的坐标

5种Touch事件

  • Down:一次触摸事件的第一个MotionEvent对象,即手指初次接触屏幕。
  • Up:通常为一次触摸事件的最后一个MotionEvent对象,即手指离开屏幕。
  • Move:通常多次发生在一次触摸事件之中。表示触摸点发生了移动,我们通常把手指放到屏幕上,实际也会触发该事件,因为人手总是在轻微抖动的。
  • Cancel:常用于取消某个触摸事件,一般是由程序逻辑来指定该事件,用于取消某次触摸事件。
  • OutSide:当触摸点发生在响应事件的View之外时,传递的事件,通常由程序逻辑来指定。

在上面5种事件中,Down为最重要的事件,因为这是一个触摸事件的起始点,程序的很多逻辑判断,都需要根据该事件做处理,例如分发拦截。一次触摸事件必须要有Down事件,这也是MotionEvent对象中都包含了本次触摸事件的Down事件发生的时间点这个属性。其次是Move和Up,通过这3个事件的逻辑处理,就构建出来滑动,点击,长按,双击等多种效果。

创建一个MotionEvent对象

复制内容到剪贴板
  1. public static MotionEvent obtain(  
  2.         long downTime,    //当用户最初按下开始一连串的位置事件。这必须得到SystemClock.uptimeMillis()  
  3.         long eventTime,   //当这个特定的事件是生成的。这必须得到SystemClock.uptimeMillis()              
  4.         int action,       //该次事件的Action                         
  5.         float x,          //该次事件的x坐标          
  6.         float y,          //该次事件的y坐标           
  7.         float pressure,   //该次事件的压力,通常感觉标准压力,从0-1取值       
  8.         float size,       //点击的区域大小,通常根据特定标准范围从0-1取值       
  9.         int metaState,    //一个修饰性的状态,好像一直都是0            
  10.         float xPrecision, //x坐标的精确度             
  11.         float yPrecision, //y坐标的精确度                     
  12.         int deviceId,     //触屏设备id,如果是0,说明这个事件不是来自物理设备        
  13.         int edgeFlags     //系统默认都是返回0,程序在传递时,可以通过逻辑判断加入方向位置   
  14. )  

 

或者一个更简单的方式:

复制内容到剪贴板
  1. public static MotionEvent obtain(  
  2.             long downTime,  
  3.             long eventTime,  
  4.             int action,  
  5.             float x,  
  6.             float y,  
  7.             int metaState)  

也可以通过一个MotionEvent来创建一个新的

复制内容到剪贴板
  1. public static MotionEvent obtain(MotionEvent event)  

通过以上的方式,我们知道,我们也可以通过代码来构建一个虚假的MotionEvent,并分发下去。

复制内容到剪贴板
  1. view.dispatchTouchEvent(  
  2.             MotionEvent.obtain(SystemClock.uptimeMillis(),  
  3.             SystemClock.uptimeMillis(),  
  4.             MotionEvent.ACTION_DOWN,100,100,0));  

然后通过延迟以此往下派发Move和Up时间,形成一个完整的触摸操作。

三、dispatchTouchEvent触摸事件分发

dispatchTouchEvent触摸事件分发.png

之前我们知道触摸事件是被包装成MotionEvent进行传递的,而该对象是继承了Parcelable接口,正因为如此,才可以从系统中传递到我们的应用中。系统通过跨进程通知ViewRoot,ViewRoot会调用DecorView的dispatchTouchEvent下发。

这里有一个和其他事件传递不同的地方,DecorView会优先传递给Activity,而不是它的子View。而Activity如果不处理又会回传给DecorView,DecorView才会再将事件传给子View。

dispatchTouchEvent就是触摸事件传递的对外接口,无论是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent参数。

我们首先来看看Activity中的dispatchTouchEvent逻辑:

复制内容到剪贴板
  1. public boolean dispatchTouchEvent(MotionEvent ev) {  
  2.     if (ev.getAction() == MotionEvent.ACTION_DOWN) {  
  3.         onUserInteraction();  
  4.         //这是一个空实现的方法,以便子类实现,该方法在Key事件和touch事件的dispatch方法中都被调用,  
  5.         // 就是方便用户在事件被传递之前做一下自己的处理。  
  6.     }  
  7.     //这才是事件真正的分发  
  8.     if (getWindow().superDispatchTouchEvent(ev)) {  
  9.         //superDispatchTouchEvent是一个抽象方法,但是getWindow()获取的对象实际是FrameWork层的  
  10.         // PhoneWindow,该对象实现了这个方法,内部是直接调用DecorView的superDispatchTouchEvent  
  11.         // 是直接调用dispatchTouchEvent,这样就传递到子View中了     
  12.         return true;  
  13.     }  
  14.     //如果上面事件没有被消费掉,那么就调用Activity的onTouchEvent事件。  
  15.     return onTouchEvent(ev);  
  16. }  
  17. //PhoneWindow的superDispatchTouchEvent方法直接调用了mDecor的superDispatchTouchEvent  
  18. public boolean superDispatchTouchEvent(MotionEvent event) {  
  19.     return mDecor.superDispatchTouchEvent(event);  
  20. }  
  21. //mDecor即为Activity真正的根View,我们通过setContentView所添加的内容就是添加在该View上,  
  22. // 它实际上就是一个FrameLayout  
  23. public boolean superDispatchTouchEvent(MotionEvent event) {  
  24.     return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent  
  25. }  

 

至此我们已经至少明白了以下几点:

1、我们可以重载Activity的onUserInteraction方法,在Down事件触发传递前,实现我们的一些需求,实际上源码中有很多这样的方法,再某个方法体的第一行提供一个空实现的回调方法,在某个方法的最后一行提供一个空实现的回调方法,以便子类去实现自己的逻辑,例如AsyncTask就有类似的方式。这些技巧都能很好的提高我们代码的扩展性。

2、Activity会间接的调用根View的dispatchTouchEvent,并通过if判断返回值,如果为true,即向上层返回true,也就是调用Activity的dispatchTouchEvent的WMS,即操作系统。

3、如果if判断为false,即根View和根View下的所有子View均为消费掉该事件,那么下面的代码就有执行机会,即Activity的onTouchEvent,并把该方法的返回值作为结果返回给上层。

3.1、View的dispatchTouchEvent

View的dispatchTouchEvent.jpg

View中的处理相当简单明了,因为不涉及到子View,所以只在自身内部进行分发。首先判断是否设置了触摸监听,并且可以响应事件,就交由监听的onTouch处理。如果上述条件不成立,或者监听的onTouch事件没有消费掉该事件,则交由onTouchEvent进行处理,并把返回结果交给上层。

复制内容到剪贴板
  1. public boolean dispatchTouchEvent(MotionEvent event) {  
  2.     if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
  3.             mOnTouchListener.onTouch(this, event)) {  
  4.         //判断mOnTouchListener是否存在,并且控件可点的情况下,执行onTouch,如果onTouch返回true,就消耗该事件  
  5.         return true;  
  6.     }  
  7.     //如果以上条件都不成立,则把事件交给onTouchEvent来处理  
  8.     return onTouchEvent(event);  
  9. }  

3.2、ViewGroup的dispatchTouchEvent

ViewGroup的dispatchTouchEvent.jpg

Down事件:

  • 通过onInterceptTouchEvent方法判断是否要拦截事件,默认fasle
  • 根据scroll换算后的坐标找出所接受的子View。有动画的子View将不接受触摸事件。
  • 找到能接受的子View后把event中的坐标转换成子View的坐标
  • 调用子View的dispatchTouchEvent把事件传递给子View。
  • 如果子View消费了该事件,则把target记录为子View,方便后面的Move和Up事件的传递。
  • 如果子View没有消费,则继续寻找下一个子View。
  • 如果没找到,或者找到的子View都不消费,就会调用View的dispatchTouchEvent的逻辑,也就是判断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理

接下来我们来研究ViewGroup的dispatchTouchEvent,这是稍微复杂的分发逻辑。

复制内容到剪贴板
  1. public boolean dispatchTouchEvent(MotionEvent ev) {  
  2.     final int action = ev.getAction();//获取事件  
  3.     final float xf = ev.getX();//获取触摸坐标  
  4.     final float yf = ev.getY();  
  5.     final float scrolledXFloat = xf + mScrollX;//获取当前需要偏移的偏移量量  
  6.     final float scrolledYFloat = yf + mScrollY;  
  7.     final Rect frame = mTempRect;    //当前ViewGroup的视图矩阵  
  8.   
  9.     boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//是否禁止拦截  
  10.   
  11.     if (action == MotionEvent.ACTION_DOWN) {//如果事件是按下事件  
  12.         if (mMotionTarget != null) {    //判断接受事件的target是否为空  
  13.             //不为空肯定是不正常的,因为一个事件是由DOWN开始的,而DOWN还没有被消费,所以目标也不是不可能被确定,  
  14.             //造成这个的原因可能是在上一次up事件或者cancel事件的时候,没有把目标赋值为空  
  15.             mMotionTarget = null;    //在此处挽救  
  16.         }  
  17.         //不允许拦截,或者onInterceptTouchEvent返回false,也就是不拦截。注意,这个判断都是在DOWN事件中判断  
  18.         if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
  19.             //从新设置一下事件为DOWN事件,其实没有必要,这只是一种保护错误,防止被篡改了  
  20.             ev.setAction(MotionEvent.ACTION_DOWN);  
  21.             //开始寻找能响应该事件的子View  
  22.             final int scrolledXInt = (int) scrolledXFloat;  
  23.             final int scrolledYInt = (int) scrolledYFloat;  
  24.             final View[] children = mChildren;  
  25.             final int count = mChildrenCount;  
  26.             for (int i = count - 1; i >= 0; i--) {  
  27.                 final View child = children[i];  
  28.                 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
  29.                         || child.getAnimation() != null) {//如果child可见,或者有动画,获取该child的矩阵  
  30.                     child.getHitRect(frame);  
  31.                     if (frame.contains(scrolledXInt, scrolledYInt)) {  
  32.                         // 设置系统坐标  
  33.                         final float xc = scrolledXFloat - child.mLeft;  
  34.                         final float yc = scrolledYFloat - child.mTop;  
  35.                         ev.setLocation(xc, yc);  
  36.                         if (child.dispatchTouchEvent(ev))  {//调用child的dispatchTouchEvent  
  37.                             //如果消费了,目标就确定了,以便接下来的事件都传递给child  
  38.                             mMotionTarget = child;  
  39.                             return true;    //事件消费了,返回true  
  40.                         }  
  41.                     }  
  42.                 }  
  43.             }  
  44.             //能到这里来,证明所有的子View都没消费掉Down事件,那么留给下面的逻辑进行处理  
  45.         }  
  46.     }  
  47.     //判断是不是up或者cancel事件  
  48.     boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
  49.             (action == MotionEvent.ACTION_CANCEL);  
  50.   
  51.     if (isUpOrCancel) {  
  52.         //如果是取消,把禁止拦截这个标志位给取消  
  53.         mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
  54.     }  
  55.   
  56.   
  57.     final View target = mMotionTarget;  
  58.     if (target == null) {  
  59.         //判断该值是否为空,如果为空,则没找到能响应的子View,那么直接调用super的dispatchTouchEvent,也就是View的dispatchTouchEvent  
  60.         ev.setLocation(xf, yf);  
  61.         return super.dispatchTouchEvent(ev);  
  62.     }  
  63.   
  64.     //能走到这里来,说明已经有target,那也说明,这里不是DOWN事件,因为DOWN事件如果有target,已经在前面返回了,执行不到这里  
  65.     if (!disallowIntercept && onInterceptTouchEvent(ev)) {//如果有目标,又非要拦截,则给目标发送一个cancel事件  
  66.         final float xc = scrolledXFloat - (float) target.mLeft;  
  67.         final float yc = scrolledYFloat - (float) target.mTop;  
  68.         ev.setAction(MotionEvent.ACTION_CANCEL);//该为cancel  
  69.         ev.setLocation(xc, yc);  
  70.         if (!target.dispatchTouchEvent(ev)) {  
  71.             //调用子View的dispatchTouchEvent,就算它没有消费这个cancel事件,我们也无能为力了。  
  72.         }  
  73.         //清除目标  
  74.         mMotionTarget = null;  
  75.         //有目标,又拦截,自身也享受不了了,因为一个事件应该由一个View去完成  
  76.         return true;//直接返回true,以完成这次事件,好让系统开始派发下一次  
  77.     }  
  78.   
  79.     if (isUpOrCancel) {//取消或者UP的话,把目标赋值为空,以便下一次DOWN能重新找,此处就算不赋值,下一次DOWN也会先把它赋值为空  
  80.         mMotionTarget = null;  
  81.     }  
  82.     //又不拦截,又有目标,那么就直接调用目标的dispatchTouchEvent  
  83.     final float xc = scrolledXFloat - (float) target.mLeft;  
  84.     final float yc = scrolledYFloat - (float) target.mTop;  
  85.     ev.setLocation(xc, yc);  
  86.   
  87.     return target.dispatchTouchEvent(ev);  
  88.     //也就是说,如果是DOWN事件,拦截了,那么每次一次MOVE或者UP都不会再判断是否拦截,直接调用super的dispatchTouchEvent  
  89.     //如果DOWN没拦截,就是有其他View处理了DOWN事件,那么接下来的MOVE或者UP事件拦截了,那么给目标View发送一个cancel事件,告诉它touch被取消了,并且自身也不会处理,直接返回true  
  90.     //这是为了不违背一个Touch事件只能由一个View处理的原则。  
  91. }  

Move和Up事件

判断事件是否被取消或者事件是否要拦截住,是的话,给Down事件找到的target发送一个取消事件。如果不取消,也不拦截,并且Down已经找到了target,则直接交给target处理,不再遍历子View寻找合适的View了。这种处理事件是正确的,我们用手机经常可以体会到,当我手指按在一个拖动条上之后,在拖动的时候手指就算移出了拖动条,依然会把事件分发给拖动条控制它的拖动。

四、onInterceptTouchEvent

ViewGroup的方法,事件拦截,return true表示拦截触摸事件,事件就不往下传递

子View可以调用getParent().requestDisallowInterceptTouchEvent( true ) 请求父控件不拦截touch事件

五、View的onTouchEvent

从View的dispatchTouchEvent可以看出,事件最终的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序员来编写逻辑,那么我们来看看onTouchEvent事件。View只能响应click和longclick,不具备滑动等特性。

Down时,设置按压状态,发送一个延迟500毫秒的长按事件。
Move时,判断是否移出了View,移出后移除按压状态,长按事件。
Up时,取消按压,并判断它是否可以通过触摸获取焦点,是的话设置焦点,判断长按事件是否执行了,如果还没执行,就删除,并执行点击事件。

复制内容到剪贴板
  1. public boolean onTouchEvent(MotionEvent event) {  
  2.     final int viewFlags = mViewFlags;  
  3.     //先判断标示位是否为disable,也就是无法处理事件。  
  4.     if ((viewFlags & ENABLED_MASK) == DISABLED) {  
  5.         if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {  
  6.             setPressed(false);  
  7.         }//如果是UP事件,并且状态为按压,取消按压。  
  8.         //系统源码解释:虽然是disable,但是还是可以消费掉触摸事件,只是不触发任何click或者longclick事件。  
  9.         //根据是否可点击,可长按来决定是否消费点击事件。  
  10.         return (((viewFlags & CLICKABLE) == CLICKABLE ||  
  11.                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
  12.     }  
  13.     if (mTouchDelegate != null) {  
  14.         //先检查触摸的代理对象是否存在,如果存在,就交由代理对象处理。  
  15.         // 触摸代理对象是可以进行设置的,一般用于当我们手指在某个View上,而让另外一个View响应事件,另外一个View就是该View的事件代理对象。  
  16.         if (mTouchDelegate.onTouchEvent(event)) {//如果代理对象消费了,则返回true消费该事件  
  17.             return true;  
  18.         }  
  19.     }  
  20.     if (((viewFlags & CLICKABLE) == CLICKABLE ||  
  21.             (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
  22.         //如果是可点击或者长按的标识位执行下面的逻辑,这些标志位可以设置,也可以设置了对应的listener后自动添加  
  23.         //因为作为一个View,它只能单纯的接受处理点击事件,像滑动之类的复杂事件普通View是不具备的。  
  24.         switch (event.getAction()) {  
  25.             case MotionEvent.ACTION_UP://处理Up事件  
  26.                 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;//是否包含临时按压状态  
  27.                 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {//如果本身处于被按压状态或者临时按压状态  
  28.                     //临时按压状态会在下面的Move事件中说明  
  29.                     boolean focusTaken = false;  
  30.                     if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
  31.                         //如果它可以获取焦点,并且可以通过触摸来获取焦点,并且现在不是焦点,则请求获取焦点,因为一个被按压的View理论上应该获取焦点  
  32.                         focusTaken = requestFocus();  
  33.                     }  
  34.                     if (prepressed) {  
  35.                         //如果是临时按压,则设置为按压状态,PFLAG_PREPRESSED是一个非常短暂的状态,用于在某些时候短时间内表示Pressed状态,但不需要绘制  
  36.                         setPressed(true);//设置为按压状态,是因为临时按压不会绘制,这个时候强制绘制一次,确保用户能够看见按压状态  
  37.                     }  
  38.                     if (!mHasPerformedLongPress) {  
  39.                         //是否执行了长按事件,还没有的话,这个时候可以移除长按的回调了,因为UP都已经触发,说明从按下到UP的时间不足以触发longPress  
  40.                         //至于longPress,会在Down事件中说明  
  41.                         removeLongPressCallback();  
  42.                         if (!focusTaken) {//如果是焦点状态,就不会触摸click,这是为什么呢?因为焦点状态一般是交给按键处理的,  
  43.                             //pressed状态才是交给触摸处理,如果它是焦点,那么它的click事件应该由按键来触发  
  44.                             if (mPerformClick == null) {    //封装一个Runnable对象,这个对象中实际就调用了performClick();  
  45.                                 mPerformClick = new PerformClick();  
  46.                             }  
  47.                             if (!post(mPerformClick)) {//向消息队列发生该runnabel,如果发送不成功,则直接执行该方法。  
  48.                                 performClick();//这个方法内部会调用clickListner  
  49.                             }  
  50.                             //为什么不直接执行呢?如果这个时候直接执行,UP事件还没执行完,发送post,可以保障在这个代码块执行完毕之后才执行  
  51.                         }  
  52.                     }  
  53.                     if (mUnsetPressedState == null) {//仍旧是创建一个Runnabel对象,执行setPressed(false)  
  54.                         mUnsetPressedState = new UnsetPressedState();  
  55.                     }  
  56.                     if (prepressed) {  
  57.                         //如果是临时按压状态,之前的Down和move都还未触发按压状态,只在up时设置了,这个状态才刚刚绘制,为了保证用户能看到,发生一个64秒的延迟消息,来取消按压状态。                        postDelayed(mUnsetPressedState,  
  58.                         ViewConfiguration.getPressedStateDuration());  
  59.                         //这是一个64毫秒的短暂时间,这是为了让这个按压状态持续一小段时间,以便手指离开时候,还能看见View的按压状态  
  60.                     } else if (!post(mUnsetPressedState)) {//如果不是临时按压,则直接发送,发送失败,则直接执行  
  61.                         mUnsetPressedState.run();  
  62.                     }  
  63.                     removeTapCallback();  
  64.                     //移除这个callBack,这个callBack内部就是把临时按压状态设置成按压状态,因为这个已经没必要了,手指已经up了  
  65.                 }  
  66.                 break;  
  67.             case MotionEvent.ACTION_DOWN:  
  68.                 mHasPerformedLongPress = false;  
  69.                 //按下事件把长按事件执行的变量设置为false,代表还没执行长按,因为才按下,表示新的一个长按事件可以开始计算了  
  70.                 if (performButtonActionOnTouchDown(event)) {  
  71.                     //先把这个事件交由该方法,该方法内部会判断是否为上下文的菜单按钮,或者是否为鼠标右键,如果是就弹出上下文菜单。  
  72.                     //现在有些手机的上下文菜单按钮也是在屏幕触屏上的  
  73.                     break;  
  74.                 }  
  75.                 //这个方法会一直往上找父View,判断自身是否在一个可以滚动的容器中  
  76.                 boolean isInScrollingContainer = isInScrollingContainer();  
  77.                 //如果是在一个滚动的容器中,那么按压事件将会被推迟一段时间,如果这段时间内,发生了Move,那么按压状态讲不会被显示,直接滚动父视图  
  78.                 if (isInScrollingContainer) {  
  79.                     mPrivateFlags |= PFLAG_PREPRESSED; //先添加临时的按压状态,该状态表示按压,但不会绘制  
  80.                     if (mPendingCheckForTap == null) {  
  81.                         mPendingCheckForTap = new CheckForTap();  
  82.                         //创建一个runnable对象,这个runnable内部会取消临时按压状态,设置为按压状态,并启动长按的延迟事件  
  83.                     }  
  84.                     postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
  85.                     //向消息机制发生一个64毫秒的延迟时间,该事件会取消临时按压状态,设置为直接按压,并启动长按时间的计时  
  86.                 } else {  
  87.                     //如果不在一个滚动的容器中,则直接设置按压状态,并启动长按计时  
  88.                     setPressed(true);  
  89.                     checkForLongClick(0);  
  90.                     //长按事件就是向消息机制发送一个runnable对象,封装的就是我们在lisner中的代码,延迟500毫秒执行,也就是说长按事件在我们按下的时候发送,在up的时候检查一下执行了吗?如果没执行,就取消,并执行click  
  91.                 }  
  92.                 break;  
  93.             case MotionEvent.ACTION_CANCEL: //如果是取消事件,那就好办了,把我们之前发送的几个延迟runnable对象给取消掉  
  94.                 setPressed(false);      //设置为非按压状态  
  95.                 removeTapCallback();    //取消mPendingCheckForTap,也就是不用再把临时按压设置为按压了  
  96.                 removeLongPressCallback();    //取消长按事件的延迟回调  
  97.                 break;  
  98.             case MotionEvent.ACTION_MOVE:    //move事件  
  99.                 final int x = (int) event.getX();    //取触摸点坐标  
  100.                 final int y = (int) event.getY();  
  101.                 // 用于判断是否在View中,为什么还要判断呢?  
  102.                 //这是因为父View是在Down事件中判断是否在该View中的,如果在,以后的Move和up都会传递过来,不再进行范围判断  
  103.                 if (!pointInView(x, y, mTouchSlop)) {  
  104.                     //mTouchSlop是一个常量,数值为8,也就是说,就算你的落点超出了View的8像素位置,也算在View中。  
  105.                     //是因为人的手指触摸点比较大,有可能你感觉点在某个控件的边缘,但是实际落点已经超出这个View,所以这里给了8像素的范围  
  106.                     removeTapCallback();//如果在范围外,就移除这些runnable回调  
  107.                     if ((mPrivateFlags & PFLAG_PRESSED) != 0) {  
  108.                         //如果是按压状态,就取消长按,设置为非按压状态,为什么这个时候取消呢,因为在Down的时候,我们可以知道,只有是按压状态,才会设置长按  
  109.                         removeLongPressCallback();  
  110.                         setPressed(false);  
  111.                     }  
  112.                 }  
  113.                 break;  
  114.         }  
  115.         return true;    //至此,可以返回true,消费该事件  
  116.     }  
  117.     return false;    //如果不可点击,也不可长按,则返回false,因为View只具备消费点击事件  
  118. }  

 

从上面的代码我们总结一下View对触摸事件的处理:

1、是否为diabale,如果是,直接根据是否设置了click和longclick来返回。
2、是否设置了触摸代理对象,如果有,把事件传递给触摸代理对象,交由其处理,如果消费了,直接返回
3、是否为click或者longclick的,如果是,返回true,不是返回false。

而View对click和longclick的处理如下:

Down:

  • 判断是否可以触摸上下文菜单。
  • 是否在可以滑动的容器中,如果是先设置临时按压,再发送一个延迟消息把临时按压改为按压,并发送一个延迟500毫秒的事件去执行长按代码
  • 如果不在滚动容器中,直接设置按压状态,并发送一个延迟500毫秒的事件去执行长按代码。

Move:

  • 取触摸点坐标判断是否在View中(额外增加了8像素的范围)
  • 如果在,不用做任何事。
  • 如果不在,取消临时按压到按压回调,取消长按延迟回调,设置为非按压状态

Up

  • 判断是否为按压或者临时按压状态
  • 如果不是,不做任何处理
  • 如果是先判断其是否可以获取焦点,然后请求焦点。
  • 如果是临时按压状态,设置临时按压状态为按压状态。保证界面被绘制成按压状态,让用户可以看见。
  • 如果长按回调还未触发,取消长按回调,如果不是焦点状态,触发click事件。
  • 如果是临时按压状态,发送一个延迟取消按压状态的,保证按压状态持续一段时间,让用户可见。
  • 如果不是临时按压状态,直接发送消息取消按压状态。发送失败,直接取消按压状态。
  • 取消把临时按压设置按压的回调。

从中我们知道View的onTouchEvent主要处理了click和longclick事件,当按下时,向消息机制发送一个延迟500毫秒的长按回调事件,当移动时候判断是否移出了View的范围,超出则取消事件。当离开时,判断长按事件是否触发了,如果没触发且不是焦点,就触发click事件。

在这里最绕的就是临时按压和按压状态,临时按压是为了处理滑动容器的,让处于滑动容器中,按下时,我们先设置的是临时按压,持续64毫秒,是为了判断接下来的时间内是否发生了move事件,如果发生了,将不会再出发按压状态,这样不会让用户看到listView滚动时,item还处于按压状态。在离开时,我们再次判断是否处于临时按压,如果是在64毫秒内触发了down和up,说明按压状态还没来得急绘制,则强制设置为按压状态,保证用户能看到,并在取消回调的方法上加上64毫秒的延迟

六、onTouch与onClick

复制内容到剪贴板
  1. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);  
  2. iv_image.setOnTouchListener(new OnTouchListener() {  
  3.   
  4.     @Override  
  5.     public boolean onTouch(View v, MotionEvent event) {  
  6.         System.out.println("iv_image---onTouch--" + event.getAction());  
  7.         return false;  
  8.     }  
  9. });  

点击ImageView的时候只会打印一次,因为onTouch()返回false,只传递down事件,不会传递up事件

复制内容到剪贴板
  1. System.out: iv_image---onTouch--0  
  2.   
  3. // ImageView天生不能被点击,没有点击事件  
  4. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);  
  5. iv_image.setOnTouchListener(new OnTouchListener() {  
  6.   
  7.     @Override  
  8.     public boolean onTouch(View v, MotionEvent event) {  
  9.         System.out.println("iv_image---onTouch--" + event.getAction());  
  10.         return true// 把返回值改为true   
  11.     }  
  12. });  

把onTouch()方法返回值改为true,点击ImageView会打印两次(down and up)

复制内容到剪贴板
  1. System.out: iv_image---onTouch--0  
  2. System.out: iv_image---onTouch--1  
复制内容到剪贴板
  1. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);  
  2. iv_image.setOnTouchListener(new OnTouchListener() {  
  3.     @Override  
  4.     public boolean onTouch(View v, MotionEvent event) {  
  5.         System.out.println("iv_image---onTouch--" + event.getAction());  
  6.         return true;  
  7.     }  
  8. });  
  9. //添加click事件  
  10. iv_image.setOnClickListener(new OnClickListener() {  
  11.     @Override  
  12.     public void onClick(View v) {  
  13.         System.out.println("iv_image---onClick");  
  14.     }  
  15. });  

还是打印两次,onTouch()返回true,click事件并不会得到执行

复制内容到剪贴板
  1. ImageView iv_image = (ImageView) findViewById(R.id.iv_image);  
  2. iv_image.setOnTouchListener(new OnTouchListener() {  
  3.     @Override  
  4.     public boolean onTouch(View v, MotionEvent event) {  
  5.         System.out.println("iv_image---onTouch--" + event.getAction());  
  6.         return false;  
  7.     }  
  8. });  
  9.   
  10. iv_image.setOnClickListener(new OnClickListener() {  
  11.     @Override  
  12.     public void onClick(View v) {  
  13.         System.out.println("iv_image---onClick");  
  14.     }  
  15. });  

打印三次,两次touch事件(down and up)和一次click事件

复制内容到剪贴板
  1. Button button = (Button) findViewById(R.id.button);  
  2. button.setOnTouchListener(new OnTouchListener() {  
  3.     @Override  
  4.     public boolean onTouch(View v, MotionEvent event) {  
  5.         System.out.println("button---onTouch--" + event.getAction());  
  6.         return false;  
  7.     }  
  8. });  

点击Button会打印两次

 

复制内容到剪贴板
  1. Button button = (Button) findViewById(R.id.button);  
  2. button.setOnTouchListener(new OnTouchListener() {  
  3.     @Override  
  4.     public boolean onTouch(View v, MotionEvent event) {  
  5.         System.out.println("button---onTouch--" + event.getAction());  
  6.         return true;  
  7.     }  
  8. });  
  9.   
  10. button.setOnClickListener(new OnClickListener() {  
  11.     @Override  
  12.     public void onClick(View v) {  
  13.         System.out.println("button---onClick");  
  14.     }  
  15. });  

打印两次,因为onTouch()返回true,不会执行onTouchEvent(),而click事件是在onTouchEvent()中执行,所以也不会执行click事件

复制内容到剪贴板
  1. Button button = (Button) findViewById(R.id.button);  
  2. button.setOnTouchListener(new OnTouchListener() {  
  3.     @Override  
  4.     public boolean onTouch(View v, MotionEvent event) {  
  5.         System.out.println("button---onTouch--" + event.getAction());  
  6.         return false;  
  7.     }  
  8. });  
  9.   
  10. button.setOnClickListener(new OnClickListener() {  
  11.     @Override  
  12.     public void onClick(View v) {  
  13.         System.out.println("button---onClick");  
  14.     }  
  15. });  

打印三次

复制内容到剪贴板
  1. public boolean dispatchTouchEvent(MotionEvent event) {  
  2.     if (!onFilterTouchEventForSecurity(event)) {  
  3.         return false;  
  4.     }  
  5.   
  6.     if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
  7.             mOnTouchListener.onTouch(this, event)) {  
  8.         return true;  
  9.     }  
  10.     return onTouchEvent(event);  
  11. }  

a 判断mOnTouchListener是否为null
b 判断当前的控件是否可用
c 判断view的onTouch。
d 如果以上一个返回为false。那么就会调用onTouchEvent

首先判断mOnTouchListener不为null,并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true ; 也就是下面的onTouchEvent(event)不会被执行了。如果我们设置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不会被执行了

onTouch是优先于onClick执行, onClick的调用在onTouchEvent(event)方法中

view的事件分发

  1. 返回true,说明可以响应down事件和up事件
  2. 返回false,只会响应down事件。不会响应up事件。在down事件如果能消费(处理)当前事件。那么在up的时候也会把事件传递给当前的view,在down事件处理不了当前事件。那么在up的时候。也不会把事件传递给当前的view

Android的事件分发实例分析

七、ScrollView的onTouchEvent

 

普通的ViewGroup并没有对onTouchEvent事件做处理,只有可以滚动的才有,我们可以分析一下ScrollView

  • Down时,判断落点是否在子View中,不再就不处理,因为ScrollView只有一个子View。

  • Move时,通过对比本次手指的位置和上一次的位置的距离,计算出Y方向的差值,然后用scorllBy进行滚动视图

  • Up时,通过速度进行fling,这里利用了两个帮助类,一个是计算速度的帮助类VelocityTracker,一个是滚动的帮助类Scroller

复制内容到剪贴板
  1. public boolean onTouchEvent(MotionEvent ev) {  
  2.     if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {  
  3.         //如果是down事件,并且触摸到边缘,就不处理EdgeFlags代表是否为边缘,其值是1/2/4/8。代表上下左右  
  4.         return false;  
  5.     }  
  6.     if (mVelocityTracker == null) {  
  7.         //这是一个追踪触摸事件,并计算速度的帮助类,实现原理就是用三个数组分别记录每次触摸的x/y和时间  
  8.         mVelocityTracker = VelocityTracker.obtain();  
  9.     }  
  10.     mVelocityTracker.addMovement(ev);  
  11.     final int action = ev.getAction();  
  12.     switch (action & MotionEvent.ACTION_MASK) {//与上ff,去掉高位有关多点的信息  
  13.         case MotionEvent.ACTION_DOWN: {//如果是down  
  14.             final float y = ev.getY();//获取y坐标  
  15.             if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) {//判断是否开始拖动  
  16.                 //原理就是判断落点是否在child中,ScrollView只能由一个child,如果在,返回true,反之false  
  17.                 //也就是说落点在child中,就是准备开始拖动,不在,就直接返回,这可能是因为设置了padding之类的缘故造成的  
  18.                 return false;  
  19.             }  
  20.             if (!mScroller.isFinished()) {//判断滚动是否完成  
  21.                 mScroller.abortAnimation();//如果没完成,停止滚动  
  22.                 //对应上一次用户手指离开时候处理fling状态,这次按下手指,直接停止滚动  
  23.             }  
  24.             //记录y坐标,以便下次事件来对比  
  25.             mLastMotionY = y;  
  26.             mActivePointerId = ev.getPointerId(0);//记住多点的id,下次取值时只取该点的  
  27.             break;  
  28.         }  
  29.         case MotionEvent.ACTION_MOVE:  
  30.             if (mIsBeingDragged) {//可以看出,如果down的时候落点在child外,则以后就算滑进了child也不处理  
  31.                 //根据上次记录的多点id,找到对应的点,取y值  
  32.                 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);  
  33.                 final float y = ev.getY(activePointerIndex);  
  34.                 final int deltaY = (int) (mLastMotionY - y);//计算位移  
  35.                 mLastMotionY = y;//重新记录y值  
  36.                 scrollBy(0, deltaY);//滚动指定的距离,这也说明了ScrollView只具备纵向滑动  
  37.             }  
  38.             break;  
  39.         case MotionEvent.ACTION_UP:  
  40.             if (mIsBeingDragged) {//如果是离开事件  
  41.                 final VelocityTracker velocityTracker = mVelocityTracker;  
  42.                 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//计算最后1秒钟内的速度,并给定一个最大速度进行限制  
  43.                 //这个最大速度是根据屏幕密度不同而不同的,所以大家也没事别使劲滑动屏幕,因为有这个最大速度限制  
  44.                 //获取y方向的速度  
  45.                 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);  
  46.                 if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) {  
  47.                     //如果有子View,并且计算出来的y的速度比最小速度要大,执行fling状态  
  48.                     //手指滑动的方向和屏幕移动的方向是相反的,所以这里加-  
  49.                     fling(-initialVelocity);  
  50.                 }  
  51.                 mActivePointerId = INVALID_POINTER;//给mActivePointerId重新赋值为-1,防止下次事件找到了错误的点  
  52.                 mIsBeingDragged = false;//恢复默认值  
  53.                 if (mVelocityTracker != null) {//清空速度计算帮助类  
  54.                     mVelocityTracker.recycle();  
  55.                     mVelocityTracker = null;  
  56.                 }  
  57.             }  
  58.             break;  
  59.         case MotionEvent.ACTION_CANCEL:  
  60.             if (mIsBeingDragged && getChildCount() > 0) {//判断条件,只有这2个条件成立,才会发生滚动事件,下面的值才会被改变,才需要恢复默认  
  61.                 mActivePointerId = INVALID_POINTER;  
  62.                 mIsBeingDragged = false;  
  63.                 if (mVelocityTracker != null) {  
  64.                     mVelocityTracker.recycle();  
  65.                     mVelocityTracker = null;  
  66.                 }  
  67.             }  
  68.             break;  
  69.         case MotionEvent.ACTION_POINTER_UP://多点触摸时,不是最后一个点离开  
  70.             onSecondaryPointerUp(ev);  
  71.             break;  
  72.     }  
  73.     return true;  
  74. }  
  75. //用于应对先按下1点,然后按下2点,1点离开后,2点仍能继续滑动的逻辑  
  76. private void onSecondaryPointerUp(MotionEvent ev) {  
  77.     final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>  
  78.             MotionEvent.ACTION_POINTER_INDEX_SHIFT;//首先对高位进行与操作,然后右移8位,获取其高位代表index的值  
  79.     final int pointerId = ev.getPointerId(pointerIndex);//取出该点的id  
  80.     if (pointerId == mActivePointerId) {//如果这个id对应的就是第一个按下的点  
  81.         //理论上pointerIndex应该是0,所以用第二个按下的点,即1index的点代替  
  82.         final int newPointerIndex = pointerIndex == 0 ? 1 : 0;  
  83.         mLastMotionY = ev.getY(newPointerIndex);//取出新点的y坐标  
  84.         mActivePointerId = ev.getPointerId(newPointerIndex);//记录新点的id  
  85.         if (mVelocityTracker != null) {//清空之前存入的MotionEvent,也就是说最后的速度只计算该点产生的  
  86.             mVelocityTracker.clear();  
  87.         }  
  88.     }  
  89. }  

通过以上分析,我们得出以下知识:

  • 在down事件的时候先判断触摸是否处于边缘,如果是,则不处理
  • 在down事件中判断落点是否在子View中,如果不在,不处理
  • 在down事件中判断是否仍在滑动,如果是,先停止
  • 记录第一个按下点的索引值
  • 每次事件都记录住当前的y值
  • 在move事件中通过记录的索引值找到对应的点,获取y坐标
  • 与上一次y坐标进行比对,scrollBy两次的差值
  • 在up事件的时候计算最后一秒钟的速度,并且有最大速度进行限制,当计算的速度大于系统默认的最小速度时,只想fling
  • up和cancel事件还原变量为默认值
  • 如果为多点离开,进行多点离开的处理
  • 该处理方式时:如果离开的是第一个按下的点,那么由第二个按下的点代替其进行y值偏移计算的基点,并清空速度计算的帮助类,重新记录MotionEvnet

八、Layout和Scroll的区别

  • Layout中设置的是自身在父View中的显示区域
  • Scroll是调整自己的显示区域
  • 当父View滚动或者layout变化后,自身在屏幕上的位置会发生变化。
    当自身Scroll滚动后,在屏幕上的显示位置是不变的,变的只是自身的显示内容。
  • Scroll滚动不会影响Layout,只是在draw的时候影响画布偏移和触摸时的坐标计算。

Scroll滚动不会影响Layout.png

Scroll滚动不会影响Layout.png

Scroll滚动不会影响Layout.png

感谢 Android-Dev 支持 磐实编程网 原文地址:
blog.csdn.net/axi295309066/article/details/59563365

文章信息

发布时间:2017-03-03

作者:Android-Dev

发布者:aquwcw

浏览次数: