事件类型MotionEvent
MotionEvent的事件有三种类型:
- ACTION_DOWN
- ACTION_MOVE
- ACTION_UP
当你点击一次屏幕的时候,整个过程中包含了一个ACTION_DOWN开始事件和多个的ACTION_MOVE以及一个ACTION_UP终止事件。当然如果没有在屏幕上滑动的也就没有ACTION_MOVE事件啦。
事件分发机制
通常我们的点击事件传递过程是Activity->Window->DecorView(View的事件分发机制),接下来具体介绍下这三者的事件传递过程:
1. Activity#dispatchTouchEvent的过程
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();// 该方法默认没有实现内容,子类Activity可在ACTION_DOWN发生时做特定互动
}
if (getWindow().superDispatchTouchEvent(ev)) {// 交由Window分发处理
return true;
}
return onTouchEvent(ev);// 整个View树的onTouchEvent都返回false(不消费事件),仍然由Activity自身处理
}
dispatchTouchEvent是系统事件传递地开端,是Window.Callback的一个重要回调方法,系统将屏幕点击事件传递于此。
2. PhoneWindow#superDispatchTouchEvent的过程
Window是一个抽象类,superDispatchTouchEvent也是个抽象方法,PhoneWindow是其唯一实现类。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);// mDecor就是DecorView,即getWindow.getDecorView()返回的那个View
}
3. 顶级View对事件的分发过程
我们先了解下几个会在View事件分发中使用到的方法:
- onTouchEvent 处理点击事件
- onInterceptTouchEvent 是否拦截处理点击事件,在ViewGroup才有
- dispatchTouchEvent 分发事件
以上三个方法的返回结果:
- false,表示事件在当前view未消耗,则继续往view树下层传递;
- true,表示事件在当前view已被消耗,则不再传递。
一段伪代码表示以上三个方法的关系:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
那么可能有同学会问,不是还有onTouchListener和onClickListener吗?
这三者的优先级是onTouchListener > onTouchEvent > onClickListener,我们可以从View源码来验证。
public boolean dispatchTouchEvent(MotionEvent event) {
....
boolean result = false;
....
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {// OnTouchListener优先调用(可用状态下)
result = true;
}
if (!result && onTouchEvent(event)) {// 若OnTouchListener返回true,则onTouchEvent被屏蔽
result = true;
}
}
....
return result;
}
onTouchListener若存在,onTouch方法返回true则会屏蔽掉onTouchEvent。
现在来看ViewGroup#dispatchTouchEvent,分段来说明:
1) requestDisallowInterceptTouchEvent设置FLAG_DISALLOW_INTERCEPT状态后,将使ViewGroup无法拦截除ACTION_DOWN以外的其他点击事件。换言之,尽管子元素有优先禁用父容器的拦截功能,但是对于ACTION_DOWN事件是个特例;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();// ACTION_DOWN事件重置了FLAG_DISALLOW_INTERCEPT标记位
}
2) 这部分代码描述当前ViewGroup是否拦截点击事件的这个逻辑
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {// ACTION_DOWN动作和子元素是否成功处理(null表示没有子元素处理)
// 若是ACTION_DOWN事件,FLAG_DISALLOW_INTERCEPT标记位会被清除,disallowIntercept为false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {// 若子元素未禁止父容器的拦截功能(ACTION_DOWN肯定会执行)
intercepted = onInterceptTouchEvent(ev);// ViewGroup自己决定是否拦截
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. 即没有子view处理时(mFirstTouchTarget 若不为空说明子元素处理成功),事件由当前ViewGroup拦截,不再调用onInterceptTouchEvent来决定是否拦截
intercepted = true;
}
由上源码分析,我们可得出结论:
- ViewGroup决定拦截事件,那后续事件将默认交由处理,不需要再调用onInterceptTouchEvent
- FLAG_DISALLOW_INTERCEPT标记位的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件,ViewGroup若要拦截,则标记位设置了也无效
总结
我们最后完整的理一遍整个流程:一个点击事件首先由Activity接收,Activity调用dispatchTouchEvent进行分发,优先传递给Window进行处理,如果Window不消耗该事件则再由Activity的onTouchEvent来处理;Window处理过程则直接委托给DecorView进行事件分发,这个DecorView是android.R.id.content的父View,而android.R.id.content的子View就是我们Activity的视图view。
接下来就进入了View的事件分发过程:
如果我们的Activity视图中的顶级ViewGroup拦截事件,即onInterceptTouchEvent返回true,则事件由该层ViewGroup处理,如果设置了onTouchListener,则onTouch被调用,否则onTouchEvent会被调用,如果还有onClickListener,则onClick最后被调用;若ViewGroup不拦截,则事件传递到点击事件链上的子View,子View的dispatchTouchEvent被调起,依上如此循环,完成事件分发。
注意点
- 一个事件序列(开始-结束)只能被一个View拦截消耗
- 子view一旦开始处理事件,ACTION_DOWN事件就应该返回true;若返回false,则后续事件序列不再交由处理,重新由其父元素onTouchEvent去处理。
- ViewGroup默认不拦截任何事件
- View只要可点击的,onTouchEvent默认会消耗事件
- 子View可以通过requestDisallowIntercceptTouchEvent干预父元素的事件分发,ACTION_DOWN事件除外
网友评论