Android的事件分发机制

点击事件的传递规则

同个事件:

是指手指触摸屏幕的那一刻起,到手指离开屏幕的那一刻结束,在整个过程中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的move事件,最终以 up 事件结束。

事件传递规则:

当一个点击事件发生之后,传递过程遵循如下顺序:Activity -> Window -> View

对于跟ViewGroup来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 方法就会被调用,如果这时ViewGroup的 onInterceptTouchEvent 方法返回 true 就表示它要拦截当前事件,接着事件就会交给ViewGroup处理,即它的 onTouchEvent 方法就会被调用;如果这个ViewGroup的 onInterceptTouchEvent 方法返回 false, 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的ispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。如果一个view的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。

伪代码:

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

事件分发过程的三个重要方法:

1 . public boolean dispatchTouchEvent(MotionEvent ev)

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

2 . public boolean onInterceptTouchEvent(MotionEvent ev)

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

ViewGroup中onInterceptTouchEvent方法默认返回false,即默认不拦截任何事件

View中没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

3 . public boolean onTouchEvent(MotionEvent event)

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

View中的onTouchEvent默认返回true,消耗事件,除非它是不可点击的(clickable和longClickable都为false)。View的longClickable默认是false的,clickable则不一定,Button默认是true,而TextView默认是false。

ViewGroup不设置监听事件的话,onTouchEvent默认返回false,ViewGroup设置监听事件的话,onTouchEvent返回true,消耗事件。

View的enable属性不影响onTouchEvent的默认返回值。哪怕一个view是disable状态的,只要它的clickable或者longClickable有一个是true,那么它的onTouchEvent就会返回true。

某个view一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父容器去处理,即调用父容器的onTouchEvent方法;如果它消耗ACTION_DOWN事件,但是不消耗其他类型事件,那么这个点击事件会消失,此时父元素的onTouchEvent方法并不会被调用,当前view依然可以收到后续的事件(即当前view的onTouchEvent方法会被调用),但是这些事件最后都会传递给Activity处理。(因为正常情况下,一个事件序列只能被一个view拦截并消耗,因为一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了。)

其它:

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

2 . 事件传递过程总是先传递给父元素,然后再由父元素分发给子view,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外,即对于ACTION_DOWN时,ViewGroup会调用自己的onInterceptTouchEvent方法来询问是否要拦截事件。

VIew的滑动冲突

如何根据坐标得到滑动的方向:

根据滑动距离和水平方向形成的夹角;

根据水平和竖直方向滑动的距离差;

根据水平和竖直方向滑动的的速度差等

滑动冲突的解决方式:

1 . 外部拦截法:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。

父容器的onInterceptTouchEvent方法伪代码:

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;
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastXIntercept;
        int deltaY = y - mLastYIntercept;
        if (父容器需要当前点击事件) {
            intercepted = true;
        } else {
            intercepted = false;
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
    }
    default:
        break;
    }

    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

事件分发过程:

父容器的onInterceptTouchEvent方法,对于down事件和move父容器不需要当前点击得事件,返回false,才能传到子元素,因为如果返回true,一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了

如果父容器需要某类点击事件,返回true,拦截事件

对于up,返回false,因为在父容器不需要任何类点击事件时,如果在up事件返回true,会导致子元素无法接收到up事件;在父容器需要某类点击事件时,在up事件返回false,不会影响onTouchEvent方法接受到up事件(因为之前move事件,在父容器需要某类点击事件返回true,如果返回true,一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了)

2 . 内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。

子元素的dispatchTouchEvent方法伪代码:

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

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {]
        Parent().requestDisallowInterceptTouchEvent(true);
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        if (父容器需要此类点击事件) {
            Parent().requestDisallowInterceptTouchEvent(false);
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        break;
    }
    default:
        break;
    }

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

父容器的onInterceptTouchEvent方法伪代码

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

事件分发过程:

父容器的onInterceptTouchEvent方法,对于down,返回false,才能传到子元素,因为如果返回true,一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了

子元素的dispatchTouchEvent方法,对于dowm,调用Parent().requestDisallowInterceptTouchEvent(true),设置了FLAG_DISALLOW_INTERCEPT,父容器将无法拦截除了down以外的事件(即down以外的事件,父容器不会再调用onInterceptTouchEvent方法),所以down以后的其他事件传到子元素。当父容器需要某类点击事件时,调用Parent().requestDisallowInterceptTouchEvent(false),对FLAG_DISALLOW_INTERCEPT重置,父容器就可以继续调用它自己的onInterceptTouchEvent方法,所以在down以外的事件,父容器的onInterceptTouchEvent方法返回true,拦截事件。

其它:ViewGroup在事件分发时,如果是down事件就会重置FLAG_DISALLOW_INTERCEPT这个标志位,导致之前子元素设置的标志位无效,所以,如果我们想提前处理所以的点,要选择dispatchTouchEvent方法,前提事件能传递到当前的ViewGroup.

参考:Android开发艺术探索

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值