View的事件浅析

来源:互联网 发布:如何防止网络购物诈骗 编辑:程序博客网 时间:2024/06/05 04:11

View的事件分发机制是一个核心知识点也是解决滑动冲突的理论基础。本篇博文会深入分析View的事件分发机制。

一 View的事件分发机制

(1).点击事件的传递规则

在介绍点击事件传递之前,首先我们要明白这里分析的对象就是MotionEvent,就点击事件,所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来完成:dispatchTouchEvent,onInterceptTouchEventonTouchEvent;接下来我们一一介绍。

Public boolean dispatchTouchEvent(MotionEvent ev)

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

 

Public boolean onInterceptTouchEvnet(MotionEvent ev)

该方法在内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会再次被调用,返回结果表示是否拦截当前事件。

 

Public boolean onTouchEvent(MotionEvent ev)

dispatchTouchEvent中调用,用来处理事件。返回结果结果表示是否消耗事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

 

上述三个方法的关系可以用如下伪代码表示:

Public boolean dispatchTouchEvent(MotionEvent ev){

Boolean consume = false;

If(onInterceptTouchEvent(ev)){

Consume=onTouchEvent(ev);

}else{

Consume=child.dispatchTouchEvent(ev);

}

return consume;

}

 

我们也可以大致了解点击事件的传递规则:对于各ViewGroup来说,当点击事件产生后,首先会传递给它,这时候它的dispatchTouchEvent就会被调用,如果这个ViewGrouponInterceptTouchEvent返回ture就表示要拦截当前事件,接着事件就会交给这个ViewGroup处理,即他的onTouchEvent会被调用;如果这个ViewGrouponInterceptTouchEvent返回false,就表示不拦截当前事件,这时当前事件就会继续传给它的子元素,接着子元素的dispatchTouchEvent方法就不会被调用,如此反复,直到事件被处理。

 

当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的OnTouch方法会被调用;如果返回ture,那么onTouchEvent将不会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvnet要高,在OnTouchEvent中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可以看出,我们常用的OnClickListener,其优先级最低。

 

另外事件的传递过程是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素事件的分发过程,但是ACTION_DOWN事件除外。

 

二 View的滑动冲突

我们碰到的滑动冲突问题通常都由于内外两层都是可以滑动引起的,解决这类问题,是由固定的套路的。接下来,我们分析常见的滑动冲突情形与解决方案。

 

(1).常见的滑动冲突场景

i. 外部滑动方向与内部滑动方向不一致

ii. 外部滑动方向与内部滑动方向一致

iii. 上面两种情况嵌套

(2).滑动冲突的处理规则

不管滑动冲突多么复杂,它都有既有的规则,根据这些我们就可以选择合理的方法去处理。

对于第一种问题,我们的处理规则是:当用左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部的View点击事件。这个时候,我们就可以他们的特征来解决滑动冲突,具体来说是:根据滑动时水平滑动还是竖直滑动来判断到底是谁来拦截事件。根据滑动过程中两个点之间的坐标就可以得出到底是水平滑动还是竖直滑动。比如我们可以根据路径和水平方向所形成的夹角,也可以根据水平方向和竖直方向上的距离来判断。

 

对于场景2.比较特殊,我们无法根据滑动的角度,距离差以及速度来做判断,但是这个时候,我们一般能从业务上找到突破点。比如业务上有规定,当处于某种状态时需要外部View响应用户的滑动,而处于另一种状态时则需要内部View来响应View的滑动。

对于场景三,它的滑动规则就更复杂了。具体的解决方案,我们也需要从业务上着手。

 

(3).滑动冲突的解决方式

我们先对场景一的问题进行分析,最终得出解决滑动冲突的通用方案。解决这类问题,有两类解决方案:外部拦截法和内部拦截法。

 

1.外部拦截法

所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件都不拦截。这样就可以解决滑动冲突的问题,这种方法比较符合事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。代码如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        intercepted = false;
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
            intercepted = true;
        }
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastXIntercept;
        int deltaY = y - mLastYIntercept;
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            intercepted = true;
        else {
            intercepted = false;
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
    }
    default:
        break;
    }

    Log.d(TAG"intercepted=" + intercepted);
    mLastX = x;
    mLastY = y;
    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}

 

上述代码是外部拦截法的典型逻辑。针对不同的冲突,只需要修改父容器需要当前点击事件的条件即可,其他均不要修改也不能修改。

 

考虑一种情况,假如事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法收到ACTION_UP事件,这个时候,子元素的onClick事件就无法触发,但是父容器特殊,一旦开始拦截任何一事件,那么后续的事件都会教给它处理,而Action_up作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在Action_UP时返回true.

2.内部拦截法

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

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        Log.d(TAG"dx:" + deltaX + " dy:" + deltaY);
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        break;
    }
    default:
        break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

 

上述代码是内部拦截法的典型代码,当面对不同的滑动策略时,只要修改里面的条件即可,奇特不需要也不能有改动。除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(this)时,父元素才能继续拦截所需的时间。

0 0
原创粉丝点击