Android View事件分发机制及View的滑动冲突

Android事件分发机制


View事件分发机制是指Android对MotionEvent事件从产生到被消耗掉的整个处理过程。

MotionEvent即点击事件,当手指接触屏幕后所产生的一系列事件中,最典型的事件类型有以下三种:

  • ACTION_DOWN——手指刚接触屏幕;
  • ACTION_MOVE——手指在屏幕上移动;
  • ACTION_UP——手指离开屏幕;
    注意在一次完整的点击事件中ACTION_DOWN和ACTION_UP会且只会被触发一次。ACTION_MOVE可能不会被触发(当点击屏幕后松开)或者被多次触发。点击事件的分发由三个很重要的方法来完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面针对这三个方法略作介绍:

  • public boolean dispatchTouchEvent(MotionEvent ev)
    用于对事件进行分发。如果事件能被传递给当前View,那么该方法一定会被调用并且是首先被调用,返回值代表当前事件是否被消耗,

  • public boolean onInterceptTouchEvent(MotionEvent ev)
    用于判断是否拦截某个事件,在上述方法的内部被调用。如果View拦截了某个事件,那么在同一事件序列中,此方法不会被再次调用。
  • public boolean onTouchEvent(MotionEvent ev)
    用于对事件的具体处理,返回值表示是否消耗当前事件。如果onTouchEvent()方法未消耗当前事件,则在同一事件序列中,当前View无法再次接收到事件。

上述三个方法之间的执行顺序及区别可用如下伪代码表示:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consumed = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consumed;
}

点击事件发生时,MotionEvent最先传递给当前Activity,然后经过PhoneWindow(Window的唯一实现类)传递给DecorView,接着DecorView将MotionEvent传递给其父类ViewGroup, 在此期间MotionEvent仅仅是被传递,并无其他实质性操作。MotionEvent传递到ViewGroup之后会经过一系列的处理最终被消耗掉。我们需要关心的就是当MotionEvent来到ViewGroup之后究竟发生了什么?

ViewGroup对MotionEvent的分发过程

当MotionEvent来到ViewGroup,首先会被传递到ViewGroup中的dispatchTouchEvent中。其关键代码如下所示:

// Check for interception.
final boolean intercepted;
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;
}

通过以上代码我们可以直接得出或引申出以下结论:

  • ViewGroup会在如下两种情况下判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget != null, 只有个当事件由ViewGroup的子元素处理并且消耗掉时mFirstTouchTarget才会被赋值。
  • 这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法设置的,一般用于子View中。一旦设置为True后,ViewGroup将无法拦截除了ActionDown之外的其他事件,这是因为ViewGroup在分发事件时,如果是ActionDown就会重置FLAG_DISALLW_INTERCET标记位。因此当面对ACTION_DOWN事件时,Viewgroup总会调用自己的onInterceptTouchEvent方法来询问是否要拦截事件。默认情况下是不拦截的,并且如果ViewGroup一旦拦截ACTION_DOWN那么接下来的一系列事件都会被拦截。ViewGroup拦截事件包含两种情况:第一种是ViewGroup没有子元素,第二种是ViewGroup的所有子元素在dispatchTouchEvent中返回了false。ViewGroup拦截的事件会交个它的父类View进行处理。
  • 默认情况下,ViewGroup不会拦截任何事件,它也没有自己的onTouchEvent方法。

具体View对点击事件的处理过程

首先我们先来看View的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean result = false;
    ...
    if(onFilerTouchEventForSecurity(event)){
        //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
    }
    return result;
}

通过以上代码我们可以直接得出或引申出以下结论:

  • View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果onTouchListener中的onTouch方法返回true,那么onTouchEvent方法将不会被调用。可见OnTouchListener的优先级要高于onTouchEvent的优先级。引申一点,在onTouchEvent方法中会调用OnClickListener(如果设置了的话)的onClick方法。所以他们三个方法之间的优先级顺序为:onTouch > onTouchEvent > onClick。

然后我们再来看一下View的onTouchEvent方法对事件的具体处理:

if(((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) = LONG_CLICKABLE)){
    swithc (event.getAction()){
        case MotionEvent.ACTION_UP:
        ...
            //如果View设置了onClickListener那么performClick方法内部会调用它的onClik方法。
            performClick();
        ...
        break;
        case MotionEvent.ACTION_MOVE:
        ...
        break;
        case MotionEvent.ACTION_DOWN:
        ...
        break;
    }
    ...
    return true;
}

通过以上代码可以直接得出或引申出如下结论:

  • 只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态。View的LONG_CLICKABLE属性默认是false,而CLICKABLE属性是否为false和true和具体View有关,确切来说可点击的View其CLICKABLE为true,不可点击的View其CLICKABL为false,比如Button是可点击的,TextView是不可点击的。通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性。

View的滑动冲突


外部拦截法

外部拦截法是指点击事件都先经过父容器的拦截处理,如果父事件需要此事件就拦截,如果不需要此事件就不拦截。外部拦截法需要重写父容器的onInterceptTouchEvent方法。这种方法的伪代码如下所示:

public boolean onInterceptTouchEvent(MotionEvent event){
    boolean intercepted = false;
    switch(event.getAction()):
    case Motion.ACTION_DOWM:
        intercepted = false//不拦截
        break;
    case Motion.ACTION_MOVE:
        if(父容器需要当前点击事件){
            intertepted = true;
        }else{
            intercepted = false;
        }
        break;
    case Motion.ACTION_UP:
        intercepted = false;
        break;
    return intercepted;
}

上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要修改也不能修改。在onTerceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,否则父容器一旦拦截ACTION_DOWM,那么后续所有事件都会交给父容器处理,这个时候就没法在向下传递了。同时ACTION_UP这个事件,父容器也必须返回false,这是因为如果父容器拦截了ACTION__UP,那么其子元素的onClick事件就无法触发。(onClick事件是在View的onTouchEvent方法中被触发的)。

内部拦截法

内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event){
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if(父容器需要此类点击事件){
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(event);
}

同时父元素所做修改如下所示:

public boolean onIntercptTouchEvent(MotionEvent event){
    int action = event.getAction();
    if(action == MotionEvent.ACTION_DOWN){
        return false;
    }else{
        return true;
    }
}

——2016年6月14日15:03:55
——写于宿舍
——雨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值