View的事件分发机制和滑动冲突解决
View的事件分发机制
概念
所谓的事件分发机制就是对MotionEvent事件的分发过程,当一个MotionEvent产生以后,系统需要把它分发给一个具体的View,这个传递的过程就是事件分发机制
重要方法
这套机制涉及到三个重要的方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用于进行事件的分发,返回值受当前View的onTouchEvent()方法和下级的dispatchTouchEvent()影响
2. public boolean onInterceptTouchEvent(MotionEvent ev)
在1方法的内部调用,用于判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件
3. public boolean onTouchEvent(MotionEvent ev)
也是在1方法内部调用,用来处理点击事件,返回结果标识是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接受到事件
用伪代码表示他们的关系是:
private boolean dispatchTouchEvent(MotionEvent ev){
boolean result = false;
if (onInterceptTouchEvent(ev)){
result = onTouchEvent(ev);
}else {
result = child.dispatchTouchEvent(ev);
}
return result;
}
事件流程
先用一张图来说明大致流程
点击事件的传递规则:对于一个根ViewGroup,点击事件产生后,首先会传递给他,这时候就会调用他的dispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View的dispatchTouchEvent方法,如此反复直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。
由此可见处理事件时的优先级关系: onTouchListener > onTouchEvent >onClickListener
关于事件传递的机制,这里给出一些结论:
- 一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
- 正常情况下,一个事件序列只能由一个View拦截并消耗。
- 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent
不会再被调用。 - 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
- ViewGroup默认不拦截任何事件。
- View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被调用。
- View的onTouchEvent默认消耗事件,除非他是不可点击的( clickable和longClickable同时为false) 。View的longClickable属性默认false,clickable默认属性分情况(如TextView为false,button为true)。
- View的enable属性不影响onTouchEvent的默认返回值。
- onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
- 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。
源码分析
首先看ViewGroup的dispatchTouchEvent()方法:
这里提一下,事件最先是到Activity的dispatchTouchEvent()方法->PhoneWindow->DecorView->最后才是最顶级的ViewGroup,如果事件最后全没处理,是会交给Activity处理
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
// view没有被遮罩,一般都成立
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
//作为新一轮的开始,reset所有相关的状态
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
// 之前的某次事件已经经由此ViewGroup派发给children后被处理掉了
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 只有允许拦截才执行onInterceptTouchEvent方法
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 在这种情况下,actionMasked != ACTION_DOWN && mFirstTouchTarget == null
// 第一次的down事件没有被此ViewGroup的children处理掉(要么是它们自己不处理,要么是ViewGroup从一开始的down事件就开始拦截),则接下来的所有事件
// 也没它们的份,即不处理down事件的话,那表示你对后面接下来的事件也不感兴趣
intercepted = true;
}
先一步步看,从上面的代码,可以看出,有两种情况下,才会去判断是否要拦截当前事件
- actionMasked == MotionEvent.ACTION_DOWN
这个好理解,就是判断按下的事件
- mFirstTouchTarget != null
当事件由ViewGroup的子类成功处理时,mFirstTouchTarget会被赋值指向子元素,换句话说,当ViewGroup不拦截事件,将事件交给子元素处理时,mFirstTouchTarget才不为空,这样当move和up事件到来时,由于这两个条件为false,就导致onInterceptTouchEvent()不会被调用,并且同一序列默认交由它处理
说明1:比如A包含B,B包含C和D,C处理的事件那么A的mFirstTouchTarget就是指向B,B的mFirstTouchTarget就是指向C
很重要的一个点:如果ViewGroup拦截事件,那么mFirstTouchTarget就为null
这里有个特殊情况:FLAG_DISALLOW_INTERCEPT标记位,这个标记位一般用于子View,通过requestDisallowInterceptTouchEvent()方法设置,一旦设置这个标记位,那么ViewGroup就不能拦截除了down以外的事件,这是因为每次down的时候都会重置这个标记位,导致子view中设置的这个标记位无效
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
这个方法里面mFirstTouchTarget也会置位null;
每一个事件开始的Down事件,ViewGroup都会询问自身的onInterceptTouchEvent是否要拦截事件
这里总结两点:
- onInterceptTouchEvent()方法不是每次都调用,如果想处理所有的事件,需要在dispatchTouchEvent()中去处理,只有它是每次都会调用,当然,前提是事件能到达当前的ViewGroup
- FLAG_DISALLOW_INTERCEPT标记位提供了一种思路,可以用来解决手势冲突
接下来就是不拦截事件,对事件进行分发
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
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();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
第一步是遍历所有的子元素,判断是否能够接受到点击事件,判断依据是两个
- 是否在播放动画
- 是否落在子元素的点击区域内
如果某个子元素满足这两点,那么事件就交给他处理
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的dispatchTouchEvent返回为true.那么mFirstTouchTarget就会被赋值,并且跳出for循环
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
跟进去看一下mFirstTouchTarget的赋值过程
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
可以看到,其实TouchTarget是一个单链表结构,mFirstTouchTarget是否被赋值,直接影响到ViewGroup的拦截策略,如果mFirstTouchTarget为null的话,ViewGroup就会默认拦截同一事件序列的所有事件
如果遍历完所有的子元素都没有找到人处理事件,这包含两种情况
- 没有子元素
- 子元素处理得了点击事件,但是在dispatchTouchEvent()方法返回了false,这一般是因为在onTouchEvent中返回了false
在这两种情况下,ViewGroup会自己处理事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
这里参数传递的为null,由下面这行源码可知:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
事件交由super.dispatchTouchEvent(event)去处理,也就是View的dispatchTouchEvent()方法,跟进去看一下
View对点击事件的处理
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//这里使用&&,前面的如果返回false,直接跳过,不会走到后面的判断
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
View对事件处理过程就很简单,因为View是一个单独的元素,他没有子元素往下传递,只能自己处理
首先会判断是否设置了OnTouchListener,如果OnTouchListener的onTouch()方法返回了true,那么就不会调用onTouchEvent了,这样是方便在外部处理点击事件
接下来看view的onTouch事件
先看view处于不可用状态下,事件的处理过程
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
return clickable;
}
从上面代码看,只要view的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管它是不是不可用状态
另外还需要注意的一个很重要的点:
case MotionEvent.ACTION_UP:
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
break;
当up事件触发的时候,会触发PerformClick()方法,跟进去看一下
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//调用设置的onClick()监听
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
也就是说click()事件是在onTouch()里面的up去调用,如果onTouch()都没有接收到,click()更是别想了
小知识点1:View的LONG_CLICKABLE属性默认是false,而CLICKABLE和具体的view有关,例如Button默认为true,TextView默认为false,通过设置onClickListener()和setLongClickListener()会改变他们的默认属性为true,
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
到这里,点击事件的分发机制源码差不多就算分析完了,下面介绍一下常用的滑动冲突解决方法
解决滑动冲突的核心就是在特定的条件下,选择父布局接受事件还是子view接受事件,不管多复杂的滑动冲突,他们之间仅仅是滑动规则不同而已,下面是解决方法
- 外部拦截法
顾名思义,就是在外部拦截,父布局需要事件,就拦截,不需要就不拦截,所以需要重写onInterceptTouchEvent()方法,下面是代码:
public boolean onInterceptTouchEvent (MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default :
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
针对不同冲突,只需修改父容器需要当前事件的条件即可。其他不需修改也不能修改。
ACTION_DOWN:必须返回false。因为如果返回true,后续事件都会被拦截,无法传递给子View。
ACTION_MOVE:根据需要决定是否拦截
ACTION_UP:必须返回false。如果拦截,那么子View无法接受up事件,无法完成click操作。而如果是父容器需要该事件,那么在ACTION_MOVE时已经进行了拦截,根据上一节的结论3,ACTION_UP不会经过onInterceptTouchEvent方法,直接交给父容器处理。
- 内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是伪代码:
public boolean dispatchTouchEvent ( MotionEvent event ) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。因此,父元素要做以下修改:
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
这样就能完成一个手势冲突,基本原则就是这,复杂的冲突也是在这个基础上去解决
下面是阅卷易一个解决手势冲突的代码
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mX = ev.getX();
mY = ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(ev.getX() - mX) > Math.abs(ev.getY() - mY)) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
滑动冲突需要好好的思考,理解其基本思想,就可以很好的解决冲突
参考书籍:Android开发艺术探索