在平常的开发中,我们经常会遇到点击,滑动之类的事件。有时候不同的view之间也存在各种滑动冲突。比如布局的内外两层都能滑动的话,那么就会出现冲突了。这个时候我们就需要了解Android的事件分发机制。
介绍
Android的触摸事件分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。我先将这三个方法大体的介绍一下。
- public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。ACTION_DOWN的dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
- public boolean onInterceptTouchEvent(MotionEvent event)
这个方法是在dispatchTouchEvent方法中调用的,用来拦截某个事件的。如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回的结果表示是否拦截当前事件。它是ViewGroup提供的方法,默认返回false。
- public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗掉当前事件(true表示消耗,false表示不消耗),如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。View和ViewGroup都有该方法,View默认返回true,表示消费了这个事件。
View里,有两个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
ViewGroup里,有三个回调函数 :
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
上述三个方法中有什么区别和关系呢?下面用一段伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通过上面的伪代码大家可能对点击事件的传递规则有了更清楚的认识,即:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true表示它要拦截此事件,接着这个事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截此事件,这是当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
下面的几张图参考自[eoe]:
- 图一:ACTION_DOWN都没被消费
图二(一):ACTION_DOWN被View消费了
图二(二):后续ACTION_MOVE和UP在不被拦截的情况下都会去找VIEW
图三:后续的被拦截了
图四:ACTION_DOWN一开始就被拦截
View事件分发源码分析
dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
如果mOnTouchListener != null,(mViewFlags&ENABLED_MASK)==ENABLED和mOnTouchListener.onTouch(this, event)这三个条件都为真,就返回true,否则就去执行onTouchEvent(event)方法并返回。
总结下来onTouch能够得到执行需要两个前提条件(都满足):
1.设置了OnTouchListener
2.控件是enable状态
而onTouchEvent能够得到执行满足以下三个条件任意一个即可:
1.没有设置OnTouchListener
2.控件不是enable状态
3.onTouch返回false
再来看一下dispatchTouchEvent的返回值,它其实受onTouch和onTouchEvent函数的返回值控制,也就是说touch事件被成功消费返回true,它也就返回true,说明分发成功,此后的事件序列也会在此被分发,而如果返回false,则认为分发失败,此后的事件序列就不再分发下去了。
onTouchEvent方法
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
...
return true;
}
View的onTouchEvent默认都会消耗掉事件(该方法返回true),除非它是不可点击的(clickable和longClickable同时为false)。并且View的longClickable默认为false,clickable属性要分情况,比如Button默认为true,TextView、ImageView默认为false。
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
这不就是我们熟悉的OnClickListener吗,它原来是在onTouchEvent中被调用的。只要mOnClickListener不是null,就会去调用它的onClick方法。
总结下来onClick能够得到执行需要两个前提条件(都满足):
1.可以执行到onTouchEvent
2.设置了OnClickListener
整个View的事件转发流程是:
dispatchEvent->setOnTouchListener->onTouchEvent->setOnClickListener
最后还有一个问题,setOnLongClickListener和setOnClickListener是否只能执行一个?
答:不是的,只要setOnLongClickListener中的onClick返回false,则两个都会执行;返回true则会屏蔽setOnClickListener。
ViewGroup事件分发源码分析
dispatchTouchEvent方法
...
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
}
}
}
}
两种可能会进入if代码段(即事件被分发给子View):
1、当前不允许拦截,即disallowIntercept = true.
2、当前没有拦截,即onInterceptTouchEvent(ev)返回false.
注:disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,可以通过ViewGroup.requestDisallowInterceptTouchEvent(boolean)进行设置;而onInterceptTouchEvent(ev)可以进行复写。
进入if代码段后,通过一个for循环,遍历当前ViewGroup下的所有子View,判断当前遍历的View是不是正在点击的View,如果是的话就会调用该View的dispatchTouchEvent,就进入了View的事件分发流程了,上面有讲。当child.dispatchTouchEvent(ev)返回true,则为mMotionTarget=child;然后return true,说明ViewGroup的dispatchTouchEvent返回值受childView的dispatchTouchEvent返回值影响,子view事件分发成功,ViewGroup的事件分发才成功,此后的事件序列也会在此分发(从上面知:子view的clickable或longClickable为true都能分发成功),而如果ViewGroup事件分发失败或者没有找到子View(点击空白位置),则会走到它的onTouchEvent,以后的事件序列也不会分发下去,直接走onTouchEvent。
整个ViewGroup的事件转发流程是:
dispatchEvent->onInterceptTouchEvent->child.dispatchEvent->(setOnTouchListener->onTouchEvent)
上面的总结都是基于:如果没有拦截;那么如何拦截呢?
onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
代码很简单,只有一句,即返回false,ViewGroup默认是不拦截的。如果你需要拦截,只要return true就行了,这样该事件就不会往子View传递了,并且如果你在DOWN return true ,则DOWN,MOVE,UP子View都不会捕获到事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获到事件。
如何不被拦截:
如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;此时子View希望依然能够响应MOVE和UP时该咋办呢?
答:onInterceptTouchEvent是定义在ViewGroup中的,子View无法修改。Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:
@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
getParent().requestDisallowInterceptTouchEvent(true);
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。
注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN里面直接return true了,那么子View是没有办法的捕获事件的!
总结
关于代码流程上面已经总结过了~
1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
好了,那么实际应用中能解决哪些问题呢?
比如你在ScrollView中嵌套了一个EditText,当EditText中文字内容太多超出范围时,你想上下滑动使EditText中文字滚动出来,却发现滚动的是ScrollView。这时我们设置EditText的onTouch事件,在onTouch中设置不让ScrollView拦截我的事件,最后在UP时把状态改回去。
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if ((view.getId() == R.id.tousuContentEditText && canVerticalScroll(tousuContentEditText))) {
view.getParent().requestDisallowInterceptTouchEvent(true);
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
view.getParent().requestDisallowInterceptTouchEvent(false);
}
}
return false;
}
private boolean canVerticalScroll(EditText editText) {
int scrollY = editText.getScrollY();
int scrollRange = editText.getLayout().getHeight();
int scrollExtent = editText.getHeight() - editText.getCompoundPaddingTop() - editText.getCompoundPaddingBottom();
int scrollDifference = scrollRange - scrollExtent;
if (scrollDifference == 0) {
return false;
}
return (scrollY > 0) || (scrollY < scrollDifference - 1);
}