View的事件分发机制
-
消息事件的传递:当产生一个触摸事件,主要由Activity传递到Window,然后传递给View,其中事件的分发主要是由onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent这三个方法共同完成,下面一一介绍下这几个方法
(1)onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent
dispatchTouchEvent:Activity,ViewGroup,View中都有该方法。进行触摸事件的分发。判断 onInterceptTouchEvent() return true则拦截事件,执行该层的onTouchEvent(),return false事件传递到下层,调用child.dispatchTouchEvent()。默认返回false onInterceptTouchEvent:ViewGroup中提供的方法,进行触摸事件的拦截,return true把事件拦截,交给该 层ViewGroup的onTouchEvent()执行,return false则把该事件传递给下一层view(ViewGroup)的 dispatchTouchEvent()进行事件分发。默认返回false onTouchEvent:Activity,ViewGroup,View中都有该方法。默认返回false(因为clickable默认为 false),把该事件传递给上一层 ViewGroup的onTouchEvent返回执行。返回true表示消费了该事件
//伪代码表示: @Override public boolean dispatchTouchEvent(MotionEvent ev) { if(onInterceptTouchEvent(ev)){ return onTouchEvent(ev); }else{ //最底层ViewGroup时,遍历所有的子view,判断事件发生的点位于哪个子View,调用该view的 //dispatchTouchEvent(),如果事件发生的点没有位于任何子view,那么由该ViewGroup来处理该事件,调 //用ViewGroup的onTouchEvent()方法 return child.dispatchTouchEvent(MotionEvent ev); } }
(2)消息传递顺序:
Activity->Window->View 事件总是先传给Activity的dispatchTouchEvent(MotionEvent ev)进行事件分发,再传给Window 最后Window再传给顶级View,然后顶级View会根据事件分发机制去分发事件,如果所有元素都不去处理这个事件, 即onTouchEvent方法return false,则Activity的onTouchEvent方法会被调用
-
OnTouchListener,onTouchEvent与OnClickListener,OnLongClickListener的关系
View上的触摸事件,会先判断是否设置onTouchListener。如果设置了就调用onTouch(),onTouch()如果return true,本次事件被消耗掉,后面的事件都不会执行。 如果onTouch()返回false,则会调用onTouchEvent。在onTouchEvent内部,如果调用了super.onTouchEvent(),并且设置了onClickListener就会去调用onClick方法。 总的来说,就是给View设置onTouchListener它的优先级比onTouchEvent要高一些,onClickListener的优先级最低。 OnLongClickListener和OnClickListener可以共存,如果OnLongClickListener的onLongClick()返回了true,则OnClickListener的onClick()不会执行
10-14 18:07:37.135 19863-19863/com.liujian.view I/MainActivity: onTouch: 10-14 18:07:37.136 19863-19863/com.liujian.view I/BasicView: onTouchEvent: ACTION_DOWN 10-14 18:07:37.136 19863-19863/com.liujian.view I/BasicView: onTouchEvent: true 10-14 18:07:37.536 19863-19863/com.liujian.view I/MainActivity: onTouch: 10-14 18:07:37.536 19863-19863/com.liujian.view I/BasicView: onTouchEvent: ACTION_MOVE 10-14 18:07:37.536 19863-19863/com.liujian.view I/BasicView: onTouchEvent: true 10-14 18:07:37.588 19863-19863/com.liujian.view I/MainActivity: onTouch: 10-14 18:07:37.588 19863-19863/com.liujian.view I/BasicView: onTouchEvent: ACTION_MOVE 10-14 18:07:37.588 19863-19863/com.liujian.view I/BasicView: onTouchEvent: true 10-14 18:07:37.637 19863-19863/com.liujian.view I/MainActivity: onLongClick: 10-14 18:07:38.070 19863-19863/com.liujian.view I/BasicView: onTouchEvent: ACTION_MOVE 10-14 18:07:38.070 19863-19863/com.liujian.view I/BasicView: onTouchEvent: true 10-14 18:07:38.400 19863-19863/com.liujian.view I/MainActivity: onTouch: 10-14 18:07:38.400 19863-19863/com.liujian.view I/BasicView: onTouchEvent: ACTION_UP 10-14 18:07:38.400 19863-19863/com.liujian.view I/BasicView: onTouchEvent: true 10-14 18:07:38.402 19863-19863/com.liujian.view I/MainActivity: onClick:
-
结论:
a.某个View一旦开始处理事件,如果它不消耗掉ACTION_DOWN(即onTouchEvent的ACTION_DOWN后返回了 false),那么同一序列中的其他事件将会由它的父元素的onTouchEvent来处理 b.如果View不消耗掉除ACTION_DOWN以外的事件(即在除ACTION_DOWN以外的事件return false)那么该点击事件 会被取消,父元素的onTouchEvent不会被调用 c.OnClick会发生的前提是当前View是可以点击的,并且收到了down和up的事件 d.View的onTouchEvent()默认返回false,因为默认view的clickable的值为false。如果设置clickable为 true即可以点击。则view的onTouchEvent()默认返回为true。 e.一个事件只能被一个View拦截且消耗 h.View中没有onInterceptTouchEvent方法,一旦有点击事件传递给它,onTouchEvent方法就会被调用
5.滑动冲突解决:
(1).场景一:外部滑动方向跟内部滑动方向不一致,比如外部左右滑动,内部上下滑动 ViewPager+Fragment配合使用,会有滑动冲突,但是ViewPager内部处理了这种滑动冲突 如果采用的不是ViewPager而是ScrollView就必须手动处理滑动事件 上下滑动时,需要上一级view把消息传递进行,因此上一级view的onInterceptTouchEvent方法 return false;左右滑动时需要上一级view执行它的onTouchEvent方法,因此它的onInterceptTouchEvent 方法应该return true; 代码如下: public boolean onInterceptTouchEvent(MotionEvent ev) { // TODO Auto-generated method stub boolean result=false; switch(ev.getAction()){ case MotionEvent.ACTION_DOWN: //getX()表示以该控件的左上角为0点 //getRawX()表示以屏幕的左上角为0点 firstX=(int) ev.getX(); firstY=(int) ev.getY(); break; case MotionEvent.ACTION_MOVE: //手指在屏幕上水平移动的绝对值 int disX=(int) Math.abs(ev.getX()-firstX); //手指在屏幕上竖直移动的绝对值 int disY=(int) Math.abs(ev.getY()-firstY); if(disX>disY&&disX>10){ result=true; } break; case MotionEvent.ACTION_UP: break; } return result; }
(2).场景二:外部滑动方向跟内部滑动方向一致,内外两层同时上下或者左右滑动 根据业务需求来判断 (3).场景三:上面两种场景的嵌套 (4).滑动冲突的解决方式: a.外部解决法:父容器对事件判断是否拦截 b.内部解决法:父容器不拦截任何事件,任何的事件都传递给子元素,如果子元素需要此事件就消耗掉,否则就由父容器进行处理。 对于底层的View来说,有一种方法可以阻止父层的View截获touch事件,就是调用getParent().requestDisallowInterceptTouchEvent(true); 方法。一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action。 public boolean dispatchTouchEvent(MotionEvent event){ switch(event.getAction()){ case MotionEvent.ACTION_DOWN: //父控件就不会拦截ListView的滑动事件 parent.requestDisallowInterceptTouchEvent(true); break; case MOtionEvent.ACTION_MOVE: if(父容器需要此类点击事件){ parent.requestDisallowInterceptTouchEvent(false); } break; } return super.dispatchTouchEvent(event); }