Android触摸事件分发机制

一 概述

触摸事件的分发机制是安卓开发中的基础知识,但这块知识又有点绕,总是让人觉得似懂非懂。其实安卓事件传递就是把用户触摸屏幕时的touch事件封装成MotionEvent对象在Activity、ViewGroup和View中传递并处理该touch事件的过程。

二 触摸事件分发的方法

现在我们知道触摸事件是在Activity、ViewGroup和View中进行传递的,对应的方法如下:

  1. Activity
    Activity不对触摸事件进行拦截,收到触摸事件后直接分发给ViewGroup,如果所有的view最后都没有处理该触摸事件,会调用Activity的onTouchEvent方法进行处理,因此Activity处理触摸事件的方法为:
    dispatchTouchEvent
    onTouchEvent
  2. ViewGroup
    当ViewGroup收到触摸事件后,它可以分发给自己的子View但在分发之前可以判断是否需要拦截该触摸事件,也可以调用自己的onTouchEvent方法处理触摸事件,因此ViewGroup处理触摸事件的方法有三个:
    dispatchTouchEvent
    onInterceptTouchEvent
    onTouchEvent
  3. View
    View和Activity一样可以接收和处理触摸事件但不能拦截触摸事件,毕竟View下面也没有子View存在了,拦截没有意义。故View中的方法为:
    dispatchTouchEvent
    onTouchEvent

三 触摸事件的传递机制

  1. 在Activity中
    当用户点击屏幕时Activity最先收到触摸事件此时会调用Activity的dispatchTouchEvent方法,源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

可以看到Activity调用getWindow().superDispatchTouchEvent(ev)方法继续把触摸事件传递给包含的View,如果有View处理了该事件则返回true,事件传递结束;如果没有View处理该事件则调用Activity的onTouchEvent(ev)方法处理该事件,无论在Activity的onTouchEvent(ev)方法中是否消费该事件,该事件的传递都结束了。

  1. 在ViewGroup中
    当在Activity中调用getWindow().superDispatchTouchEvent(ev)方法时,Touch事件会被传递给Activity包含的最外层ViewGroup,然后层层向下传递。我们分析ViewGroup是如何处理Touch事件的。
    当Touch事件传递到ViewGroup会先调用ViewGroup的dispatchTouchEvent方法:
 /**
  * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
    public boolean dispatchTouchEvent(MotionEvent ev) { 

    ... // 仅贴出关键代码

    // ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
    if (disallowIntercept || !onInterceptTouchEvent(ev)) {  

    // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
    // 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
            // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
            // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断

        ev.setAction(MotionEvent.ACTION_DOWN);  
        final int scrolledXInt = (int) scrolledXFloat;  
        final int scrolledYInt = (int) scrolledYFloat;  
        final View[] children = mChildren;  
        final int count = mChildrenCount;  

    // 通过for循环,遍历了当前ViewGroup下的所有子View
    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);  

            // 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
            // 若是,则进入条件判断内部
            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;  

                // 条件判断的内部调用了该View的dispatchTouchEvent()
                // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
                if (child.dispatchTouchEvent(ev))  { 

                mMotionTarget = child;  
                return true; 
                // 调用子View的dispatchTouchEvent后是有返回值的
                // 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                // 即把ViewGroup的点击事件拦截掉

                        }  
                    }  
                }  
            }  
        }  
    }  
}

/**
* 作用:是否拦截事件
* 说明:
*     a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
*     b. 返回false = 不拦截(默认)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {  
    return false;
} 

从代码中可以看出在ViewGroup的dispatchTouchEvent方法中先判断该ViewGroup是否拦截该Touch事件,如果拦截了,Touch事件就不会往下传递而是直接调用ViewGroup的onTouchEvent方法处理事件;如果没有拦截则遍历所有子View找到正在点击的那个View并把Touch事件传递给它。
3.在View中
View收到Touch事件后会先调用View的dispatchTouchEvent方法,在dispatchTouchEvent方法中调用该View的onTouchEvent方法去处理该Touch事件,如果该View的onTouchEvent方法返回true则表示该View消费了该事件,否则会继续调用其父View的onTouchEvent方法去处理该事件,直到某个View的onTouchEvent方法消费了该事件,或者传递到Activity的onTouchEvent方法,则事件传递结束。
在View的onTouchEvent方法中如果接收并消费了ACTION_DOWN事件,则该View会接收到后续的ACTION_MOVE、ACTION_UP等事件;反之,如果该View没有消费ACTION_DOWN事件则后续的事件不会再传递给该View。

四 注意事项

  1. ViewGroup的onInterceptTouchEvent方法默认返回false,ViewGroup进行事件分发都会调用该方法,但是一旦onInterceptTouchEvent方法返回true则表示该ViewGroup拦截了触摸事件,后续进行事件分发不再调用onInterceptTouchEvent方法。
    举个栗子:我们在onInterceptTouchEvent方法中判断是ACTION_MOVE事件就返回true,在该ViewGroup的子View可以收到ACTION_DOWN事件,如该子View消费了ACTION_DOWN事件,则在第一个ACTION_MOVE事件到来时,ViewGroup会拦截该事件,但是并不会调用ViewGroup的onTouchEvent方法,同时把ACTION_CANCEL事件传递给子View。后续的事件都不会传递给子View了,而是直接调用ViewGroup的onTouchEvent方法去处理。
  2. View的onTouch方法会先于onTouchEvent方法执行,如下所示(onClick在onTouchEvent方法中执行):
button1.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e("onTouch","touch:button1");
        //返回true不执行onClick方法
        //返回false接着执行onClick方法
        return false;
    }
});


button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.e("button1","点击了:button1");
    }
});
  1. View的onClick方法是在ACTION_UP事件之后执行的,并不是在ACTION_DOWN事件到来时就执行。因此如果在父View中拦截了ACTION_MOVE或者ACTION_UP事件,是不会执行该方法的。
  2. 可以调用getParent().requestDisallowInterceptTouchEvent(true)方法请求父View不要拦截Touch事件。注意这个方法不能在子View初始化时调用(无效),最好在子View接收到Touch事件也就是在子View的dispatchTouchEvent方法中调用。调用完该方法后,父View以及父View的父View就不会再调用onInterceptTouchEvent方法去判断是否拦截了。

好了,触摸事件的传递机制就讲到这里啦,有不对的地方欢迎留言指正。

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