Android中View的事件分发机制与滑动冲突的解决方案

Android事件分发机制:

1.MotionEvent概念

在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
ACTION_DOWN:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上移动
ACTION_UP:手指从屏幕上松开的一瞬间
ACTION_CANCEL:当前手势已终止

正常情况下,一次手指触摸屏幕的行为会触发上述事件①②③所组成的一系列事件
当子View消费事件的过程中,父View突然进行拦截,则子View会收到ACTION_CANCEL这个事件。

2.View的事件分发机制

所谓的事件分发,就是指对MotionEvent事件的分发过程。当一个MotionEvent产生了后,系统需要把这个事件传递给一个具体的View来处理,这个传递过程就是分发过程。其中有三个重要的方法dispatchTouchEventonInterceptTouchEventonTouchEvent

public boolean dispatchTouchEvent(MotionEvent event)
用来进行事件的分发。如果此事件能传递到当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法内部调用,用于判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示了是否拦截当前事件。

public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再次接收到事件。

如下一段伪代码可以表示三个函数之间的关系:

//事件分发给当前View
public boolean dispatchTouchEvent(MotionEvent e){
    boolean consume = false;
    //做拦截判断
    if(onInterceptTouchEvent(e)){
        //拦截该事件,然后处理该事件
        consume = onTouchEvent(e);
    }else{
        //未拦截该事件,事件被分发给子View
        consume = child.dispatchTouchEvent(e);
    }
    return consume;
}
事件分发大致流程:

当一个点击事件产生后,它会由Activity传递到PhoneWindow,PhoneWindow再传递到DecorView即顶级View。顶级View(一般是ViewGroup)接收到事件后,就开始进行事件分发。首先,顶级View的dispatchTouchEvent方法会被调用,如果这个View的onInterceptTouchEvent方法返回true,则表示它要拦截这个事件,那么此事件就会交由它处理,即调用它的onTouchEvent方法来处理事件;如果这个View的onInterceptTouchEvent方法返回false,则表示它不拦截这个事件,这时当前事件就会通过遍历的方式继续传递给它的所有子View,对应的子View的dispatchTouchEvent方法就会被调用。就这样一直传递到事件被处理为止。特殊情况:如果一个View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,以此类推,如果所有元素都不处理这个事件,最终事件会回传到Activity进行处理。

事件处理大致流程:

当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。所以事件处理的优先级onTouchListener的onTouch高于onTouchEvent高于onClick。

Tips:
①某个View一旦决定拦截,那么这一整个事件序列都只能由它来处理,并且它的onInterceptTouchEvent方法不再被调用。
②ViewGroup默认不拦截任何事件。
③View没有onInterceptTouchEvent方法,一旦有事件传给它,那么它就一定会执行。
④View的onTouchEvent默认会消费事件(返回true),除非它是不可点击的。

滑动冲突解决方案:

了解了上面的事件分发机制后,我们会发现所谓的滑动冲突,无非就是父View与子View之间的事件拦截逻辑未做相应处理所造成的。所以想要解决滑动冲突问题,我们可以从父View和子View的事件拦截的处理逻辑上下手。

滑动冲突的解决方案主要分为两大类:
1. 外部拦截法(拦截的处理逻辑交由父View执行)
2. 内部拦截法(拦截的处理逻辑交由子View执行)

外部拦截法

该方法主要重写父View的onInterceptTouchEvent方法,在内部做拦截的逻辑判断。伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent event){
    boolean intercepted = false;
    //从MotionEvent中获取xy坐标
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            //一定要返回false,否则子View无法接受到该事件序列的后续任何事件
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            //此处做拦截判断,可根据获得的xy坐标判断用户手指的滑动方向...
            if(判断父View是否需要当前事件){
                intercepted = true;
            }else{
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            //一定要返回false,否则子View中无法接收到该事件,可能导致子View的onClick方法无法触发
            intercepted = false;
            break;
        default:
            break;
    }
    //记录本次xy坐标
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

该方法只需重写父View的onInterceptTouchEvent方法,实现起来比较简单,思路也符合Android中的事件分发机制的执行流程。

内部拦截法

内部拦截法主要通过重写子View中的dispatchTouchEvent方法来实现对事件的拦截并进行逻辑判断。为代码如下:

public boolean dispatchTouchEvent(MotionEvent event){
    //从MotionEvent中获取xy坐标
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            //父View在后续事件序列中不准进行拦截
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            //此处做拦截判断,可根据获得的xy坐标判断用户手指的滑动方向...
            if(判断父View是否需要当前事件){
                //准许父View对后续事件进行拦截
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    //记录本次xy坐标
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

通过requestDisallowInterceptTouchEvent方法,我们可以动态的设置父View能否被准许拦截后续事件。
为了能在父View在被准许拦截后成功的对后续事件进行拦截,我们还需要对父View的onInterceptTouchEvent方法做重写。伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent event){
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            //一定要返回false,否则子View无法接受到该事件序列的后续任何事件
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_UP:
        default:
            intercepted = true;
            break;
    }
    return intercepted;
}

可以看到,通过将除了ACTION_DOWN以外的所有事件都设置返回true,可以保证该次事件一定被父View拦截处理。

以上就是两种解决滑动冲突的常见方案。

你可能感兴趣的:(Android中View的事件分发机制与滑动冲突的解决方案)