欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

View事件分发机制

程序员文章站 2022-05-14 15:26:09
...

点击事件从activity -> window -> DactorView传递;
MotionEvent:点击事件,包含点击状态:down/move/up/cancel;点击坐标,getX()/getY()/getRowX()/getRowY()。

1. 相关方法:

  • disptchTouchEvent(): 分发点击事件
  • onInterceptTouchEvent(): 拦截点击事件,ViewGroup才有。
  • onTouchEvent():响应点击事件。

伪代码:

public boolean disptchTouchEvent(MotionEvent e) {
    boolean consume;
    if(onInterceptTouchEvent(e)) {
        consume = onTouchEvent(e);
    } else {
        consume = child.disptchTouchEvent(e);
    }
    
    return consume;
}

2. 原则总结:

  • 事件序列是指一组1个down事件,n个move事件和1个up事件组成的。

  • 正常情况下,一组事件序列都只能被一个View拦截并消耗,除非通过特殊手段强制转移给其他view处理;

  • 当ViewGroup决定拦截一组事件序列后(这里指的是down事件),后面的点击事件(down、move和up事件)会有它自己处理,而且都不会调用onInterceptTouchEvent()方法,因为既然决定都要它处理了,以后都不需要再次判断是否需要拦截,符合生活规律。

  • 如果down事件被消费掉(onTouchEvent返回true), 以后的move事件和up事件都会发给这个view,如果不消费move和up事件(onTouchEvent返回false),也会不断接收到move事件或up事件,这些事件会消失,不会被发送给parent的onTouchEvent, 最终这些消失的事件会被activity处理。

  • 如果down事件没有被消费掉(onTouchEvent返回false),这个事件就会被parent的onTouchEvent处理;以后的move和up事件都不会被再发给这个view了。实际当中,上级让你完成一个任务,你没有完成,把任务交给上司,那上司在短时间内不会再把任何交给你了,对你不信任了。

  • ViewGroud的onInterceptToucheEvent默认是false,不拦截;

  • View没有onInterceptToucheEvent,分到事件后直接调用onTouchEvent。

  • onTouchEvent默认是true,只要有clickable或者longClickable有一个为true。

    默认longClickable为false,但是clickable根据不同的控件类型默认值不一样。比如Buttton的clickable默认为true,但是Textview的clickable默认是false。

  • setEnable(false)不会影响事件传递。

  • onClick能执行的前提是是可点击的(clickable和longClickable至少一个为true),并且收到down和up事件。

  • setOnTouchListener以后,这个监听器的onTouch方法的返回值将影响后续的onTouchEvent流程。如果onTouch返回true,后续OnTouchEvent不会执行,onClick也不会执行了。如果onTouch返回false,onTouchEvent接着执行。onTouchEvent返回true并且enable为true,onClick也会在up事件后被调用,。

  • 事件传递都是由外到内的,由父元素传到子元素的。通过requestDisallowInterceptTouchEvent能让子元素影响父元素的事件分发过程,但是ACTION_DOWN是无法影响的。

3. 源码分析

Activity::dispatchTouchEvent

//如果在所有的view里都没有消费点击事件,由activity自己兜底消费掉。
public boolean disptchTouchEvent(MotionEvent ev) { 
	if(ev.getAction == MotionEvent.ACTION_DOWN) {
    	onUserInteraction();   
	}
    
	if(getWindow().superDispatchTouchEvent(ev)) {
        return true;
	}
    
	return onTouchEvent(ev);
}

ViewGroup::disptchTouchEvent

//mFirstTouchTarget表示子view里有找到消费者。
//如果down事件的时候直接onInterceptTouchEvent返回true,那mFirstTouchTarget == null,那接下来的move和up事件直接走4分支,不会再经过1分支,证明了原则总结第3条。
//如果如果down事件的时候直接onInterceptTouchEvent返回false,然后在子view里找到了消费者,那mFirstTouchTarget != null,以后的move和up事件都可以走1分支。
//mGroupFlags默认为0,所以disallowIntercept为false,默认会进入分支2。
//requestDisallowInterceptTouchEvent(true):disallowIntercept变成true,会进入3分支,ViewGroup直接消费掉;
//requestDisallowInterceptTouchEvent(false):disallowIntercept变成false,会进入分支2;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    //1
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if(!disallowIntercept) {
       //2
        intercept = onInterceptTouchEvent(ev);
        ev.setAction(ev);
    }else{
        //3
        intercept = false;
    }
}else{
    //4
    intercept = true;
}

4. 滑动冲突

  • 外部拦截发(推荐)

    在父ViewGroup的onInterceptTouchEvent里做拦截

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch(ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(父类需要的拦截条件) {
                    //这时候move事件就会被自己消费掉。
                    intercept = true;
                }else{
                    //move事件又被下发到子view里消费掉。
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                //下发到子view,否则子view的onClick事件无法执行。
                intercept = false;
                break;
        }
        
        return intercept;
    }
    
  • 内部拦截法

    父ViewGroup::onInterfceptTouchEvent

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_DOWN) {
            //down事件不拦截,可以让后续其他时间下发到子view
            return false;
        }else{
            //move和up事件被拦截,自己处理掉
            return true;
        }
    }
    

    子view::dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent ev) {
       boolean intercept = false;
       switch(ev.getAction()) {
           case MotionEvent.ACTION_DOWN:
               parent.requestDisallowInterceptTouchEvent(true);
               break;
           case MotionEvent.ACTION_MOVE:
               if(父类需要的拦截条件) {
                   //这时候move事件到不了治理,会调用父类的onInterceptTouchEvent(),返回true,被父类消费。
                    parent.requestDisallowInterceptTouchEvent(false);
               }else{
                   //进入ViewGroup::disptchTouchEvent的分支3,直接发给子view消费
               }
               break;
           case MotionEvent.ACTION_UP:
               break;
       }
       
       return super.dispatchTouchEvent(ev);
    }
    

5. 应用层面分析

View事件分发机制

上图只是示意,并不完全准确,比如onTouchEvent在down事件返回false才会上发整个事件序列给parent的onTouchEvent方法,但如果是down事件返回true,move或者up事件才返回false,则move或up事件不会上发的,事件会消失,消失的事件最后才会发给activity。

重写touch事件返回值角度分析:

  • dispatchTouchEvent():

    • true: 事件序列结束;

    • false: Activity: 事件结束;

      ​ 非Activity:上发给parent的onTouchEvent()。

    • super: Activity: ViewGroup.dispatchTouchEvent();

      ​ ViewGroup: onInterceptTouchEvent();

      ​ View: onTouchEvent();

  • onInterceptTouchEvent():

    • true: onTouchEvent(),自己消耗事件。

         如果down事件被消耗,以后的move和up事件都会直接发给自己的ouTouchEvent()消耗;
      		
         如果down事件是子view消耗,只是move或up事件消耗,那会给子view发送一个cancel事件,然后剩下的move或者up事件被自己劫持,调用自己的onTouchEvent()方法;
      
    • false/super: 子View的dispatchTouchEvent();

  • onTouchEvent():

    • true: Activity: 事件结束;
      View/ViewGroup: 消耗事件(如果down事件被消耗,以后的move和up事件直接发给自己消耗;如果只是down被消耗,但是move或up事件不消耗(返回false),事件会消失,然后最终发给activity。

    • false: Activity: 事件结束;

      ​ View/ViewGroup: 不消耗事件(如果down事件不消耗,以后的事件直接上发给parent的onTouchEvent,再也不会下发给自己;如果是down被消耗,但是move或up事件不消耗(返回false),事件会消失,然后最终发给activity。)

    • super:如果clickable或者longClickable有一个为true,返回就是true,表示消耗事件,默认的longClickable都是false,但是Button的clickable默认为true,TextView的clickable默认为false。

view的enable不影响touch事件的分发,但是会影响onClick的执行。

onClick能执行的前提:

  • setOnTouchListener的onTouch方法返回true或者没有设置TouchListener。
  • view的enable为true;
  • onTouchEvent的返回值为true;表示消耗事件,DOWN,MOVE,UP事件都能发过来,尤其UP事件。

优先级: onTouch -> onTouchEvent -> onClick。