Android事件分发原理详解

Android事件分发机制是android工程师必须掌握的基础知识,网上关于事件分发的文章有很多,这里我来阐述下自己对事件分发的看法。

MotionEvent

何为事件?事件就是用户手指触摸屏幕时,产生的的一系列触摸行为。Android中将事件定义为MotionEvent,其中事件的类型又分为四种:

  • ACTION_DOWN:按下
  • ACTION_MOVE:滑动
  • ACTION_UP:抬起,对应ACTION_DOWN
  • ACTION_CANCEL:取消,非人为因素造成的事件结束

正常用户一次从触摸屏幕到离开屏幕会产生一系列事件,根据情况不同又可分为两种:

  1. 点击屏幕后立即松开,事件序列为ACTION_DOWN—>ACTION_UP。
  2. 点击屏幕滑动一段距离后松开,事件序列为ACTION_DOWN—>ACTION_MOVE—>ACTION_UP。
    Android事件分发原理详解_第1张图片
    事件产生必然会有接收者接收事件,那么接收者是谁呢?我们一起深入源码一探究竟。
    事件分发的入口是在Activity中的dispatchTouchEvent(MotionEvent ev),这里要解释下为什么事件分发入口是在Activity中的dispatchTouchEvent(MotionEvent ev)中,下面给出一张用户触发事件图:
    Android事件分发原理详解_第2张图片

Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
		//判断当前是否为ACTION_DOWN事件
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //调用了Window的superDispatchTouchEvent
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //如果Window的superDispatchTouchEvent返回false,则继续执行onTouchEvent(ev),否则不会执行
        return onTouchEvent(ev);
    }
		
	//该方法是个空实现,当activity处于栈顶时,触屏点击按home,back,menu键等都会触发此方法
	public void onUserInteraction() {
    }


Window是个抽象类,它的唯一子类是PhoneWindow。当调用PhoneWindow的superDispatchTouchEvent(ev)返回true时,Activity#dispatchTouchEvent方法结束并返回true,否则继续执行Activity#onTouchEvent(ev)。

Phonewindow#superDispatchTouchEvent(ev)

public boolean superDispatchTouchEvent(MotionEvent event) {
		//mDecor是DecorView的实例对象,调用了DecorView#superDispatchTouchEvent
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView#superDispatchTouchEvent(MotionEvent event)

public boolean superDispatchTouchEvent(MotionEvent event) {
		//DecorView的父类是FrameLayout,故这里会调用父类(ViewGroup)的dispatchTouchEvent(event)
        return super.dispatchTouchEvent(event);
    }

通过上述源码分析可知实际上事件从Activty传入到ViewGroup中。
ViewGroup#dispatchTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent ev) {
	***
	boolean handled = false//在事件分发的过程中如果某个View消费了事件,则会将handled 置为true,最后会将handled 返回。
	if (actionMasked == MotionEvent.ACTION_DOWN) {
	//当前事件为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.
     
    //取消并且清空TouchTarget,TouchTarget是一个单项链表结构的数据类,其中保存了当前触摸的
    View以及下一个TouchTarget的实例引用。此处会将mFirstTouchTarget置null。
    cancelAndClearTouchTargets(ev);
    
    //重置触摸状态  将FLAG_DISALLOW_INTERCEPT标记为取反。
    resetTouchState();
 }
            
	// Check for interception.//检查是否拦截
       final boolean intercepted;
      if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    
         //当事件为ACTION_DOWN时,disallowIntercept 一定为false,因为resetTouchState()
           将FLAG_DISALLOW_INTERCEPT标记位进行了取反操作。
           所以这里FLAG_DISALLOW_INTERCEPT标记位可以影响除了ACTION_DOWN事件以外
           的事件,子类可以通过getParent.requestDisallowInterceptTouchEvent(boolean disallowIntercept)
           改变FLAG_DISALLOW_INTERCEPT标记位,从而来决定是让父类否拦截事件。
                
           final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
          //当为ACTION_DOWN时disallowIntercept为false,必定会调用onInterceptTouchEvent(ev),
           该方法默认是返回false,也就是不拦截事件,子类(只能是ViewGroup,只有ViewGroup才能拦截事件)
           可重写该方法。
            if (!disallowIntercept) {
               intercepted = onInterceptTouchEvent(ev);
               ev.setAction(action); // restore action in case it was changed
              } else {
                	//如果disallowIntercept为true则表示不拦截
                    intercepted = false;
                }
          } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                //没有触摸目标且当前事件不为ACTION_DOWN,则继续拦截触摸事件
                intercepted = true;
            }
			***
			TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {//没有拦截事件
				***
			if (actionMasked == MotionEvent.ACTION_DOWN
               || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
               //这里会对ACTION_DOWN事件进行特殊处理
               
                 ***
                 
              final int childrenCount = mChildrenCount;
              if (newTouchTarget == null && childrenCount != 0) {
                 for (int i = childrenCount - 1; i >= 0; i--) {
                  //这里需要解释下为什么会采用倒叙遍历子View,这是一个概率问题,一般外层View
                  消费事件的概率较内层View大,所以采用倒叙遍历的方式,这样可以减少遍历子View
                  的时间。
                final int childIndex = getAndVerifyPreorderedIndex(
                                   childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                                    
			  		              ***		
			  		             
			    if (!canViewReceivePointerEvents(child) || 
						!isTransformedTouchPointInView(x, y, child, null)) {
						
					//这里对当前子View进行了条件判断,当view不可见、view在执行动画或者当前触摸
					位置不在子View范围内时,则跳过该View继续遍历子View。
                       ev.setTargetAccessibilityFocus(false);
                       continue;
											
				if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
					//该方法会将当前时间分发给子View,稍后去看源码,接着往下走。
				    //当上述条件成立时,会执行下面代码。
				    
					                             ***
					
				   newTouchTarget = addTouchTarget(child, idBitsToAssign);//将当前子View跟
				   Touchtarget对象绑定,并将绑定的Touchtarget对象赋值给mFirstTouchTarget,
				   这里也侧面说明了如果该子View能够消费事件,那么mFirstTouchTarget就指向
				   该View,否则mFirstTouchTarget为null。
					可能这里大家心里会有疑问,问什么要将当前子View与Touchtarget绑定?原因是
					为了后续事件能够快速响应,不需要在遍历全部子View。
									
                    alreadyDispatchedToNewTouchTarget = true;//该标记位标识已经将事件分发给了
                    新的View
                    break;//跳出子View遍历循环。
							  }
                            }
                    	}
					}
						// Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
            	//mFirstTouchTarget 为空则说明没有子View消费事件,会调用dispatchTransformedTouchEvent
            	并且第三个参数传入的为null
            	
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {	//代表有子View能够消费事件
				TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                
                	//alreadyDispatchedToNewTouchTarget 是在对ACTION_DOWN事件特殊处理时赋值为true,
                	也就是说当前事件为ACTION_DOWN时,handled会被赋值为true。
                	
                	if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {//这里也说明了如果子View消费了事件,ViewGroup不会对后续事件进行处理,全部交给
                    子View处理
                    
                    	***
                    	'
                     //一个事件序列中ACTION_DOWN事件后的其他事件都会执行
                    dispatchTransformedTouchEvent,该方法在我们上述分析中出现过多次,
                    接下来我们就看看该方法对子View做了什么。
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                             target.child, target.pointerIdBits)) {
                             
                        handled = true;
                        }
                    }
                }
			}
         }
	}	
	***
return handled
}

ViewGroup#dispatchTransformedTouchEvent


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
		final boolean handled;
		                             ***
		if (child == null) {
				如果传入的子View不存在,则会调用父类(View)的dispatchTouchEvent(event)方法。
                handled = super.dispatchTouchEvent(event);
            } else {
            	//如果子View存在,则会调用子View的dispatchTouchEvent(event)。
                handled = child.dispatchTouchEvent(event);
            }
            return handled;
}

至此ViewGroup事件的分发就结束了,这里总结下整个ViewGroup事件分发流程。
先通过伪代码来整理下思路:

 public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        boolean intercepted = false;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //子View可通过getParent.requestDisallowInterceptTouchEvent请求父容器拦截事件
            final boolean disallowIntercept = (mDroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {//如果允许父容器拦截事件,会调用父容器onInterceptTouchEvent方法
                intercepted = onInterceptTouchEvent(ev);
            } else {
                intercepted = false;//否则父容器不拦截事件
            }
        } else {
            intercepted = true; //没有子View消费事件或者当前不是ACTION_DOWN事件,事件就不会继续传递下去
        }
        //判断是否拦截事件
        if (intercepted ) {
            //如果拦截,则ViewGroup调用自身onTouchEvent方法
            consume = onTouchEvent(ev);
        } else {
            //如果不拦截,则调用子View的dispatchTouchEvent方法
            consume = child.dispatchTouchEvent(ev);
        }
        return consume;
    }

再来张流程图,加深理解。
Android事件分发原理详解_第3张图片
ViewGroup中的dispatchTouchEvent负责将事件进行分发,并不会消费事件,如果有子View则会将事件传递下去,我们来看下子View中是如何处理事件。

View#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
		***
		boolean result = false;
         ListenerInfo li = mListenerInfo;//mListenerInfo包含View的很多监听,有onLongClickListener,
         onClickListener等
         if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                    //如果设置onTouchListener ,并且onTouchListener#onTouch返回true,则
                    View#dispatchTouchEvent返回true,表示子View消费事件。
            result = true;
          }
          //如果未设置onTouchListener,则会调用View#’onTouchEvent,如果该方法返回true, 
          View#dispatchTouchEvent返回true,表示子View消费事件。
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        ***
        return result;
    }

View#dispatchTouchEvent中不会再对事件进行分发,这里子View会决定是否消费事件,如果调用了View.setOnTouchListener(onTouchListener listener)且onTouchListener#onTouch
返回true,那么此次事件消费,否则会调用 onTouchEvent(event)。如果设置了onTouchListener,会先执行onTouchListener#onTouch方法,根据其返回值决定是否调用onTouchEvent。也就是onTouchListener#onTouch要先于onTouchEvent执行。

View#onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
	***
	final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //当前View是否为可点击的
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
		
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;//如果当前View状态是DISABLED,那么只要是可点击的,依然能够消费事件。
        }
		if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
		 switch (action) {
		 //当接触到屏幕时
		 case MotionEvent.ACTION_DOWN:
			***
			mHasPerformedLongPress = false;//将该变量赋值为false,表示还未处理长按事件。
			***
			// Walk up the hierarchy to determine if we're inside a scrolling container.
			//判断当前子View是否在滚动容器中
            boolean isInScrollingContainer = isInScrollingContainer();

             // For views inside a scrolling container, delay the pressed feedback for
             // a short period in case this is a scroll.
             //对于滚动视图中的View,短时间内延时按下的回调,以免当前正处于滚动状态。
              if (isInScrollingContainer) {//如果当前正处于滚动状态
                  mPrivateFlags |= PFLAG_PREPRESSED;
                  if (mPendingCheckForTap == null) {
                     mPendingCheckForTap = new CheckForTap();//CheckForTap实现了runnable接口
                   }
                   mPendingCheckForTap.x = event.getX();
                   mPendingCheckForTap.y = event.getY();
                   postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());//通过postDelayed
                   方法名称可知是延时一段时间执行runnable中run方法, ViewConfiguration.getTapTimeout()是
                   100毫秒。CheckForTap中run方法会执行checkForLongClick方法,检查长按事件。
                } else {//如果当前子View没有处于正在滚动的容器中
                  // Not inside a scrolling container, so show the feedback right away
                  setPressed(true, x, y);
                  checkForLongClick(0, x, y);//这里也会执行checkForLongClick 
               }
		break;
		//当离开屏幕时
		case MotionEvent.ACTION_UP:
		***
		//PFLAG_PRESSED标志位在ACTION_DOWN中调用setPressed(true, x, y)设置,此处prepressed 为true
		boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        	***
        	//mHasPerformedLongPress 如果此时为false,说明长按事件没有触发,那么解除长按事件延时执行
        	事件,这里可能大家会有点蒙,什么叫解除长按事件延时执行呢?后面会回大家揭晓,这里先跟着流
        	程走完。mIgnoreNextUpEvent默认是false,那么所有条件都满足。
        	if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                  // This is a tap, so remove the longpress check
                   removeLongPressCallback();//移除长按事件延时
                   // Only perform take click actions if we were in the pressed state
                   //当只有处于按下状时,才会执行click行为,这里的click是不是就是View#onClickListener中的
                   onClick方法呢?我猜是的,接着往下走。
                    if (!focusTaken) {
                        // Use a Runnable and post this rather than calling
                        // performClick directly. This lets other visual state
                        // of the view update before click actions start.
                         if (mPerformClick == null) {
                            mPerformClick = new PerformClick();//PerformClick也实现了Runable,在其run方法中调用
                            了performClickInternal()。
                           }
                         if (!post(mPerformClick)) {
                               performClickInternal();//这里也调用了performClickInternal(),先说结果吧!
                               在performClickInternal中拖过一系列的调用最终会调用View#OnClickListener中的onClick方法,至此时间分发也就结束了。
                               }
                            }
                        }
        }
		break;
		}
		return true;//这里的返回值会返回至View#dispatchTouchEvent,View#dispatchTouchEvent的返回值会
		返回值ViewGroup#dispatchTransformedTouchEvent,ViewGroup#dispatchTransformedTouchEvent返回值会返回至ViewGroup#dispatchTouchEvent,这样事件消费情况一级一级网上传,直到回到Activity#dispatchTouchEvent。
 }
 retrun false;
}

在View#onTouchEvent中首先会执行ACTION_DOWN(事件的开始肯定是从ACTION_DOWN开始),在ACTION_DOWN中会对长按事件进行检测,那么具体是怎么检测呢?带着问题看看之前分析所遗漏的checkForLongClick方法。
View#checkForLongClick

 private void checkForLongClick(int delayOffset, float x, float y) {
 		//如果当前View是可长按的就会往下执行。
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        	//将mHasPerformedLongPress 置为false,表示长按事件没有执行。
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();//
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);//这里会发送一个延时任务,也就是在
                    ACTION_UP中判断是否已经执行了长按事件,如果没有执行则解除延时任务,这是同一个任
                    务。这里延迟时间 ViewConfiguration.getLongPressTimeout()是500毫秒。
        }
    }

如果当前View可处理长按事件,那么发送一个500毫秒的延时任务,这个延时任务具体执行内容是什么呢?CheckForLongPress中的run方法会告诉我们答案。

private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                    
				//1.会执行performLongClick方法,并将mHasPerformedLongPress
				
                if (performLongClick(mX, mY)) {
                 置为true
                    mHasPerformedLongPress = true;
                }
            }
        }


public boolean performLongClick(float x, float y) {
        mLongClickX = x;
        mLongClickY = y;
        
        //2. 这里又会执行performLongClick,跟在ACTION_UP中的performClickInternal
        
        final boolean handled = performLongClick();
        有点相似啊!我猜这里面就会调用View#onLongClickListener中的onLongClick方法。
        mLongClickX = Float.NaN;
        mLongClickY = Float.NaN;
        return handled;
    }


 public boolean performLongClick() {
 
 		//3.performLongClickInternal
 		
        return performLongClickInternal(mLongClickX, mLongClickY);
    }
    
private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
        
        	//4.执行View#onLongClickListener中的onLongClick方法,并将该返回值返回
        	
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
      	***
        return handled;
    }

直到此处,豁然明朗。在View#onTouchEvent方法中首先会执行ACTION_DOWN,在ACTION_DOWN中会对长按事件进行检测,并发送一个500毫秒的延时任务,在ACTION_UP中对mHasPerformedLongPress 判断是否执行了长按事。这里涉及到一个时间问题,如果ACTION_DOWN到ACTION_UP事件发生时间间隔不到500毫秒,长按事件还没执行,则会执行View#onClickListener中onClick。如果时间间隔大于500毫秒则会执行View#onLongClickListener中onLongClick,这里会出现两种情况:
1、onLongClick方法返回true时,表示消费了长按事件,则不会再执行View#onclickListener中onClick。
2、onLongClick方法返回false时,表示没有消费长按事件,会执行View#onclickListener中onClick。
通过上述分析可以得出结论:
1、如果View设置OnLongClickListener,从接触屏幕到离开屏幕至离开屏幕时间间隔不足500毫秒,不会执行OnLongClickListener#onLongClick。
2、如果View设置OnLongClickListener、OnClickListener,并且OnLongClickListener#onLongClick返回true,不会执行OnClickListener#onClick;如果返回false,则会执行OnClickListener#onClick。

View#dispatchTouchEvent事件处理结论

View中是对事件进行消费而不是分发,如果View事件没有消费,则会交给上层View处理。如果当前View设置OnTouchListener,且OnTouchListener#onTouch返回true,则事件被消费不会继续传递。返回false则将事件传递至onTouchEvent,如果View设置OnLongClickListener、OnClickListener,如果OnLongClickListener#onLongClick返回true,OnClickListener#onClick不会执行,反之则会执行。综上可知OnTouchListener、OnLongClickListener、OnClickListener优先级层层降低,OnTouchListener优先级最高。个人语言表达能力实在有限,那么画张图帮大家梳理梳理。我还是你的小佩琪.jpg
Android事件分发原理详解_第4张图片

事件分发机制原理总结

  • Acttivity
    事件分发的入口是在Acttivity#dispatchTouchEvent。通过系列传递,Activity会将事件传递至ViewGroup中,如果事件在ViewGroup或其子类中被消费,那么事件传递结束;否则,事件将会被Activity#onTouchEvent处理。

  • ViewGroup
    ViewGroup中的事件是Activity传递而来,如果ViewGroup不对事件进行拦截,寻找当前ViewGroup中全部子View,将事件传递给当前手指触摸位置在其范围内的子View。如果拦截事件,当前序列事件的其他事件(MOVE,UP)都将不会再传递,全部交由ViewGroup#onTouchEvent处理,如果onTouchEvent消费事件,则事件传递结束,否则,将事件传回Activity#onTouchEvent。

  • View
    事件传递至View#dispatchTouchEvent,直接View#onTouchEvent。如果View消费事件,那么事件传递结束;如果没有消费事件,View将事件传回至ViewGroup中,交由ViewGroup#onTouchEvent处理。

具体流程图如下:
Android事件分发原理详解_第5张图片
相信看到这里大家对事件分发应该有了全新的认识。在下水平有限,若文中有错,欢迎纠正。
本片文章就到此结束啦!咱们下期见!

你可能感兴趣的:(Android事件分发原理详解)