博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android事件分发、View事件Listener全解析
阅读量:4050 次
发布时间:2019-05-25

本文共 9486 字,大约阅读时间需要 31 分钟。

     转载请声明出处

问题

在自定义控件和设置事件Listener的时候,很多人想当然,完全根据自己的意愿处理返回值,比如down事件我不想处理,我要交给view自己处理所以return false或者return super.onTouchEvent(event);;我要自己处理move事件,不想交给view处理所以return true。这些想法给自己也给别人带来了很多bug。很多android开发者会遇到这样的问题:

    为什么我在自定义view的onTouchEvent中处理了touch事件后我的onClickListener,onLongClickListener没有执行了?

    为什么onTouchEvent中down的时候return false就收不到后面的事件了?而在move的时候返回return false为什么还可以收到up事件?

    为什么我自定义的view会误触发LongClick事件和Click事件?

    为什么我给view设置了onTouchListener处理事件后会误触发LongClick事件和Click事件?

    onTouchEvent,onTouchListener,onClickListener,onLongClickListener这些方法和接口在什么时候什么地方会被调用?

    。。。。。。

这些典型的事件冲突问题,在看完这篇文章之后,你就知道这些问题的答案了。文章中贴的源码是android4.0的,看有中文注释的地方就行了。

ViewGroup事件分发

     首先从ViewGroup开始讲。平时我们使用到的xxxLayout都是继承自ViewGroup,ViewGroup担任着事件分发者的角色,将TouchEvent分发给Layout中的子View,它自己也可以像View一样处理无View认领的事件,因为ViewGroup的父类是View。ViewGroup中分发事件的方法是dispatchTouchEvent。当一个TouchEvent产生的时候,当前Activity会将该event交给位于最外层的xxxLayout,xxxLayout中的dispatchTouchEvent得到执行,在dispatchTouchEvent中将event交给包含该touch区域的子View。除了Activity中最外层的layout,所有View获得的事件序列的开始都是down事件。要了解事件分发的过程,就需要看ViewGroup的dispatchTouchEvent源码了,下面的源码讲解是按顺序的,为了方便讲解,代码有省略,如果想要看完可以自己去找来看。
首先进入dispatchTouchEvent:
[java] 
  1. ......  
  2. // Handle an initial down.  
  3. if (actionMasked == MotionEvent.ACTION_DOWN) {  
  4.     // Throw away all previous state when starting a new touch gesture.  
  5.     // The framework may have dropped the up or cancel event for the previous gesture  
  6.     // due to an app switch, ANR, or some other state change.  
  7.     cancelAndClearTouchTargets(ev);  
  8.     resetTouchState();  
  9. }  
    可以看到,down事件到来的时候会重置所有状态并清空Target list,这个list是用来存放处理了event的子view的target的,暂时不用理会。继续,接着看下面的,注意两个变量
newTouchTarget
alreadyDispatchedToNewTouchTarget
[java] 
  1. ......  
  2.             TouchTarget newTouchTarget = null;  
  3.             boolean alreadyDispatchedToNewTouchTarget = false;  
  4.             if (!canceled && !intercepted) {  
  5.                 if (actionMasked == MotionEvent.ACTION_DOWN  
  6.                         || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)  
  7.                         || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {  
  8.             ......  
  9.             //遍历子view  
  10.             for (int i = childrenCount - 1; i >= 0; i--) {  
  11.                             final View child = children[i];  
  12.                             if (!canViewReceivePointerEvents(child)  
  13.                                     || !isTransformedTouchPointInView(x, y, child, null)) {  
  14.                                 continue;  
  15.                             }  
  16.   
  17.                             newTouchTarget = getTouchTarget(child);  
  18.                             if (newTouchTarget != null) {  
  19.                                 // Child is already receiving touch within its bounds.  
  20.                                 // Give it the new pointer in addition to the ones it is handling.  
  21.                                 newTouchTarget.pointerIdBits |= idBitsToAssign;  
  22.                                 break;  
  23.                             }  
  24.             ......  
  25.             //如果child的dispatchTouchEvent返回true,dispatchTransformedTouchEvent也会返回true,这时候条件就会成立,newTouchTarget和alreadyDispatchedToNewTouchTarget就会被修改。这里传进去child,方法里就会调用child.dispatchTouchEvent,如果传null就会调用super.dispatchTouchEvent  
  26.             if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {  
  27.                                 // Child wants to receive touch within its bounds.  
  28.                                 mLastTouchDownTime = ev.getDownTime();  
  29.                                 mLastTouchDownIndex = i;  
  30.                                 mLastTouchDownX = ev.getX();  
  31.                                 mLastTouchDownY = ev.getY();  
  32.                                 //将处理了down事件的view的target添加到target list的头部,此时newTouchTarget和mFirstTouchTarget是相等的  
  33.                                 newTouchTarget = addTouchTarget(child, idBitsToAssign);  
  34.                                 //标记事件已经分发给子view  
  35.                                 alreadyDispatchedToNewTouchTarget = true;  
  36.                                 break;  
  37.                             }  
  38.             }  
  39.         }  
  40.     }  
  41.     ......  
先不管事件拦截啥的,看这段代码,就是看这两个变量newTouchTarget和alreadyDispatchedToNewTouchTarget在过了这段代码后的值有没有改变,在down的时候可以进入这里面,所以如果子view的dispatchTouchEvent如果返回false的话该子view的target就不会被添加到target list中了,两个变量的值没有被改变。但在move和up的时候就进不去这里,所以在move和up事件时newTouchTarget是null的,alreadyDispatchedToNewTouchTarget是false。好的,接着往下
[java] 
  1.       ......  
  2. //mFirstTouchTarget是一个全局变量,指向target list的第一个元素。  
  3.  // Dispatch to touch targets.  
  4.            if (mFirstTouchTarget == null) {  
  5.     //进入这里,说明前面down事件处理的子view返回false,导致没有target被添加到list中,也就是这个事件没有view认领。  
  6.                // No touch targets so treat this as an ordinary view.  
  7.     //这里有个参数传了null,方法里面会判断这个参数,如果为null就调用super.dispatchTouchEvent,也就是自己来处理event。由于down后面的事件都没法修改mFirstTouchTarget,所以之后的事件都在这里执行了,该子view就没法接收到后面的事件了  
  8.                handled = dispatchTransformedTouchEvent(ev, canceled, null,  
  9.                        TouchTarget.ALL_POINTER_IDS);  
  10.            } else {  
  11.     //能进入这里,说明子view在处理down事件后返回了true。后面的move和up事件会直接进入这里  
  12.                // Dispatch to touch targets, excluding the new touch target if we already  
  13.                // dispatched to it.  Cancel touch targets if necessary.  
  14.                TouchTarget predecessor = null;  
  15.                TouchTarget target = mFirstTouchTarget;  
  16.                while (target != null) {  
  17.     //这里遍历这个target list,挨个分发事件,为了方便理解,可以暂时认为list里面此时就只包含一个target的,也就是当前处理事件的view的target  
  18.                    final TouchTarget next = target.next;  
  19.     //如果是down事件的话,就会进入这个if里面,由于down事件在前面已经处理了,所以直接handled = true。因为如果是move和up的话alreadyDispatchedToNewTouchTarget是false,newTouchTarget是null。  
  20.                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {  
  21.         //handled会被返回到当前Activity的dispatchTouchEvent中,具体在Activity中怎么使用可以查看其源码  
  22.                        handled = true;  
  23.                    } else {  
  24.         //move和up事件会进入这里  
  25.                        final boolean cancelChild = resetCancelNextUpFlag(target.child)  
  26.                        || intercepted;  
  27.         //看这里,由此可知在move的时候返回true或者false只会影响到layout返回给Activity的值,由于不是down事件所以不会影响up事件的获取。  
  28.                        if (dispatchTransformedTouchEvent(ev, cancelChild,  
  29.                                target.child, target.pointerIdBits)) {  
  30.                            handled = true;  
  31.                        }  
  32.                        if (cancelChild) {  
  33.                            if (predecessor == null) {  
  34.                                mFirstTouchTarget = next;  
  35.                            } else {  
  36.                                predecessor.next = next;  
  37.                            }  
  38.                            target.recycle();  
  39.                            target = next;  
  40.                            continue;  
  41.                        }  
  42.                    }  
  43.                    predecessor = target;  
  44.                    target = next;  
  45.                }  
  46.            }  
  47. ......  
注释已经写的很明白了,在注释中已经回答了一开始的几个问题了,处理down事件和处理down后面的事件有很大的区别,只要记住事件序列是以down开始的就可以了。

事件分发总结:

如果down事件没有被子view处理或者子view处理down事件后返回false,那么ViewGroup自己处理,也就是用父类View的代码处理,若父类给自己返回false的话,那么Activity不会把后面的move和up事件分发给ViewGroup了。
如果某一子view处理down事件并返回true,那么将该子view记录下来。后面move和up事件到来时直接将事件交给处理了down事件的该子view。
OK,既然事件已经分发至view中了。那我们就开始进入view中的事件处理讲解吧~

View事件处理

 前面layout把事件分发到这里了,就算没有字view处理,layout也会交给父类也就是View处理,依然是这里。在View事件处理中,将会讲到View的dispatchTouchEvent,onTouchListener,onClickListener,onLongClickListener这些个方法和监听器的执行和回调时间点。前面看到,ViewGroup把event分给View的dispatchTouchEvent,现在,就从这里开始讲。
      进入到View中:
[java] 
  1. public boolean dispatchTouchEvent(MotionEvent event) {  
  2.         ......  
  3.         //看这里吧,如果设置了onTouchListener就在这里回调的  
  4.             if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
  5.                     mOnTouchListener.onTouch(this, event)) {  
  6.                 return true;  
  7.             }  
  8.         //再来看这里,调用了onTouchEvent,onClickListener和onLongClickListener就在onTouchEvent里面回调的  
  9.             if (onTouchEvent(event)) {  
  10.                 return true;  
  11.             }  
  12.         ......  
  13.         return false;  
  14.     }  
我把其他暂时不用考虑的省略了,突出重点方便理解。看到View的dispatchTouchEvent,我们就可以开始回答前面的一些问题了:
假如我给View设置了onTouchListener并且在onTouch处理后返回true,那么onTouchEvent就不会被执行了,也就是说onClickListener和onLongClickListener都不会被回调了。
假如我在onTouchListener的onTouch中处理down的时候返回false,move和up返回值先不用管,这时候如果move事件持续事件长的话会触发长按事件,长按事件不触发就会触发点击事件。
具体为什么,还要继续往下看,下面进入View的重头戏onTouchEvent中看看:
[java] 
  1. public boolean onTouchEvent(MotionEvent event) {  
  2.     ......  
  3.     //从if条件可以看到,基本的View在onTouchEvent中只处理click事件和longclick事件  
  4.     if (((viewFlags & CLICKABLE) == CLICKABLE ||  
  5.                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
  6.             switch (event.getAction()) {  
  7.                 case MotionEvent.ACTION_UP:  
  8.                         ......  
  9.                         if (!mHasPerformedLongPress) {  
  10.                 //能进入这里说明up的时候长按事件还没有被执行  
  11.                             // This is a tap, so remove the longpress check,将长按回调从消息列表删除  
  12.                             removeLongPressCallback();  
  13.                                 ......  
  14.                                 // Use a Runnable and post this rather than calling  
  15.                                 // performClick directly. This lets other visual state  
  16.                                 // of the view update before click actions start.  
  17.                     //接下来执行点击事件的回调  
  18.                                 if (mPerformClick == null) {  
  19.                                     mPerformClick = new PerformClick();  
  20.                                 }  
  21.                     //用PerformClick异步执行,若不成功再调用performClick()  
  22.                                 if (!post(mPerformClick)) {  
  23.                                     performClick();  
  24.                                 }  
  25.                             ......  
  26.                         }  
  27.                         ......  
  28.                     break;  
  29.   
  30.                 case MotionEvent.ACTION_DOWN:  
  31.             //down的时候开始设置标记,标记整个事件过程中长按事件是否被执行了  
  32.                     mHasPerformedLongPress = false;  
  33.   
  34.                     if (performButtonActionOnTouchDown(event)) {  
  35.                         break;  
  36.                     }  
  37.   
  38.                     // Walk up the hierarchy to determine if we're inside a scrolling container.  
  39.                     boolean isInScrollingContainer = isInScrollingContainer();  
  40.   
  41.                     // For views inside a scrolling container, delay the pressed feedback for  
  42.                     // a short period in case this is a scroll.  
  43.             //这里判断父控件是否是一个可滚动的控件,如果是,长按事件的回调会被延长,CheckForTap类是实现了Runnable接口内部类,在其run方法中会执行checkForLongClick方法,将长按的回调放入handle消息列表中,一段时间后执行  
  44.                     if (isInScrollingContainer) {  
  45.                         mPrivateFlags |= PREPRESSED;  
  46.                         if (mPendingCheckForTap == null) {  
  47.                             mPendingCheckForTap = new CheckForTap();  
  48.                         }  
  49.                         postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
  50.                     } else {  
  51.                         // Not inside a scrolling container, so show the feedback right away  
  52.                         mPrivateFlags |= PRESSED;  
  53.                         refreshDrawableState();  
  54.                 //将长按回调放入消息列表,传入0表示经过默认时间后执行长按事件回调  
  55.                         checkForLongClick(0);  
  56.                     }  
  57.                     break;  
  58.   
  59.                 case MotionEvent.ACTION_CANCEL:  
  60.                     mPrivateFlags &= ~PRESSED;  
  61.                     refreshDrawableState();  
  62.                     removeTapCallback();  
  63.                     break;  
  64.   
  65.                 case MotionEvent.ACTION_MOVE:  
  66.                     final int x = (int) event.getX();  
  67.                     final int y = (int) event.getY();  
  68.   
  69.                     // Be lenient about moving outside of buttons  
  70.                     if (!pointInView(x, y, mTouchSlop)) {  
  71.                         // Outside button,手指移出view区域时会移除消息列表中的所有回调,包括长按的回调  
  72.                         removeTapCallback();  
  73.                         if ((mPrivateFlags & PRESSED) != 0) {  
  74.                             // Remove any future long press/tap checks  
  75.                             removeLongPressCallback();  
  76.   
  77.                             // Need to switch from pressed to not pressed  
  78.                             mPrivateFlags &= ~PRESSED;  
  79.                             refreshDrawableState();  
  80.                         }  
  81.                     }  
  82.                     break;  
  83.             }  
  84.             return true;  
  85.         }  
  86.         //如果一个View不可点击也不可长按,这里返回false会导致dispatchTouchEvent返回false  
  87.         return false;  
  88.         }  
从源码可以看到,down事件到来的时候,View就开始为LongClick计时了,所以说,在开始计时后如果没有接收到后续事件的话,长按事件就会触发,这时候mHasPerformedLongPress被赋值为true,click事件就不会被执行了,如果在计时结束之前,传进来up事件的话,
mHasPerformedLongPress还是false,长按回调消息被移除,执行click回调。到这里,已经可以回答前面的事件冲突问题了:
假如我自定义了View,复写了onTouchEvent,返回值是true或者false,并没有调用父类的代码,这时候设置了Listener当然不会被调用了,除非自己在这里面手动执行performLongClick()或performClick()。
假如我自定义了View,复写了onTouchEvent,在down处理完后又想将事件交给父类处理所以return super.onTouchEvent;接下来的move事件我不想给父类处理了,直接return true或false,这时候过一会儿就会触发长按事件;假如我在长按事件触发前处理完了up事件return super.onTouchEvent,这时候就会触发click事件。
这是在没有设置onTouchListener情况下的误触发,在设置了onTouchListener时的误触发这两个事件道理是一样的。要解决这些问题,可以参照view的处理方式,在覆盖父类方法的情况下手动执行回调。有时候使用别人写的控件的时候别人可能没有处理这些Listener,这时候怎么设置Listener都不会被回调的,需要自己修改。有时候自己并不想触发长按事件,那就想办法把回调消息删掉,可以用通过反射删除,后面讲仿QQ下拉刷新的时候会用到。
如果想要更清楚的了解android的事件分发机制,去仔细看源码吧。。。
你可能感兴趣的文章
可以在线C++编译的工具站点
查看>>
关于无人驾驶的过去、现在以及未来,看这篇文章就够了!
查看>>
所谓的进步和提升,就是完成认知升级
查看>>
为什么读了很多书,却学不到什么东西?
查看>>
长文干货:如何轻松应对工作中最棘手的13种场景?
查看>>
如何用好碎片化时间,让思维更有效率?
查看>>
No.174 - LeetCode1305 - 合并两个搜索树
查看>>
No.175 - LeetCode1306
查看>>
No.176 - LeetCode1309
查看>>
No.182 - LeetCode1325 - C指针的魅力
查看>>
mysql:sql alter database修改数据库字符集
查看>>
mysql:sql truncate (清除表数据)
查看>>
yuv to rgb 转换失败呀。天呀。谁来帮帮我呀。
查看>>
yuv420 format
查看>>
yuv420 还原为RGB图像
查看>>
LED恒流驱动芯片
查看>>
驱动TFT要SDRAM做为显示缓存
查看>>
使用file查看可执行文件的平台性,x86 or arm ?
查看>>
qt 创建异形窗体
查看>>
简单Linux C线程池
查看>>