Android View事件派发机制详解与源码分析
2016-04-11 15:36
543 查看
参考的文章有:
http://blog.csdn.net/yanbober/article/details/45887547
http://ztelur.github.io/2016/02/04/%E5%9B%BE%E8%A7%A3Android%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92%E4%B9%8BView%E7%AF%87/
http://blog.csdn.net/guolin_blog/article/details/9097463
http://wangkuiwu.github.io/
http://blog.csdn.net/cyp331203/article/details/45071069
2 基础实例现象
2-1 例子
从一个例子分析说起吧。如下是一个很简单不过的Android实例:
2-2 现象
当稳稳的点击Button时打印如下:
当稳稳的点击除过Button以外的其他地方时打印如下
当手指点击Button时按在Button上晃动了一下松开后的打印如下
现在我们来分析一下上面的情况:
细心的朋友应该可以注意到,onTouch方法是有返回值的,这里我们返回的是false,如果我们尝试把onTouch方法里的返回值改成true,再运行一次,再次点击Button结果如下:
运行结果:
我们发现,onClick方法不再执行了!为什么会这样呢?你可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。
Android控件的Listener事件触发顺序是先触发onTouch,其次onClick。
如果控件的onTouch返回true将会阻止事件继续传递,返回false事件会继续传递。
看了官方这个继承图是不是明白了上面例子中说的LinearLayout是ViewGroup的子类,ViewGroup是View的子类,Button是View的子类关系呢?其实,在Android中所有的控件无非都是ViewGroup或者View的子类,说高尚点就是所有控件都是View的子类。通过继承关系是说明一切控件都是View,同时View与ViewGroup又存在一些区别,所以该模块才只单单先分析View触摸屏事件传递机制
首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent方法,可是你会发现Button类里并没有这个方法,那么就到它的父类TextView里去找一找,你会发现TextView里也没有这个方法,那没办法了,只好继续在TextView的父类View里找一找,这个时候你终于在View里找到了这个方法,示意图如下:
然后我们来看一下View中dispatchTouchEvent方法的源码:
dispatchTouchEvent的代码有点长,我们通过流程图来进行看:
首先在第10行,判断当前View是否为事件,如果是false返回false,true就往下执行。
第21行,只是一个输入法一致的处理,并不影响返回的结果,这里不作分析,往下走重点
到31行的if (onFilterTouchEventForSecurity(event))语句判断当前View是否没被遮住
33行,ListenerInfo局部变量,ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法;
首先li对象自然不会为null,li.mOnTouchListener呢?你会发现ListenerInfo的mOnTouchListener成员是在哪儿赋值的呢?怎么确认他是不是null呢?通过在View类里搜索可以看到
第一:上面的实例中我们是设置过Button的setOnTouchListener方法的,所以也不为null
第二:(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true
第三:这个比较关键了,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法
首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的,也是印证了刚刚的打印结果。
而如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch返回true,onClick就不会再执行了
如果只要没有设置touchListener或者不是ENABLEDY会返回false就会执行onTouchEvent
可以参考onTouchEvent事件的流程图:
http://ztelur.github.io/2016/02/04/%E5%9B%BE%E8%A7%A3Android%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92%E4%B9%8BView%E7%AF%87/
View的dispatchTouchEvent中的onTouchEvent源码:
首先、6到14行可以看出,如果控件(View)是disenable状态,(并且是可以clickable的或者是长按等则onTouchEvent直接消费事件返回true,),关于控件的enable或者clickable属性可以通过java或者xml直接设置.
第二,上面的条件不满足的情况下就会进入到MotionEvent.ACTION_UP:里面,判断了是否按下过,同时是不是可以得到焦点,然后尝试获取焦点,然后判断如果不是longPressed则通过post在UI Thread中执行一个PerformClick的Runnable,也就是performClick方法。具体如下:
这个方法也是先定义一个ListenerInfo的变量然后赋值,接着判断li.mOnClickListener是不是为null,决定执行不执行onClick。你指定现在已经很机智了,和onTouch一样,搜一下mOnClickListener在哪赋值的呗,结果发现:
看见了吧!控件只要监听了onClick方法则mOnClickListener就不为null,而且有意思的是如果调运setOnClickListener方法设置监听且控件是disclickable的情况下默认会帮设置为clickable。
onClick就在onTouchEvent中执行的,而且是在onTouchEvent的ACTION_UP事件中执行的。
onTouchEvent方法中会在ACTION_UP分支中触发onClick的监听
当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action
解释一下:
如果在onTouch方法中的执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true(意思就是要在dispatchtouchEvent方法里面到的onTouchEvent调用之前result要为true),才会触发后一个action(就是执行onTouchEvent(event)方法里面的action_down,up,move这些)。
很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。
参考着我们前面分析的源码,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后我们来看一下onTouchEvent方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。
是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得前面的例子中ACTION_UP可以得到执行。
onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
如果你阅读了Android滑动框架完全解析,教你如何一分钟实现滑动菜单特效 这篇文章,你应该会知道滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。
为什么图片轮播器里的图片使用Button而不用ImageView?
提这个问题的朋友是看过了Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用Button,主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable=”true”的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。
4-2 现象分析
4-2-1 点击Button(手抽筋了一下)
分析上面发现:
dispatchTouchEvent方法先派发down事件,完事调用onTouch的down事件,完事调用onTouchEvent返回true,同时dispatchTouchEvent返回true,
然后dispatchTouchEvent继续派发move或者up事件,循环,直到onTouchEvent处理up事件时调运onClick事件,完事返回true,同时dispatchTouchEvent返回true;一次完整的View事件派发流程结束。
4-2-2 简单修改onTouchEvent返回值为true
分析结果:
可以发现,当自定义了控件(View)的onTouchEvent直接返回true而不调运super方法时,事件派发机制如同4.2.1类似,只是最后up事件没有触发onClick而已(因为没有调用super),解释:因为我们重写了父类的onTouchEvent方法,也就是根部View的onTouchEvent方法,然后在dispatchTouchEvent的时候调用的就是我们重写的onTouchEvent方法,由于onClick方法是在onTouchEvent里面调用了,我们重写了没有调用performClick方法,所以onClick方法没有调用。
所以可想而知,如果TestButton类的onTouchEvent修改为如下:
分析:这个的运行效果和第一个效果是一样的。没有什么区别
4-2-3 简单修改onTouchEvent返回值为false
分析:你会发现如果onTouchEvent返回false(也即dispatchTouchEvent一旦返回false将不再继续派发其他action,立即停止派发),这里只派发了down事件,后面的up,move就都没有触发了。至于后面触发了LinearLayout的touch与click事件我们这里不做关注,下一篇博客会详细解释为啥(其实你可以想下的,LinearLayout是ViewGroup的子类,你懂的),这里你只用知道View的onTouchEvent返回false会阻止继续派发事件
同理修改如下:
4-2-4 简单修改dispatchTouchEvent返回值为true
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
分析:你会发现如果dispatchTouchEvent直接返回true且不调运super任何事件都得不到触发,onTouch和onTouchEvent都不触发了
继续修改如下呢?
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
可以发现所有事件都可以得到正常派发,和4.2.1类似。
4-2-5 简单修改dispatchTouchEvent返回值为false
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
点击Button如下:
你会发现事件不进行任何继续触发,关于点击Button触发了LinearLayout的事件暂时不用关注,下篇详解。
继续修改如下呢?
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
点击Button如下:
你会发现结果和4.2.3的第二部分结果一样,也就是说如果dispatchTouchEvent返回false事件将不再继续派发下一次。
4-2-6 简单修改dispatchTouchEvent与onTouchEvent返回值
修改dispatchTouchEvent返回值为true,onTouchEvent为false:
将TestButton类的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基础代码保持不变:
修改dispatchTouchEvent返回值为false,onTouchEvent为true:
将TestButton类的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基础代码保持不变:
由此对比得出结论,dispatchTouchEvent事件派发是传递的,如果返回值为false将停止下次事件派发,如果返回true将继续下次派发。譬如,当前派发down事件,如果返回true则继续派发up,如果返回false派发完down就停止了。
5 总结View触摸屏事件传递机制
上面例子也测试了,源码也分析了,总得有个最终结论方便平时写代码作为参考依据呀,不能每次都再去分析一遍源码,那得多蛋疼呢!
综合得出Android View的触摸屏事件传递机制有如下特征:
触摸控件(View)首先执行dispatchTouchEvent方法。
在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
如果控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回一样。
如果控件不是enable的设置了onTouch方法也不会执行,只能通过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回一样。
如果控件(View)是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。
当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action(也就是说dispatchTouchEvent返回true才会进行下一次action派发)。
关于上面的疑惑还有ViewGroup事件派发机制你可以继续阅读下一篇博客
【工匠若水 http://blog.csdn.net/yanbober】
http://blog.csdn.net/yanbober/article/details/45887547
http://ztelur.github.io/2016/02/04/%E5%9B%BE%E8%A7%A3Android%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92%E4%B9%8BView%E7%AF%87/
http://blog.csdn.net/guolin_blog/article/details/9097463
http://wangkuiwu.github.io/
http://blog.csdn.net/cyp331203/article/details/45071069
2 基础实例现象
2-1 例子
从一个例子分析说起吧。如下是一个很简单不过的Android实例:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mylayout" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:orientation="vertical" > <Button android:id="@+id/my_btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="click test" /> </LinearLayout>
public class MainActivity extends Activity implements View.OnTouchListener, View.OnClickListener { public static final String TAG = "MainActivity"; private LinearLayout mLayout; private Button mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mLayout = (LinearLayout) this.findViewById(R.id.mylayout); mButton = (Button) this.findViewById(R.id.my_btn); mLayout.setOnTouchListener(this); mButton.setOnTouchListener(this); mLayout.setOnClickListener(this); mButton.setOnClickListener(this); } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(TAG, "OnTouchListener--onTouch-- action_down --" + v); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "OnTouchListener--onTouch-- action_move --" + v); break; case MotionEvent.ACTION_UP: Log.i(TAG, "OnTouchListener--onTouch-- action_up --" + v); break; } return false; } @Override public void onClick(View v) { Log.i(TAG, "OnClickListener--onClick--" + v); } }
2-2 现象
当稳稳的点击Button时打印如下:
当稳稳的点击除过Button以外的其他地方时打印如下
当手指点击Button时按在Button上晃动了一下松开后的打印如下
现在我们来分析一下上面的情况:
onTouch方法里能做的事情比onClick要多一些,比如判断手指按下、抬起、移动等事件,那么如果我两个事件都注册了也,onTouch是优先于onClick执行的,并且onTouch执行了两次,如果你的手指按在上面左右移动一下onTouch会执行更多次,因此事件传递的顺序是先经过onTouch,再传递到onClick。
细心的朋友应该可以注意到,onTouch方法是有返回值的,这里我们返回的是false,如果我们尝试把onTouch方法里的返回值改成true,再运行一次,再次点击Button结果如下:
@Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(TAG, "OnTouchListener--onTouch-- action_down --" + v); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "OnTouchListener--onTouch-- action_move --" + v); break; case MotionEvent.ACTION_UP: Log.i(TAG, "OnTouchListener--onTouch-- action_up --" + v); break; } return true; }
运行结果:
我们发现,onClick方法不再执行了!为什么会这样呢?你可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。
2-3 总结结论
好了,经过这个简单的实例验证你可以总结发现:Android控件的Listener事件触发顺序是先触发onTouch,其次onClick。
如果控件的onTouch返回true将会阻止事件继续传递,返回false事件会继续传递。
3、现在我们来分析一下View与ViewGroup之间的关系
如下是几个继承关系图:看了官方这个继承图是不是明白了上面例子中说的LinearLayout是ViewGroup的子类,ViewGroup是View的子类,Button是View的子类关系呢?其实,在Android中所有的控件无非都是ViewGroup或者View的子类,说高尚点就是所有控件都是View的子类。通过继承关系是说明一切控件都是View,同时View与ViewGroup又存在一些区别,所以该模块才只单单先分析View触摸屏事件传递机制
首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent方法,可是你会发现Button类里并没有这个方法,那么就到它的父类TextView里去找一找,你会发现TextView里也没有这个方法,那没办法了,只好继续在TextView的父类View里找一找,这个时候你终于在View里找到了这个方法,示意图如下:
然后我们来看一下View中dispatchTouchEvent方法的源码:
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (!isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } // 如果该View被遮蔽,并且该View在被遮蔽时不响应点击事件; // 此时,返回false;不会执行onTouch()或onTouchEvent(),即过滤调用该点击事件。 // 否则,返回true。 // 被遮蔽的意思是:该View不是位于顶部,有其他的View在它之上。 if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
dispatchTouchEvent的代码有点长,我们通过流程图来进行看:
首先在第10行,判断当前View是否为事件,如果是false返回false,true就往下执行。
第21行,只是一个输入法一致的处理,并不影响返回的结果,这里不作分析,往下走重点
到31行的if (onFilterTouchEventForSecurity(event))语句判断当前View是否没被遮住
33行,ListenerInfo局部变量,ListenerInfo是View的静态内部类,用来定义一堆关于View的XXXListener等方法;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; }
首先li对象自然不会为null,li.mOnTouchListener呢?你会发现ListenerInfo的mOnTouchListener成员是在哪儿赋值的呢?怎么确认他是不是null呢?通过在View类里搜索可以看到
/** * Register a callback to be invoked when a touch event is sent to this view. * @param l the touch listener to attach to this view */ public void setOnTouchListener(OnTouchListener l) { getListenerInfo().mOnTouchListener = l; }
第一:上面的实例中我们是设置过Button的setOnTouchListener方法的,所以也不为null
第二:(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true
第三:这个比较关键了,mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。也就是说如果我们在onTouch方法里返回true,就会让这三个条件全部成立,从而整个方法直接返回true。如果我们在onTouch方法里返回false,就会再去执行onTouchEvent(event)方法
结论:
首先在dispatchTouchEvent中最先执行的就是onTouch方法,因此onTouch肯定是要优先于onClick执行的,也是印证了刚刚的打印结果。
而如果在onTouch方法里返回了true,就会让dispatchTouchEvent方法直接返回true,不会再继续往下执行。而打印结果也证实了如果onTouch返回true,onClick就不会再执行了
如果只要没有设置touchListener或者不是ENABLEDY会返回false就会执行onTouchEvent
onClick一定与onTouchEvent有关系,onClick的调用肯定是在onTouchEvent(event)方法中的,接下来就分析分析dispatchTouchEvent方法中的onTouchEvent方法。
可以参考onTouchEvent事件的流程图:
http://ztelur.github.io/2016/02/04/%E5%9B%BE%E8%A7%A3Android%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92%E4%B9%8BView%E7%AF%87/
View的dispatchTouchEvent中的onTouchEvent源码:
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // 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) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_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 (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 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) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
首先、6到14行可以看出,如果控件(View)是disenable状态,(并且是可以clickable的或者是长按等则onTouchEvent直接消费事件返回true,),关于控件的enable或者clickable属性可以通过java或者xml直接设置.
第二,上面的条件不满足的情况下就会进入到MotionEvent.ACTION_UP:里面,判断了是否按下过,同时是不是可以得到焦点,然后尝试获取焦点,然后判断如果不是longPressed则通过post在UI Thread中执行一个PerformClick的Runnable,也就是performClick方法。具体如下:
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
这个方法也是先定义一个ListenerInfo的变量然后赋值,接着判断li.mOnClickListener是不是为null,决定执行不执行onClick。你指定现在已经很机智了,和onTouch一样,搜一下mOnClickListener在哪赋值的呗,结果发现:
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
看见了吧!控件只要监听了onClick方法则mOnClickListener就不为null,而且有意思的是如果调运setOnClickListener方法设置监听且控件是disclickable的情况下默认会帮设置为clickable。
onClick就在onTouchEvent中执行的,而且是在onTouchEvent的ACTION_UP事件中执行的。
总结:
onTouchEvent方法中会在ACTION_UP分支中触发onClick的监听
当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action
解释一下:
如果在onTouch方法中的执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true(意思就是要在dispatchtouchEvent方法里面到的onTouchEvent调用之前result要为true),才会触发后一个action(就是执行onTouchEvent(event)方法里面的action_down,up,move这些)。
解惑:
很多的朋友肯定要有巨大的疑问了。这不是在自相矛盾吗?前面的例子中,明明在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP不是都得到执行了吗?其实你只是被假象所迷惑了,让我们仔细分析一下,在前面的例子当中,我们到底返回的是什么。
参考着我们前面分析的源码,首先在onTouch事件里返回了false,就一定会进入到onTouchEvent方法中,然后我们来看一下onTouchEvent方法的细节。由于我们点击了按钮,就会进入到第14行这个if判断的内部,然后你会发现,不管当前的action是什么,最终都一定会走到第89行,返回一个true。
是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得前面的例子中ACTION_UP可以得到执行。
onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
如果你阅读了Android滑动框架完全解析,教你如何一分钟实现滑动菜单特效 这篇文章,你应该会知道滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。
为什么图片轮播器里的图片使用Button而不用ImageView?
提这个问题的朋友是看过了Android实现图片滚动控件,含页签功能,让你的应用像淘宝一样炫起来 这篇文章。当时我在图片轮播器里使用Button,主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable=”true”的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。
4、透过源码继续进阶实例验证
4-1 例子<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mylayout" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:orientation="vertical" > <com.example.viewdispatch.TestButton android:id="@+id/my_btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="click test" /> </LinearLayout>
public class TestButton extends Button { public TestButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "dispatchTouchEvent--- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_up --"); break; } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "onTouchEvent--action_up --"); break; } return super.onTouchEvent(event); } }
public class ListenerActivity extends Activity implements View.OnTouchListener, View.OnClickListener { public static final String TAG = "ListenerActivity"; private LinearLayout mLayout; private TestButton mButton; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.listener); mLayout = (LinearLayout) this.findViewById(R.id.mylayout); mButton = (TestButton) this.findViewById(R.id.my_btn); mLayout.setOnTouchListener(this); mButton.setOnTouchListener(this); mLayout.setOnClickListener(this); mButton.setOnClickListener(this); } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(TAG, "OnTouchListener--onTouch-- action_down --" + v); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "OnTouchListener--onTouch-- action_move --" + v); break; case MotionEvent.ACTION_UP: Log.i(TAG, "OnTouchListener--onTouch-- action_up --" + v); break; } return false; } @Override public void onClick(View v) { Log.i(TAG, "OnClickListener--onClick--" + v); } }
4-2 现象分析
4-2-1 点击Button(手抽筋了一下)
分析上面发现:
dispatchTouchEvent方法先派发down事件,完事调用onTouch的down事件,完事调用onTouchEvent返回true,同时dispatchTouchEvent返回true,
然后dispatchTouchEvent继续派发move或者up事件,循环,直到onTouchEvent处理up事件时调运onClick事件,完事返回true,同时dispatchTouchEvent返回true;一次完整的View事件派发流程结束。
4-2-2 简单修改onTouchEvent返回值为true
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "onTouchEvent--action_up --"); break; } return true; }
分析结果:
可以发现,当自定义了控件(View)的onTouchEvent直接返回true而不调运super方法时,事件派发机制如同4.2.1类似,只是最后up事件没有触发onClick而已(因为没有调用super),解释:因为我们重写了父类的onTouchEvent方法,也就是根部View的onTouchEvent方法,然后在dispatchTouchEvent的时候调用的就是我们重写的onTouchEvent方法,由于onClick方法是在onTouchEvent里面调用了,我们重写了没有调用performClick方法,所以onClick方法没有调用。
所以可想而知,如果TestButton类的onTouchEvent修改为如下:
@Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "onTouchEvent--action_up --"); break; } return true; }
分析:这个的运行效果和第一个效果是一样的。没有什么区别
4-2-3 简单修改onTouchEvent返回值为false
@Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(TAG, "OnTouchListener--onTouch-- action_down --" + v); break; case MotionEvent.ACTION_MOVE: Log.i(TAG, "OnTouchListener--onTouch-- action_move --" + v); break; case MotionEvent.ACTION_UP: Log.i(TAG, "OnTouchListener--onTouch-- action_up --" + v); break; } return false; }
分析:你会发现如果onTouchEvent返回false(也即dispatchTouchEvent一旦返回false将不再继续派发其他action,立即停止派发),这里只派发了down事件,后面的up,move就都没有触发了。至于后面触发了LinearLayout的touch与click事件我们这里不做关注,下一篇博客会详细解释为啥(其实你可以想下的,LinearLayout是ViewGroup的子类,你懂的),这里你只用知道View的onTouchEvent返回false会阻止继续派发事件
同理修改如下:
@Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "onTouchEvent--action_up --"); break; } return false; }
4-2-4 简单修改dispatchTouchEvent返回值为true
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "dispatchTouchEvent--- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_up --"); break; } return true; }
分析:你会发现如果dispatchTouchEvent直接返回true且不调运super任何事件都得不到触发,onTouch和onTouchEvent都不触发了
继续修改如下呢?
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { super.dispatchTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "dispatchTouchEvent--- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_up --"); break; } return true; }
可以发现所有事件都可以得到正常派发,和4.2.1类似。
4-2-5 简单修改dispatchTouchEvent返回值为false
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "dispatchTouchEvent--- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_up --"); break; } return false; }
点击Button如下:
你会发现事件不进行任何继续触发,关于点击Button触发了LinearLayout的事件暂时不用关注,下篇详解。
继续修改如下呢?
将TestButton类的dispatchTouchEvent方法修改如下,其他和基础代码保持不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "dispatchTouchEvent--- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_up --"); break; } super.dispatchTouchEvent(event); return false; }
点击Button如下:
你会发现结果和4.2.3的第二部分结果一样,也就是说如果dispatchTouchEvent返回false事件将不再继续派发下一次。
4-2-6 简单修改dispatchTouchEvent与onTouchEvent返回值
修改dispatchTouchEvent返回值为true,onTouchEvent为false:
将TestButton类的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基础代码保持不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "dispatchTouchEvent--- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_up --"); break; } super.dispatchTouchEvent(event); return true; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "onTouchEvent--action_up --"); break; } super.onTouchEvent(event); return false; }
修改dispatchTouchEvent返回值为false,onTouchEvent为true:
将TestButton类的dispatchTouchEvent方法和onTouchEvent方法修改如下,其他和基础代码保持不变:
@Override public boolean dispatchTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "dispatchTouchEvent--- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "dispatchTouchEvent-- action_up --"); break; } super.dispatchTouchEvent(event); return false; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_down --"); break; case MotionEvent.ACTION_MOVE: Log.i(ListenerActivity.TAG, "onTouchEvent-- action_move --"); break; case MotionEvent.ACTION_UP: Log.i(ListenerActivity.TAG, "onTouchEvent--action_up --"); break; } super.onTouchEvent(event); return true; }
由此对比得出结论,dispatchTouchEvent事件派发是传递的,如果返回值为false将停止下次事件派发,如果返回true将继续下次派发。譬如,当前派发down事件,如果返回true则继续派发up,如果返回false派发完down就停止了。
5 总结View触摸屏事件传递机制
上面例子也测试了,源码也分析了,总得有个最终结论方便平时写代码作为参考依据呀,不能每次都再去分析一遍源码,那得多蛋疼呢!
综合得出Android View的触摸屏事件传递机制有如下特征:
触摸控件(View)首先执行dispatchTouchEvent方法。
在dispatchTouchEvent方法中先执行onTouch方法,后执行onClick方法(onClick方法在onTouchEvent中执行,下面会分析)。
如果控件(View)的onTouch返回false或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是enable的情况下会调运onTouchEvent,dispatchTouchEvent返回值与onTouchEvent返回一样。
如果控件不是enable的设置了onTouch方法也不会执行,只能通过重写控件的onTouchEvent方法处理(上面已经处理分析了),dispatchTouchEvent返回值与onTouchEvent返回一样。
如果控件(View)是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent方法。
当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发下一个action(也就是说dispatchTouchEvent返回true才会进行下一次action派发)。
关于上面的疑惑还有ViewGroup事件派发机制你可以继续阅读下一篇博客
【工匠若水 http://blog.csdn.net/yanbober】
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories