Android事件分发及应用

事件分发

事件分发是Android View体系里非常重要的知识点,熟悉事件分发之后可以自定义出各种交互复杂的View,也可以解决开源库中的各种滑动冲突bug。下边开始具体的实验和分析。

创建了2个自定义的ViewGroup:GrandFatherLayout继承自LinearLayout,FatherLayout继承自RelativeLayout。创建了一个自定义View(MyButton)继承自Button。3个View中,只在dispatchTouchEventonInterceptTouchEventonTouchEvent中增加了打印日志的代码,其他未做修改。将MyButton放在FatherLayout中,将FatherLayout放在GrandFatherLayout中,视图层次结构如下:

打印日志相关代码类似如下代码:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            Log.e("zx", "GrandFatherLayout-onTouchEvent-DOWN");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e("zx", "GrandFatherLayout-onTouchEvent-MOVE");
            break;
        case MotionEvent.ACTION_UP:
            Log.e("zx", "GrandFatherLayout-onTouchEvent-UP");
            break;
        default:
            break;
    }
    return super.onTouchEvent(event);
}

未修改任何方法返回值时,点击按钮,打印日志如下:

2.jpg

从以上结果可以总结出:

  • 事件是以ACTION_DOWN开始,中间有若干个ACTION_MOVE,最后以ACTION_UP结束
  • 事件是从视图树上由上往下传递,调用顺序为dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent

onInterceptTouchEvent

ViewGroup的特有方法,表示是否拦截事件,默认不拦截。

如果在FatherLayout的onInterceptTouchEvent()中返回true,事件流如下所示:

3.jpg

如果在FatherLayout的onInterceptTouchEvent()中返回true,同时onTouchEvent()也返回true,事件流如下所示:

4.jpg

从结果中可以总结出:

  • onInterceptTouchEvent()返回true时,代表ViewGroup拦截此事件,此ViewGroup的onTouchEvent()会被调用,子View将无法收到事件流

  • onInterceptTouchEvent()返回true时,后续事件都不会再调用它的onInterceptTouchEvent(),而是直接调用它的onTouchEvent()。通俗点讲就是:如果事件被当前ViewGroup拦截,后续的一系列事件,一直到ACTION_UP(本次事件流结束),都是由它来处理,不会再询问是否拦截

至于为什么onTouchEvent()返回true时,事件流改变了,将会在onTouchEvent中解释。

增加条件,如果在FatherLayout的onInterceptTouchEvent()的Down事件中返回false,Move事件中返回true,也就是不拦截按下事件,但是拦截移动事件。同时onTouchEvent()也返回true,事件流如下所示:

6.jpg

注意绿色箭头处的日志,FatherLayout的子View收到了ACTION_CANCEL事件。总结出:父级View拦截事件流时,如果同一事件流中,拦截之前已经有事件到达了子View中,那么子View会收到ACTION_CANCEL事件,用于子View结束事件流(例如:按钮将按下的状态恢复)

onTouchEvent

实际的事件处理逻辑,返回值代表是否消耗当前事件。

上边onInterceptTouchEvent的测试中,onTouchEvent()返回false,打印出如下结果:

4.jpg

onTouchEvent()返回true,打印出如下结果:

4.jpg

从结果中可以总结出:

  • onTouchEvent()返回true时,事件会被消耗掉,不会向上交给父级的onTouchEvent()

  • onTouchEvent()返回false时,事件向上交给父级的onTouchEvent(),父级再交给父级的父级,层层向上,直到被消耗掉(也就是onTouchEvent()返回true)

修改一下,让onTouchEvent() ACTION_DOWN事件时返回true,ACTION_MOVE和ACTION_UP事件返回false。打印结果如下:

7.jpg

结果跟onTouchEvent()中全部返回true相比,区别只是绿色箭头指向的部分。事件没有向上交给父级,也就是说事件被消耗了。同时ACTION_MOVE和ACTION_UP事件没有被消耗时,事件不会交给父级而是直接抛给了Activity。总结:

  • 只需要在onTouchEvent()的ACTION_DOWN事件中返回true即可消耗事件

  • View不消耗ACTION_MOVE和ACTION_UP事件时,事件将不经过父级,而是直接抛给Activity

如果反过来,让onTouchEvent() ACTION_DOWN事件时返回false,ACTION_MOVE和ACTION_UP事件返回true。打印结果如下:

8.jpg

这个结果与前边onTouchEvent()事件全部返回false的结果一模一样(onTouchEvent章节一开始的例子中没有打印Activity的日志,如果打印了,结果是一致的),都是将ACTION_DOWN事件逐级向上交给父级,如果向上传递的过程中没有被消耗,就会一直传递给Activity。并且,如果没有消耗ACTION_DOWN事件,那么设置的是否消耗后续事件也就不生效了,后续事件与它以及它的父级就无缘了,后续事件会被直接抛给Activity。

除了onTouchEvent()之外,View还可以通过setOnTouchListener中的onTouch()响应事件,二者的区别是:

  • onTouchEvent()既可以是全局(整个Activity)的监听,又可以是单个View的监听,而setOnTouchListener的onTouch()则是针对某个具体的View的监听,并且需要提前setOnTouchListener才能被回调。

  • onTouch()的优先级比onTouchEvent()高,当onTouch()返回值是true(事件被消耗)时,onTouchEvent()将不会被执行;当onTouch()返回值是false(事件未消耗)时,onTouchEvent()方法才会被执行。

    需要额外说明的是,如果onTouchEvent()方法不会被执行时,依赖于onTouchEvent()的监听都不会被执行,这些监听有如下几种:OnClickListener的onClick()(在onTouchEvent的ACTION_UP事件中执行的)、OnLongClickListener的onLongClick(),GestureDetector.OnGestureListener的一系列事件、GestureDetector.OnDoubleTapListener的一系列事件。

dispatchTouchEvent

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

View的dispatchTouchEvent()逻辑如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    //如果setOnTouchListener了,并且onTouch返回true,就返回true
    if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
        return true;
    }

    // 如果没有setOnTouchListener了,或者onTouch返回false,就调用onTouchEvent,
    // 并将onTouchEvent的返回值作为dispatchTouchEvent的返回值返回。
    return onTouchEvent(event);
}

这是伪代码,源代码比这个多。这个伪代码中可以看出:如果View setOnTouchListener了,就先调用OnTouchListener的onTouch(),如果onTouch()返回true,dispatchTouchEvent就返回true,代表事件被消耗。如果onTouch()返回false,代表onTouch()没有消耗事件,就继续执行onTouchEvent(),并将onTouchEvent的返回值作为dispatchTouchEvent的返回值返回。这段伪代码,也证明了之前onTouchEvent()与onTouch()的区别。

ViewGroup的dispatchTouchEvent()伪代码如下:

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

如果ViewGroup的onInterceptTouchEvent()方法返回true就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup处理,即它的 onTouchEvent方法就会被调用(此时时间ViewGroup等同于View,如果有OnTouchListener也会优先被执行);如果这个ViewGroup的onInterceptTouchEvent()方法返回false,就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent()方法就会被调用,如此反复直到事件被最终消耗。

requestDisallowInterceptTouchEvent

这个方法一般在子View中调用,通过调用getParent().requestDisallowInterceptTouchEvent(true) ,阻止父级ViewGroup对其MOVE或者UP事件进行拦截。requestDisallowInterceptTouchEvent()只对当前事件流有效,无法一直起作用,当下一个事件流的Down事件到来时,父级ViewGroup会将拦截状态FLAG_DISALLOW_INTERCEPT重置,所以requestDisallowInterceptTouchEvent()无法影响父级ViewGroup对Down事件的处理,由于拦截状态重置,上次时间流调用的requestDisallowInterceptTouchEvent()就失效了,如果想再次请求父组件不拦截,需要再调用一次requestDisallowInterceptTouchEvent()requestDisallowInterceptTouchEvent()通常用于解决滑动冲突。

滑动冲突

这一部分直接引用任玉刚大佬的《Android开发艺术探索》,过于经典,所以就原封不动借鉴(程序员的事情,怎么能叫抄呢)过来了。

外部拦截法

所谓外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的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:
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (父容器需要当前点击事件) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需做修改并且也不能修改。这里对上述代码再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义。 考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。

内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和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);
}

上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisal-lowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。 为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就无法起作用了。父元素所做的修改如下所示。

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

自定义View时触摸事件处理

自定义View或者ViewGroup时,可以按照以下步骤处理触摸事件:

  1. 重写 onTouchEvent(),在里面写上触摸反馈逻辑,比如响应拖动等等,并返回 true(关键是 ACTION_DOWN 事件时返回 true)。

  2. 如果是会发生触摸冲突的 ViewGroup,还需要重写 onInterceptTouchEvent(),在ACTION_DOWN时返回 false,并在确认接管事件流时返回一次 true,以实现对事件的拦截。

  3. 当子 View 临时需要阻止父 View 拦截事件流时,可以调用父 View 的 requestDisallowInterceptTouchEvent(true) ,通知父 View 在当前事件流中不再通过 onInterceptTouchEvent() 来拦截。

手势事件监听

用于监听用户的单击、滑动、长按、双击等行为,主要用到系统提供的GestureDetector和3种Listener。

使用步骤如下:

  1. 按照自己需求,实现3种Listener中的一种。

  2. 使用上一步实现的Listener创建GestureDetector

  3. 接管目标View的onTouchEvent()或者OnTouchListener的onTouch()方法,并将GestureDetector的onTouchEvent()的返回值返回,如下所示:

    //接管onTouchEvent
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
    }
    
    //接管OnTouchListener的onTouch
    view.setOnTouchListener(new View.OnTouchListener() {
        @Override 
        public boolean onTouch(View v, MotionEvent event) {
            return gestureDetector.onTouchEvent(event);
        }
    });
    

下边是3种Listener的介绍。

监听器功能
OnGestureListener手势检测,有以下类型事件:按下(onDown)、 触摸反馈(onShowPress) 、单击抬起(onSingleTapUp)、滚动(onScroll)、长按(onLongPress)、快速滑动(onFling)
OnDoubleTapListener检测双击,有三个类型事件:双击(onDoubleTap)、单击确认(onSingleTapConfirmed) 和 双击事件回调(onDoubleTapEvent)
SimpleOnGestureListener上述接口的空实现,使用这个时,不必实现接口的所有方法,比较方便

需要特别说明的2点:

  1. 设置了手势监听后,OnClickListener的onClick()将不会被执行。使用接管onTouchEvent这种方式时,由于onTouchEvent直接被接管了,View类中自带的onTouchEvent里的代码都不会被执行,所以onClick()无法被执行;使用接管OnTouchListener的onTouch这种方式时,由于onTouch的优先级比onTouchEvent高,所以onTouchEvent不会被执行,而onClick()依赖于onTouchEvent,所以也不会被执行。
  2. 连续两次点击,DoubleTap和SingleTapConfirmed只有一个会被执行,也就是说,要么是单击,要么双击。原因是SingleTapConfirmed会在第一次点击后等待,等时间超过了双击事件最长间隔后才回调(如果没有第二次点击),此时就绝对不会是双击事件,这也是Confirmed的意思。而OnGestureListener的onSingleTapUp就不会严格区分,双击事件第一次抬起时,onSingleTapUp就会被执行。

简单理解

事件分发的整个流程,非常像公司里任务分配的流程。

  1. 任务总是由老板往下分派(Activity的dispatchTouchEvent),并且老板不会主动接某个具体的任务(Activity没有onInterceptTouchEvent)。
  2. 任务到达中层领导时,中层领导会进行分派(ViewGroup的dispatchTouchEvent),任务分派过程中会首先考虑自己是否要接这个任务(ViewGroup的onInterceptTouchEvent)。一般情况下,中层领导也不接这个任务(ViewGroup的onInterceptTouchEvent默认返回false),而是向下分派给下级(child.dispatchTouchEvent)。有时候,中层领导由于某些原因需要自己亲自接手这个任务(ViewGroup的onInterceptTouchEvent返回true),他就会亲自解决任务(执行onTouchEvent),此时任务就不会分配给他的下级(子View无法收到事件流)。如果任务解决了(onTouchEvent返回true),任务就不会再交给他的领导(父级onTouchEvent不会被执行),如果任务没解决(onTouchEvent返回false),任务就会交给他的领导,由他的领导继续处理(父级onTouchEvent会被执行)。如果这个中层领导是个光杆司令(没有子View的ViewGroup),那么任务就得由他自己来处理(执行onTouchEvent)。如果任务经过层层的领导,这些领导都没有接手(ViewGroup都没有拦截),最终任务会会被分配给底层的程序员(View)。
  3. 程序员如果解决了这个任务(View的onTouchEvent返回true),那么任务就终结了(事件流结束)。如果程序员没能解决这个问题,那么就只能把任务继续向上交给上级领导(父级ViewGroup的onTouchEvent会被处理),领导如果解决了,任务结束,领导如果没解决,就会继续向上交给领导的领导。如果任务一级一级交上去,始终没有被解决,那么任务最终会被丢给老板(Activity执行onTouchEvent)。
  4. 有时候程序员会主动告诉领导某某类型的任务你就交给我吧(parent.requestDisallowInterceptTouchEvent(true)),此时领导就会直接将任务交给程序员(View的onTouchEvent被执行),当程序员处理了一部分后觉得剩余的任务由领导处理更合适时(达到父容器的拦截条件),就会告诉父级接下来的任务由你来继续吧(parent.requestDisallowInterceptTouchEvent(false)),然后父级接手,继续处理任务,直到任务被解决。

这个例子可以方便理解事件分发流程,但是这个例子会丢失一些细节,所以这个例子只适合初步理解,初步理解之后还是应该看源码和写验证代码来学习其中的细节。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值