Android触摸事件分发机制

用户在操作的时候,不可避免地就会触发触摸事件。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机制,感谢,侵删):

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的,我们来看一下这个方法:

Android触摸事件分发机制_第2张图片
View的dispatchTouchEvent()实现

可以看到,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处理并结束传递。

你可能感兴趣的:(Android触摸事件分发机制)