Android手势&触摸事件的分发和消费机制

在Android 客户端开发过程中,经常会遇到手势事件的处理,本篇博文讲一下本人对touch事件处理机制的一些理解,希望对一些初级开发者有所帮助。

我们知道Android的视图是树形结构,如下图所示为例:


由于PhoneWindow和DecorView我们平时是不会有改动的,也用不到,所以我们只关注能用到的三个类:Activity、ViewGroup、View。Activity中有两个方法和touch事件有关,分别是dispatchTouchEvent和onTouchEvent。ViewGroup中有三个方法和touch事件有关,分别是dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。View中有两个方法和touch事件有关,分别是dispatchTouchEvent和onTouchEvent。

我们先讲一下dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法都是干什么的。

1、  dispatchTouchEvent方法是处理touch事件的入口,Activity、ViewGroup、View 都有这个方法,MotionEvent事件会首先由这个方法进行处理。方法负责MotionEvent的分发,如果其自身消费或者其孩子消费了,那么该方法会返回true,否则该方法会返回false,touch事件会返回到其上一层的dispatchTouchEvent方法,交由上层处理。

2、  onInterceptTouchEvent方法只有ViewGroup才有,也就是说只有容器控件才有这个方法,这个方法是ViewGroup用来拦截一个MotionEvent的,由dispatchTouchEvent调用。如果该方法返回true,也就意味着该ViewGroup拦截该touch事件,touch事件将不会分发给孩子,尤其自身的onTouchEvent方法处理;否则,意味着该ViewGroup不拦截touch事件,如果该ViewGroup有孩子,那么分发给孩子处理,如果没有孩子,则由自身的onTouchEvent方法处理。

3、  onTouchEvent方法是针对不同类型的touch事件来处理具体业务的方法,MotionEvent事件的类型主要有ACTION_DOWN、ACTION_MOVE、ACTION_UP等,这个方法里面根据不同的手势操作来处理UI,如果想让上层知道该touch事件被自己消费了,则返回true。

上面只是简单地说了说三个方法的作用,下面会从原理上面讲述touch事件的传递和消费机制,由于该过程十分复杂,描述清楚应该是不可能的事,所以我只能把自己的理解讲一讲,希望读者去其糟粕取其精。

我们以一个简单的UI层次Activity->ViewGroup->View来讲述从手指按下到抬起这期间安卓源码对touch事件的处理流程。

1、  首先Activity的dispatchTouchEvent捕捉到类型为ACTION_DOWN的手势事件,我们先看一下源码,这段源码较短 。

扫描二维码关注公众号,回复: 876191 查看本文章

//ActivitydispatchTouchEvent方法源码

public boolean dispatchTouchEvent(MotionEvent ev) {
   
if (ev.getAction()== MotionEvent.ACTION_DOWN) {
        onUserInteraction()
;
   
}
   
if (getWindow().superDispatchTouchEvent(ev)){
       
return true;
   
}
   
return onTouchEvent(ev);
}

通过源码可以看到,如果是事件类型为ACTION_DOWN,会调用onUserInteraction方法,这里跟touch事件的传递没有关系,不做过多阐述。getWindow().superDispatchTouchEvent(ev)这里会通过Window把touch事件传递给Activity的ViewGroup的dispatchTouchEvent方法,如果getWindow().superDispatchTouchEvent(ev)返回true,那么直接就返回true了,否则会调用Activity的onTouchEvent方法。白话得解释就是如果嵌在Activity的ViewGroup处理该touch事件,那么就不会执行自己的onTouchEvent,相反,如果Activity的ViewGroup没有处理该touch事件,那么就会执行自己的onTouchEvent.

2、  ViewGroup的dispatchTouchEvent收到了touch事件,类型为ACTION_DOWN。下面我们根据源码来分析具体逻辑。我们首先来看这段:

if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        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.
    intercepted = true;
}

这段代码会判断MotionEvent事件类型是不是ACTION_DOWN或者mFirstTouchTarget是否为null,单独判断一下ACTION_DOWN说明了该类型的touch事件的特殊性,了解全流程后就会明白,mFirstTouchTarget这个变量代表了该ViewGroup中目前处理touch事件的View,对于ACTION_DOWN类型的事件,mFirstTouchTarget自然为null。根据if语句的条件判断,onInterceptTouchEvent会被调用,我们来看下onInterceptTouchEvent源码:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

可知,一般情况下onInterceptTouchEvent方法会返回false。回到dispatchTouchEvent方法,可知道intercepted=false。之后的逻辑就是ViewGroup会建立一个孩子列表,这个孩子列表的顺序是从前台到后台的,根据Z轴的值(android屏幕绘图其实是以三维坐标系为坐标轴的,一些图像的立体显示就是通过Z轴值得变化来实现的)来判断孩子的图层的上下关系。ViewGroup拿到这个孩子列表后,会通过for循环,依次取出一个孩子,调用该孩子的dispatchTouchEvent方法,直到某一个孩子的dispatchTouchEvent返回true,循环停止,mFirstTouchTarget指向该孩子。如果完全按照源码的流程,View的dispatchTouchEvent也会返回false的。

3、  我们来看下View的dispatchTouchEvent的,ViewGroup是View的子类,它重写了该方法,所以ViewGroup和View的dispatchTouchEvent方法的处理逻辑是不一样的,只不过在某些情况下ViewGroup会super.disapatchTouchEvent。

public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    if (event.isTargetAccessibilityFocus()) {
        // We don't have focus or no virtual descendant has it, do not handle the event.
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // We have focus and got the event, then use normal event dispatch.
        event.setTargetAccessibilityFocus(false);
    }

    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }

    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)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

从代码可知,View的dispatchTouchEvent会调用自身注册的OnTouchListener的onTouch方法,如果返回true,则不会再调用onTouchEvent,否则会调用onTouchEvent。View的onTouchEvent方法会判断该View是否可以点击:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {

默认情况下clickable为false,只有在主动调用setClickable(true)或者setOnclickListner方法后,该变量会为true,那么View会执行onclick方法,到这里读者应该知道了我们平时为View设置的onClick方法是什么被调用的,这种情况下View的onTouchEvent会返回true。我们还是不考虑任何人设情况,一律走原生的代码,那么View的onTouchEvent还是返回false。

4、  我们再回到ViewGroup的dispatchTouchEvent方法。在上面3的结尾处说道ViewGroup会依次调用孩子的dispatchTouchEvent方法,默认情况下,当然是所有的孩子都会返回false,那么最后ViewGroup的dispatchTouchEvent自然的毫无意外地也只能返回false,返回值被Activity的dispatchTouchEvent收到。根据1的描述,Activity的dispatchTouchEvent会代用自己的onTouchEvent事件。到这里ACTION_DOWN类型事件算是处理完毕了,传递了一圈,由于没有孩子来消费,最后只能自己消费了。

5、  这一节作为一个补充,仔细想想还是补充一下Activity和ViewGroup之间经过了哪些环节,由1可知Activity的dispatchTouchEvent会调用getWindow().superDispatchTouchEvent(ev),getWindow()获取到的其实是PhoneWindow实例,PhoneWIndow的superDispatchTouchEvent(ev)方法调用是DecorView的dispatchTouchEvent方法,而DecorView是FrameLayout的子类,到这里大家都能明白了吧。

6、  接下来我们来看类型为ACTION_MOVE的touch事件,由Activity的dispatchTouchEvent传到DecorView的dispatchTouchEvent方法,其实调的就是ViewGroup的方法,该方法的逻辑前面已经讲过,DevorView的dispatchTouchEvent方法返回true,并且并没有把touch事件给孩子传递,因为它的mFirstTouchTarget为null。所以最终消费touch事件的还是Activity的onTouchEvent。

7、  ACTION_UP的touch事件的消费过程和7相同,不在赘述。

再次重申一遍,上面的流程是源码的原流程,不考虑经过任何的方法重写,当然也就是最基本的touch事件分发与消费流程了,用这个流程是因为方便讲述。但是实际项目过程中,我们自定义的View,甚至很多Android的封装好的View都已经重写了父类的方法,导致Touch事件的分发与消费流程多种多样,但是只要能弄清楚touch事件的消费的机制,我们就能很好地理解与开发。

这里在总结一下Touch事件消费机制的个人心得:

1、  特别留意ACTION_DOWN事件的处理,因为第一个处理它的View会被标记为target,该target与后续的touch事件处理有很大关系。

2、  Touch事件的传递在视图的树型结构中是按照深度优先遍历方法的,Activity作为根节点,直到某个子View的dispatchTouchEvent为true,停止遍历。如果遍历到直到做后一个叶子节点,都没有View来消费,那么会按照与之前遍历的相反的顺序,调用每个View的onTouchEvent。如下面日志所显示:


3、  ACTION_CANCEL事件我们很少会遇到,但是有时还是能碰到的,这个事件一般是在touch事件传递到target之前,其父View把事件拦截了,那么该target会收到一个ACTION_CANCEL事件,我们明白有这种情况会出现能很好地理解我们实际开发中的一些问题。比如子View对ACTION_DOWN事件消费了,但是其父View重写了onInterceptTouchEvent,对ACTION_MOVE进行了拦截,那么对第一个ACTION_MOVE事件,父View的dispatchTouchEvent会把ACTION_MOVE转化为一个ACTION_CANCEL事件传递给子View,记住父View其实没有处理第一个ACTION_MOVE事件,也就是父View的onTouchEvent方法不会处理第一个ACTION_MOVE事件,虽然对该类型进行了拦截,但是是从第二个ACTION_MOVE开始处理的。

4、  Touch事件传递根据不同情况,具体流程千差万别,所以不可能是读一片文章就能完全明白的,需要通过阅读源码加上自己的实践才能更深入地领会。


猜你喜欢

转载自blog.csdn.net/baidu_27196493/article/details/80294419