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

View 的事件分发机制

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

  • public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。如果事件能够传递给当前 View ,那么此方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和 下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
  • public boolean onInterceptTouchEvent(MotionEvent ev)
    在上述方法内部调用,用来判断是否拦截某个事件,如果当前 ViewGroup(只存在 ViewGroup中) 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
      如果 onInterceptTouchEvent 方法对 down 事件返回 false ,后续的事件(多个move 和 up)依然会传递给它们的 onInterceptTouchEvent 方法,这一点和 onTouchEvent 的行为是不一样的。
      如果onInterceptTouchEvent方法对 down 事件返回 true(这表示当前控件拦截此事件序列),后续的事件(多个move和up)则不会在执行 onInterceptTouchEvent 方法,然后会执行 onTouchEvent 方法。如果onTouchEvent 方法返回 true 表示消费此事件,返回 false 表示不消费此事件。
  • public boolean onTouchEvent(MotionEvent ev)
    在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
      假如 down 事件传给 view 的 onTouchEvent 方法时,它返回了 false ,down 事件会继续向上传递给父 View 的 onTouchEvent ,即使它们在 onInterceptTouchEvent 方法中说它们不想拦截这个 down 事件,但是么有办法,没有子 view 愿意处理该事件。

递归的含义:以 down 事件为例,down 事件的处理实际上经历了一下一上的两个过程,下是指:从最外层到最内层的 onInterceptTouchEvent ,上是指:从最内层到最外层的 onTouchEvent,当然,任何一步的事件传递的方法返回 true ,都能阻止它继续传播。

类型相关方法ActivityViewGroupView
事件分发dispatchTouchEvent
事件拦截onInterceptTouchEvent××
事件消费onTouchEvent

从上表可以看出 Activity 和 View 都是没有拦截事件(onInterceptTouchEvent)的,这是因为:

  • Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕无法响应事件,这不是我们需要的结果
  • View 作为事件传递的最末端,要么消费事件,要么不处理进行回传,根本没有必要进行事件拦截。

上述三个方法大概可以用以下伪代码表示:

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

上述伪代码已将三者的关系表现的淋漓尽致。通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 返回 true 就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法就会被调用;如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。
  当一个 View 需要处理事件时,如果它设置了 OnTouchListener ,那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false ,则当前 View 的 onTouchEvent 方法会被调用;如果返回 true ,那么 onTouchEvent 方法将不会被调用。由此可见,给 View 设置的 OnTouchListener ,其优先级比 onTouchEvent 要高。在 onTouchEvent 方法中,如果当前设置的有 OnClickListener ,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 OnClickListener ,其优先级最低,即处于事件传递的尾端。
  总体来说,当一个点击事件产生后,它的传递过程遵循如下顺序: Activity -> Window -> View ,即事件总是先传递给 Activity ,Activity 再传递给 Window,最后 Window 在传递给顶级 View 。顶级 View 接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个 View 的 onTouchEvent 返回 false ,那么它的父容器的 onTouchEvent 将会被调用,以此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理,即 Activity 的 onTouchEvent 方法会被调用。

关于事件传递的机制,常用的结论:

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
  2. 正常情况下,一个事件序列只能被一个 View 拦截且消耗。因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别有两个 View 同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理。
  3. 某个 View 一旦决定拦截,那么一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。
  4. 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了false),那么同一事件序列中的其他事件不会再交给它来处理,并且事件将重新交给它的父元素去处理,即父元素的 onTouchEvent 会被调用。
  5. 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,这时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
  6. ViewGroup 默认不拦截任何事件。Android 源码中 ViewGroup 的 onInterceptTouchEvent 方法默认返回 false 。
  7. View (不含ViewGroup)没有 onInterceptTouchEvent 方法,一旦点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  8. View 的 onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认都是为 false ,clickable 属性要分情况,比如 Button 的 clickable 属性默认为 true ,而 TextView 的 clickable 默认为 false 。
  9. View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
  10. onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。
  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再有父元素分发给子View ,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

结合以下文章效果更佳:
可能是讲解Android事件分发最好的文章
Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
Android事件分发机制 详解攻略,您值得拥有

下面来说一个比较重要的方法requestDisallowInterceptTouchEvent:这个方法可以设置 FLAG_DISALLOW_INTERCEPT标记位,FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了down以外的其他点击事件。

ViewGroup.中 dispatchTouchEvent 重点源码:

/**
  * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
    public boolean dispatchTouchEvent(MotionEvent ev) { 

     		// ..................省略
     		
     		// ViewGroup会在down事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,
     		//因此子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对down事件的处理
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

 			// ViewGroup 分两种情况下会判断要拦截当前事件:1、事件类型为 down 2、和 mFirstTouchTarget != null
 			//1、只要是down事件就拦截
 			//2、mFirstTarget 的意思是:当事件由ViewGroup的子元素成功处理时,mFirstTarget 会被赋值并指向子元素。
 				//换句话说就是:ViewGroup不拦截事件并交由子元素处理时mFirstTouchTarget != null成立
 				//一旦事件由ViewGroup拦截时,mFirstTouchTarget != null就不成立,进一步来讲,当 move和up事件到来时,由于mFirstTouchTarget != null不成立,
 				//将导致ViewG的 onInterceptTouchEvent不会再被调用,并且同一序列的其他事件都会默认交给这个ViewGroup处理。
            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的所有子元素
			final View[] children = mChildren;
			for (int i = childrenCount - 1; i >= 0; i--) {
				//.....省略验证
				//判断子元素是否能够接收到点击事件
				if (!canViewReceivePointerEvents(child)
						|| !isTransformedTouchPointInView(x, y, child, null)) {
					ev.setTargetAccessibilityFocus(false);
					continue;
				}
				//判断点击事件是否坐落在子元素的区域内
				newTouchTarget = getTouchTarget(child);
				if (newTouchTarget != null) {
					// Child is already receiving touch within its bounds.
					// Give it the new pointer in addition to the ones it is handling.
					newTouchTarget.pointerIdBits |= idBitsToAssign;
					break;
				}

				resetCancelNextUpFlag(child);
				//实际上调用的就是子元素的dispatchTouchEvent方法
				if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
					// Child wants to receive touch within its bounds.
					mLastTouchDownTime = ev.getDownTime();
					if (preorderedList != null) {
						// childIndex points into presorted list, find original index
						for (int j = 0; j < childrenCount; j++) {
							if (children[childIndex] == mChildren[j]) {
								mLastTouchDownIndex = j;
								break;
							}
						}
					} else {
						mLastTouchDownIndex = childIndex;
					}
					mLastTouchDownX = ev.getX();
					mLastTouchDownY = ev.getY();
					newTouchTarget = addTouchTarget(child, idBitsToAssign);
					alreadyDispatchedToNewTouchTarget = true;
					break;
				}

				// The accessibility focus didn't handle the event, so clear
				// the flag and do a normal dispatch to all children.
				ev.setTargetAccessibilityFocus(false);
			}
			//.........省略
}

ViewGroup 中 onInterceptTouchEvent 的源码:

/**
  * 分析:ViewGroup.onInterceptTouchEvent()
  * 作用:是否拦截事件
  * 说明:
  *     a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
  *     b. 返回false = 不拦截(默认)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
    return false;
  } 

View 中 dispatchTouchEvent 源码:

/**
  * 源码分析:View.dispatchTouchEvent()
  */
  public boolean dispatchTouchEvent(MotionEvent event) {  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 
        return onTouchEvent(event);  
  }
  // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
  //     1. mOnTouchListener != null
  //     2. (mViewFlags & ENABLED_MASK) == ENABLED
  //     3. mOnTouchListener.onTouch(this, event)
  // 下面对这3个条件逐个分析


/**
  * 条件1:mOnTouchListener != null
  * 说明:mOnTouchListener变量在View.setOnTouchListener()方法里赋值
  */
  public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  
    // 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)

} 

/**
  * 条件2:(mViewFlags & ENABLED_MASK) == ENABLED
  * 说明:
  *     a. 该条件是判断当前点击的控件是否enable
  *     b. 由于很多View默认enable,故该条件恒定为true
  */

/**
  * 条件3:mOnTouchListener.onTouch(this, event)
  * 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)
  */
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  

            return false;  
        }  
    });
    // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
    // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)

滑动冲突

外部拦截法

所谓外部拦截法时指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写 父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可。
在 onInterceptTouchEvent 方法中:
down 事件必须返回 false;
move 事件根据需要来决定,如果父容器拦截返回就放回 true,否则返回 false;
up 事件 返回 false 。
伪代码:

  	@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
         
            if (父容器需要拦截当前事件) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
        }
        return intercepted;
    }

内部拦截法

内部拦截法是值父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和 Android 中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来较外部拦截法稍显复杂,需要:
①重写子元素的 dispatchTouchEvent 方法
down 事件的parent.requestDisallowInterceptTouchEvent(true);
move 事件根据需要返回parent.requestDisallowInterceptTouchEvent(false),这时父容器可以拦截事件,否则不拦截。
伪代码:

  	@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        Log.i("子元素","dispatchTouchEvent: "+ event);
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
            if (父容器需要拦截当前事件) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
        }
        return super.dispatchTouchEvent(event);
    }

②重写父元素也需要在 onInterceptTouchEvent 默认拦截除了 down 以外的其他事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false) 方法时,父元素才能继续拦截所需的事件。
伪代码:

 	@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int action = event.getAction();
        Log.i("父容器","onInterceptTouchEvent: "+ event);
        if (action == MotionEvent.ACTION_DOWN) {
            return false;//down事件传递到子元素,在子元素中开始 dispatchTouchEvent 的循环处理
        } else {
            return true;// move和up事件返回 true,这时本控件的onTouchEvent 会执行。
        }
    }

对于内部拦截法我有一个疑问:

  1. 父元素拦截除了 down 事件以外的其他事件,那么父元素在拦截 move 事件后,这个move 事件就让不能传递到子元素中,那么子元素的 move 事件就无法接收到被父元素拦截的 move 事件,那怎么在子元素的 move 事件里面执行 parent.requestDisallowInterceptTouchEvent(false) ?有老铁知道的留个言!

其实这个问题就出现在 “那么父元素在拦截 move 事件后” 这里了!这里的问题就已经假设了父元素已经拦截的 move 事件,其实不然!在父容器拦截之前,还有一个 dispatchTouchEvent 过程,这个父 dispatchTouchEvent 会调用 子dispatchTouchEvent ,就是在 子dispatchTouchEvent 中我们设置了 parent.requestDisallowInterceptTouchEvent(false) 和 父元素中我们已经拦截了 move 事件,这才导致父元素才能成功拦截 move 事件。

对于滑动冲突的内部拦截中,左右切换时,在父元素里处理序列事件的流程图(虚线框内表示不执行):
序列事件的流程图
序列事件的流程图

不知道你看到第 move 事件的执行流程会不会感觉奇怪呢?为什么第一个 move 事件不执行 onInterceptTouchEvent 方法?那是因为在 down 事件的 dispatchTouchEvent 中设置了 parent.requestDisallowInterceptTouchEvent(true) 父元素不拦截事件,就导致直接去执行子元素的dispatchTouchEvent 方法了。那为什么第二个 move 事件又拦截 move事件呢?因为在第一个 move 事件当 滑动距离水平大于竖直时,子元素 dispatchTouchEvent 设置了parent.requestDisallowInterceptTouchEvent(false) 父元素拦截事件,所以就会执行 onInterceptTouchEvent 方法。

测试源码在Github,滑动冲突外部和内部拦截

站在巨人的肩膀上:
Android 开发艺术探究 ——任玉刚

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值