Android View事件分发机制及View的滑动冲突
2016-06-14 15:04
856 查看
Android事件分发机制
View事件分发机制是指Android对MotionEvent事件从产生到被消耗掉的整个处理过程。MotionEvent即点击事件,当手指接触屏幕后所产生的一系列事件中,最典型的事件类型有以下三种:
ACTION_DOWN——手指刚接触屏幕;
ACTION_MOVE——手指在屏幕上移动;
ACTION_UP——手指离开屏幕;
注意在一次完整的点击事件中ACTION_DOWN和ACTION_UP会且只会被触发一次。ACTION_MOVE可能不会被触发(当点击屏幕后松开)或者被多次触发。点击事件的分发由三个很重要的方法来完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面针对这三个方法略作介绍:
public boolean dispatchTouchEvent(MotionEvent ev)
用于对事件进行分发。如果事件能被传递给当前View,那么该方法一定会被调用并且是首先被调用,返回值代表当前事件是否被消耗,
public boolean onInterceptTouchEvent(MotionEvent ev)
用于判断是否拦截某个事件,在上述方法的内部被调用。如果View拦截了某个事件,那么在同一事件序列中,此方法不会被再次调用。
public boolean onTouchEvent(MotionEvent ev)
用于对事件的具体处理,返回值表示是否消耗当前事件。如果onTouchEvent()方法未消耗当前事件,则在同一事件序列中,当前View无法再次接收到事件。
上述三个方法之间的执行顺序及区别可用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consumed = false; if(onInterceptTouchEvent(ev)){ consume = onTouchEvent(ev); }else{ consume = child.dispatchTouchEvent(ev); } return consumed; }
点击事件发生时,MotionEvent最先传递给当前Activity,然后经过PhoneWindow(Window的唯一实现类)传递给DecorView,接着DecorView将MotionEvent传递给其父类ViewGroup, 在此期间MotionEvent仅仅是被传递,并无其他实质性操作。MotionEvent传递到ViewGroup之后会经过一系列的处理最终被消耗掉。我们需要关心的就是当MotionEvent来到ViewGroup之后究竟发生了什么?
ViewGroup对MotionEvent的分发过程
当MotionEvent来到ViewGroup,首先会被传递到ViewGroup中的dispatchTouchEvent中。其关键代码如下所示:// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
通过以上代码我们可以直接得出或引申出以下结论:
ViewGroup会在如下两种情况下判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget != null, 只有个当事件由ViewGroup的子元素处理并且消耗掉时mFirstTouchTarget才会被赋值。
这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法设置的,一般用于子View中。一旦设置为True后,ViewGroup将无法拦截除了ActionDown之外的其他事件,这是因为ViewGroup在分发事件时,如果是ActionDown就会重置FLAG_DISALLW_INTERCET标记位。因此当面对ACTION_DOWN事件时,Viewgroup总会调用自己的onInterceptTouchEvent方法来询问是否要拦截事件。默认情况下是不拦截的,并且如果ViewGroup一旦拦截ACTION_DOWN那么接下来的一系列事件都会被拦截。ViewGroup拦截事件包含两种情况:第一种是ViewGroup没有子元素,第二种是ViewGroup的所有子元素在dispatchTouchEvent中返回了false。ViewGroup拦截的事件会交个它的父类View进行处理。
默认情况下,ViewGroup不会拦截任何事件,它也没有自己的onTouchEvent方法。
具体View对点击事件的处理过程
首先我们先来看View的dispatchTouchEvent方法:public boolean dispatchTouchEvent(MotionEvent event){ boolean result = false; ... if(onFilerTouchEventForSecurity(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 } return result; }
通过以上代码我们可以直接得出或引申出以下结论:
View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果onTouchListener中的onTouch方法返回true,那么onTouchEvent方法将不会被调用。可见OnTouchListener的优先级要高于onTouchEvent的优先级。引申一点,在onTouchEvent方法中会调用OnClickListener(如果设置了的话)的onClick方法。所以他们三个方法之间的优先级顺序为:onTouch > onTouchEvent > onClick。
然后我们再来看一下View的onTouchEvent方法对事件的具体处理:
if(((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) = LONG_CLICKABLE)){ swithc (event.getAction()){ case MotionEvent.ACTION_UP: ... //如果View设置了onClickListener那么performClick方法内部会调用它的onClik方法。 performClick(); ... break; case MotionEvent.ACTION_MOVE: ... break; case MotionEvent.ACTION_DOWN: ... break; } ... return true; }
通过以上代码可以直接得出或引申出如下结论:
只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态。View的LONG_CLICKABLE属性默认是false,而CLICKABLE属性是否为false和true和具体View有关,确切来说可点击的View其CLICKABLE为true,不可点击的View其CLICKABL为false,比如Button是可点击的,TextView是不可点击的。通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性。
View的滑动冲突
外部拦截法
外部拦截法是指点击事件都先经过父容器的拦截处理,如果父事件需要此事件就拦截,如果不需要此事件就不拦截。外部拦截法需要重写父容器的onInterceptTouchEvent方法。这种方法的伪代码如下所示:public boolean onInterceptTouchEvent(MotionEvent event){ boolean intercepted = false; switch(event.getAction()): case Motion.ACTION_DOWM: intercepted = false//不拦截 break; case Motion.ACTION_MOVE: if(父容器需要当前点击事件){ intertepted = true; }else{ intercepted = false; } break; case Motion.ACTION_UP: intercepted = false; break; return intercepted; }
上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要修改也不能修改。在onTerceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,否则父容器一旦拦截ACTION_DOWM,那么后续所有事件都会交给父容器处理,这个时候就没法在向下传递了。同时ACTION_UP这个事件,父容器也必须返回false,这是因为如果父容器拦截了ACTION__UP,那么其子元素的onClick事件就无法触发。(onClick事件是在View的onTouchEvent方法中被触发的)。
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:public boolean dispatchTouchEvent(MotionEvent event){ switch(event.getAction()){ case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if(父容器需要此类点击事件){ parent.requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } return super.dispatchTouchEvent(event); }
同时父元素所做修改如下所示:
public boolean onIntercptTouchEvent(MotionEvent event){ int action = event.getAction(); if(action == MotionEvent.ACTION_DOWN){ return false; }else{ return true; } }
——2016年6月14日15:03:55
——写于宿舍
——雨
相关文章推荐
- android关于异常退出的学习
- Android中使用Handler引发的内存泄露
- Android搭建客户端,实现手机和服务器的交互
- Android中的Drawable资源—— LevelListDrawable
- 安卓设计的一些东西
- Android之Fragment(一):静态使用碎片
- android学习4#--使用Intent创建Activity
- mImageView.setBackgroundResource(R.anim.loading_anim);在studio中会报错
- Android中的Drawable资源—— StateListDrawable
- Android 开发之 手势开发
- Android NDK 编译 引用第三方 so 文件
- Android ListViewview入门
- edittext只能输入数字和小数点,且小数点后只能输入两位
- 初识Android Studio中的Gradle
- Android的manifest配置
- 反射
- Android下使用W25Q32
- 检测和解决Android应用的性能问题
- ScrollView冲突问题
- Android中bindService()启动Service的过程分析