用户在操作的时候,不可避免地就会触发触摸事件。Android把触摸过程分成很多个动作(Action),而开发中最常见也最主要考虑的触摸过程就是:从ACTION_DOWN(触摸落下)开始、到ACTION_UP(触摸弹起)/ACTION_CANCEL(触摸取消,譬如在按下控件,将控件移动到外层控件的时候,就会触发,而不是UP)结束,在这之间的是ACTION_MOVE(触摸移动,非必然存在)。
同时,我们在界面上触发触摸事件的时候,同样不可避免地会涉及到这三部分:Activity(可视界面当然还有Fragment,Dialog这些,但它们都依附着Activity),ViewGroup(通常来说做布局容器的LinearLayout、RelativeLayout等都算是),View(Button、ImageView等等)。一个简单的情况就是我们在点击界面(Activity)上在布局(ViewGroup)中的Button(View)的时候,这必然触发了触摸事件,但这具体是怎样的一个过程?我们可以在这些过程中做些什么?这就涉及到了Android触摸事件分发机制,先看一张简略的分发流程图(图片来源于从Android源码的角度理解应用开发(1)-Touch机制,感谢,侵删):
从流程图可以得知,Touch事件的分发情况是这样的,Activity将事件分发到ViewGroup中,而ViewGroup层层分发直到找到需要处理Touch事件的子元素(可能是View也可能是ViewGroup),将事件传递下去。这里也可以提前告知一个逻辑,就算传递到了,但如果子元素不能处理触摸事件,会将事件交回上一级处理,最后可以到Activity去处理,总之触摸事件最终会被消费掉。
先让我们来了解一下Touch事件分发和处理的三个重要方法。
1.public boolean dispatchTouchEvent(MotionEvent ev)
MotionEvent-手势事件,它里面就包含了上面说的Action。这个方法从字面上的意思都很好理解,调度触摸事件,这个方法是Activity、ViewGroup、View都有的,Touch事件都从它开始,也就是说Touch事件的分发和处理过程中,dispatchTouchEvent()是第一个被调用的方法。
2.public boolean onInterceptTouchEvent(MotionEvent ev)
也是从字面意思上就很好理解了,拦截触摸事件。如果返回值为true,就拦截当前事件,不分发给子元素。很明显这个方法View是没有的,可以说三者中只有ViewGroup才有,因为Activity肯定要把事件分发下去(后面有说到),而View下面是没有子元素的,要么处理触摸事件要么交回ViewGroup处理。
3.public boolean onTouchEvent(MotionEvent ev)
这个方法表示对事件进行处理,在dispatchTouchEvent方法内部调用,如果返回true表示消耗当前事件,如果返回false表示不消耗当前事件。
为了更好地理解整个流程,我们从View——>ViewGroup——>Activity的顺序展开,让我们先来看看是怎么处理Touch事件的,再看具体是怎么分发的。就以上面说到的按钮点击的情况来说一说。
View
上面的按钮点击中,View就是Button,刚刚说到整个过程最先被调用的就是dispatchTouchEvent(),Button的dispatchTouchEvent()是调用View的,我们来看一下这个方法:
可以看到,if语句的判断条件有三个部分,只要有一部分条件是false,那么就会执行onTouchEvent()。
1.判断是否有注册触摸监听事件;
2.判断控件是否可用(ENABLE),一般控件是默认可用的,除非通过setEnable(false)禁用控件,不然这部分条件一般为true;
3.监听事件里面的onTouch()的返回值。
所以很简单的,控件监听触摸事件让onTouch()返回true,就不会再执行下去了,当然我们是要看下去的,看一下onTouchEvent()里面实现了什么?
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
...
break;
case MotionEvent.ACTION_DOWN:
...
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
这里我们可以简单分析一下,这里面主要有三块if语句:
1.如果控件是禁用(DISABLED)的,控件可以点击(CLICKABLE)或长按点击(LONG_CLICKABLE),返回true,消费事件但不做任何操作,如果控件不可点击,onTouchEvent()返回false;
2.如果存在触摸委托对象(TouchDelegate),交由其onTouchEvent()处理;
3.首先可以看到,只要控件是不可点击的,不满足if语句条件,直接返回false;反之,再处理不同的ACTION,都会返回true。
有意思的是,继续深入查看源码可以发现,控件的onLongClick事件是在ACTION_DOWN里面触发的(postDelayed())、onClick事件是在ACTION_UP里面触发的(performClick()),这里不展开说。
上面说了那么多,好像和分发没什么关系啊?别急,我们可以知道View的Touch事件其实都是为了知道View的dispatchTouchEvent()的返回值是什么,而这个值与分发事件大有关联,请看ViewGroup。
ViewGroup
从上面简略的流程图可以看出,触摸事件由ViewGroup传递给View,很显然,这就是一个事件分发过程,那么,这个过程是怎么做的呢?
ViewGroup首先调用dispatchTouchEvent(),让我们来看一看具体的实现:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
...
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)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
...
if (target == null) {
...
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
...
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
...
return target.dispatchTouchEvent(ev);
}
我们可以看到,首先对ACTION_DOWN做了一大堆处理,来看一下具体在干嘛。
首先清除手势目标;
第二个是重点:
disallowIntercept-禁止拦截,这个变量是一个Boolean值,默认值是false,意味着一般情况下是不禁止拦截功能的;
onIntercepTouchEvent()-这个方法在上面也提到过,拦截事件分发下去,将触摸事件交给ViewGroup自己处理。
很好,那么
if (disallowIntercept || !onInterceptTouchEvent(ev))
这个条件语句的意思是只要不拦截,就进入条件判断内部,它的逻辑运算符是||,只要disallIntercept为true或者!onInterceptTouchEvent(ev)为true,也就是onInterceptTouchEvent(ev)为false就好了。
第一个条件,disallIntercept默认为false,但我们可以通过requestDisallowInterceptTouchEvent(Boolean)使得它的值为true;
第二个条件,看一下onInterceptTouchEvent(ev)的源码:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
看!它的返回值是默认false的!也就是!onInterceptTouchEvent(ev)为true,那么也就是默认情况下,并不拦截事件,进入条件判断内部。
这一大块代码具体做了什么?
1.获得ViewGroup下的子View
2.for循环判断,手指落下的范围在哪个View之中,接着!
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
上面说了那么多View里面获取dispatchTouchEvent()的返回值,在这里用上了,确定分发目标为dispatchTouchEvent()返回值为true的子View!同时,ViewGroup的dispatchTouchEvent()的返回值也是true,那么ACTION_DOWN就结束了,再次进来就是其它ACTION的执行逻辑了。
所以如果,View的dispatchTouchEvent()返回值为false,target为空,那么可以看到往下的逻辑里面,将自身作为一个View设为target:
if (target == null) {
···
return super.dispatchTouchEvent(ev);
}
也就是View的onTouch()返回值为true,View禁用,View的onTouchEvent()返回false等种种情况都会导致把事件重新交回给ViewGroup,然后ViewGroup执行super.dispatchTouchEvent(ev),ViewGroup的超类是View,又回到了前面说的View的内容了。
总结(来自于从Android源码的角度理解应用开发(1)-Touch机制,很精准简练的总结,感谢,侵删):
1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target;
2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch());
3.如果有Target并且拦截,则发送Cancel给子View ;
4.如果有Target并且不拦截,则调用Target的dispatchTouch;
5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)。
接着就剩下最后一块了,我们来看一下Activity又是怎么样把事件分发给ViewGroup的。
Activity
还是先看dispatchTouchEvent():
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
代码很简单,但是其实经过一系列的步骤之后,传递过程是这样的:
Activity->Window->DecorView->ViewGroup,同理,如果最后Touch事件没有被消费,也会交回由Activity的onTouchEvent()里面去处理。
补充: ACTION的传递过程:
对于在onTouchEvent消费事件的情况:
在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。
对于ACTION_MOVE、ACTION_UP总结:
ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。