【自定义View系列】04--谈谈事件分发
2016-07-09 22:01
399 查看
引言:这部分会分三个模块来讲,先讲View对Touch的处理,再讲ViewGroup的事件分发,最后讲如何解决滑动冲突。
我习惯通过在源码中添加注释来理解源码,以下是我提取出来几个重要方法,将不重要的部分删掉,并且添加了中文注释。
onTouchEvent()是决定事件是否被消耗的最后一道门,如果返回false,则它的父View的onTouchEvent会被调用,否则不会;
先来看看重要部分的源码:
引用一张谷歌的小弟画的流程图:
需要注意的点:
onTouch()与onTouchEvent()以及click三者的区别和联系 :
onTouch()与onTouchEvent()都是处理触摸事件的API
onTouch()属于TouchListener接口中的方法,是View暴露给用户的接口便于处理触摸事件,而onTouchEvent()是Android系统自身对于Touch处理的实现
先调用onTouch()后调用onTouchEvent()。而且只有当onTouch()未消费Touch事件才有可能调用到onTouchEvent()。即onTouch()的优先级比onTouchEvent()的优先级更高。
在onTouchEvent()中处理ACTION_UP时会利用ClickListener执行Click事件。所以Touch的处理是优先于Click的
简单地说三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()
View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有,请勿混淆
事件分发体系最重要的几个方法:
dispatchTouchEvent(event)
主要完成事件分发的逻辑,只要事件到达该View,一定会调用这个方法,返回值表示是否消耗当前事件。
onInterceptTouchEvent(Event)
判断是否拦截某个事件,返回值表示是否拦截
onTouchEvent(Event)
用来处理Touch事件,返回值表示是否消耗该事件。
他们的关系可以这样表示:
来看看源码:
最后会调用dispatchTransformedTouchEvent
最后会递归的执行子View的这套流程,或者被ViewGroup自身拦截掉,亲自用onTouchEvent处理这次事件
onInterceptTouchEvent()表示是否拦截此次事件,ViewGroup的默认实现是不拦截,return false;所以我们往往可以在自定义控件中重写这个方法,来决定什么情况下拦截事件
总结下来就是:
Touch事件的传递顺序为 :
Activity–>外层ViewGroup–>内层ViewGroup–>View
如果Touch事件在中间某一层被拦截了,DOWN事件将不会再传递给更底层的View
Touch事件的消费顺序为 :
View–>内层ViewGroup–>外层ViewGroup–>Activity
如果Touch事件在中间某一层被消费了,将不会再通知更上层的View,只有当所有子View都不消费Touch事件,顶层ViewGroup才会自己处理这次Touch事件。
有两个比较常见的解决方案:
1、在父View中准确地进行事件分发和拦截
比如重写onInterceptTouchEvent()和onTouchEvent(),对事件进行正确的分配,保证在合适的时候Touch时间可以传递给子View
2、使用Google在support.v4包提供的两个支持嵌套滚动的接口:onNestedScrollChild、onNestedScrollParent。(有一个例子,在我的Github上一个快速开发框架里的下拉刷新SwipeLayout中有用到,贴上地址:https://github.com/miomin/Shareward)
ViewGroup的onInterceptTouchEvent默认返回false,也就是默认不会拦截事件,交给下层的View来处理。
View没有onInterceptTouchEvent方法,一旦有事件到达,就会调用onTouchEvent。
可点击的View的onTouchEvent默认返回true,也就是消耗事件。
我习惯通过在源码中添加注释来理解源码,以下是我提取出来几个重要方法,将不重要的部分删掉,并且添加了中文注释。
一、先从View讲起
如果一个View(比如Button)接收到Touch,那么该Touch事件首先会传入到它的dispatchTouchEvent( )方法,所以我们从这里开始学习View对Touch事件的处理。// 返回值表示Touch事件是否被该View消费 public boolean dispatchTouchEvent(MotionEvent event) { //result的值决定最后该方法的返回值,也就是决定Touch事件是否被消费 boolean result = false; /***/ if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; //该if判断中一共包含了4个条件,必须同时满足时才表示Touch事件被消费 //在这四个条件中,我们通常最关心的就是最后一个:TouchListener的onTouch()方法。假如这四个条件中的任意一个不满足,那么result仍为false;则进入下一步调用自身的onTouchEvent() if (li != null && li.mOnTouchListener != null && (mViewFlags&ENABLED_MASK)==ENABLED && li.mOnTouchListener.onTouch(this,event)) { result = true; } //调用View自身的onTouchEvent()处理Touch事件,由onTouchEvent的返回值决定result的值(当然,如果在上一步中,如果已经将result设为true,就不会去判断onTouchEvent()了) if (!result && onTouchEvent(event)) { result = true; } } /***/ return result; }
onTouchEvent()是决定事件是否被消耗的最后一道门,如果返回false,则它的父View的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(); //如果一个View是disable的,CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE消耗掉,且不会触发onClick事件回调 if ((viewFlags & ENABLED_MASK) == DISABLED) { return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //如果View不是disable的,会继续执行,对CLICK,LONG_CLICK,CONTEXT_CLICKABLE进行处理 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_DOWN: /***/ break; case MotionEvent.ACTION_MOVE: /***/ break; case MotionEvent.ACTION_CANCEL: /***/ break; case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { /***/ if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { /***/ if (!focusTaken) { /***/ if (!post(mPerformClick)) { //performClick()中会回调onClick,所以我们平时常见的onClick回调都是在ACTION_UP的时候触发的 performClick(); } } } } /***/ } //这里表示,只要View是enable的,Touch事件都会被消耗掉 return true; } return false; }
引用一张谷歌的小弟画的流程图:
需要注意的点:
onTouch()与onTouchEvent()以及click三者的区别和联系 :
onTouch()与onTouchEvent()都是处理触摸事件的API
onTouch()属于TouchListener接口中的方法,是View暴露给用户的接口便于处理触摸事件,而onTouchEvent()是Android系统自身对于Touch处理的实现
先调用onTouch()后调用onTouchEvent()。而且只有当onTouch()未消费Touch事件才有可能调用到onTouchEvent()。即onTouch()的优先级比onTouchEvent()的优先级更高。
在onTouchEvent()中处理ACTION_UP时会利用ClickListener执行Click事件。所以Touch的处理是优先于Click的
简单地说三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()
View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有,请勿混淆
二、ViewGroup的事件分发
Touch事件会从PhoneWindow开始一直传递到最顶层的ViewGroup,然后调用到最顶层的dispatchTouchEvent()事件分发体系最重要的几个方法:
dispatchTouchEvent(event)
主要完成事件分发的逻辑,只要事件到达该View,一定会调用这个方法,返回值表示是否消耗当前事件。
onInterceptTouchEvent(Event)
判断是否拦截某个事件,返回值表示是否拦截
onTouchEvent(Event)
用来处理Touch事件,返回值表示是否消耗该事件。
他们的关系可以这样表示:
public boolean dispatchTouchEvent(MotionEvent e) { if(onInterceptTouchEvent(ev)) { //如果拦截,就自己处理,调用自己的onTouchEvent,如果onTouchEvent返回true就消费掉,如果返回false就传给上层处理 return onTouchEvent(ev); } else { //如果不拦截,就走子view的分发流程 return child.dispatchTouchEvent(ev); } }
来看看源码:
public boolean dispatchTouchEvent(MotionEvent ev) { if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { //如果是DOWN事件,进行初始化和还原操作 cancelAndClearTouchTargets(ev); resetTouchState(); } // 判断是否需要拦截事件,根据intercepted的值确定 // mFirstTouchTarget用于多点触控 // mFirstTouchTarget不为空,表示有子View消费了Touch事件 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 如果是DOWN或者有子View消费,则根据onInterceptTouchEvent判断是否拦截 // ViewGroup的onInterceptTouchEvent默认返回false,也就是不拦截 // 所以我们往往可以在自定义控件中重写这个方法,来决定什么情况下拦截事件 intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { // 如果disallowIntercept == false,就是不允许拦截,可以在子View设置不允许父View拦截 intercepted = false; } } else { // 执行到这里,说明mFirstTouchTarget为null或者不是DOWN事件,需要ViewGroup自己处理此次Touch事件 // 也就是拦截本次Touch事件 intercepted = true; } // 如果Touch事件没有被取消也没有被拦截,那么ViewGroup将类型为ACTION_DOWN的Touch事件分发给子View。 if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 根据Touch事件的坐标,找到触摸到了哪个子View final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } /***/ // 找到之后,调用dispatchTransformedTouchEvent,传入子view // 子View没有消费Touch事件则该方法的返回值为false,此时mFirstTouchTarget仍为null // 如果子View消费掉了Touch事件那么该方法的返回值为true,然后执行newTouchTarget = addTouchTarget(child, idBitsToAssign); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // 如果子View消费掉了Touch事件那么该方法的返回值为true,然后执行 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } } } // mFirstTouchTarget为空,表示没有子View消耗Touch事件,需要ViewGroup自己处理 if (mFirstTouchTarget == null) { // 同样也会调用dispatchTransformedTouchEvent,但是传入Null,标明由父View自己处理这次Touch事件 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { /***/ } /***/ } }
最后会调用dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; /***/ // 如果child == null,表明没有子View消费这次Touch事件 if (child == null) { // 所以会调用super.dispatchTouchEvent,此时,ViewGroup就化身为了普通的View,它会在自己的onTouch(),onTouchEvent()中处理Touch handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } // 如果child不为空,表示有子View处理这次Touch事件,直接调用child的dispatchTouchEvent // 当然该view可能是一个View也可能是一个ViewGroup handled = child.dispatchTouchEvent(transformedEvent); } // Done. transformedEvent.recycle(); return handled; }
最后会递归的执行子View的这套流程,或者被ViewGroup自身拦截掉,亲自用onTouchEvent处理这次事件
onInterceptTouchEvent()表示是否拦截此次事件,ViewGroup的默认实现是不拦截,return false;所以我们往往可以在自定义控件中重写这个方法,来决定什么情况下拦截事件
总结下来就是:
Touch事件的传递顺序为 :
Activity–>外层ViewGroup–>内层ViewGroup–>View
如果Touch事件在中间某一层被拦截了,DOWN事件将不会再传递给更底层的View
Touch事件的消费顺序为 :
View–>内层ViewGroup–>外层ViewGroup–>Activity
如果Touch事件在中间某一层被消费了,将不会再通知更上层的View,只有当所有子View都不消费Touch事件,顶层ViewGroup才会自己处理这次Touch事件。
三、滑动冲突处理
引发原因:两个可以滑动的View互相嵌套,且滑动方向相同,则会产生滑动冲突。有两个比较常见的解决方案:
1、在父View中准确地进行事件分发和拦截
比如重写onInterceptTouchEvent()和onTouchEvent(),对事件进行正确的分配,保证在合适的时候Touch时间可以传递给子View
2、使用Google在support.v4包提供的两个支持嵌套滚动的接口:onNestedScrollChild、onNestedScrollParent。(有一个例子,在我的Github上一个快速开发框架里的下拉刷新SwipeLayout中有用到,贴上地址:https://github.com/miomin/Shareward)
四、需要注意的地方
一个事件序列指的是从手指按下的一刻起直到手指放开,通常情况下,一个事件序列只能被一个View拦截或消耗,拦截和消耗通常都是在DOWN事件进行,如果不对DOWN时间进行消费,则不会有机会消耗后续的MOVE事件,如果消耗了DOWN事件,后续的MOVE和UP事件同样由这个View消费。(support.v4包中的NestedScrolling接口可以打破这个原则,允许多个View同时处理同一个事件序列)ViewGroup的onInterceptTouchEvent默认返回false,也就是默认不会拦截事件,交给下层的View来处理。
View没有onInterceptTouchEvent方法,一旦有事件到达,就会调用onTouchEvent。
可点击的View的onTouchEvent默认返回true,也就是消耗事件。
相关文章推荐
- 使用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