Android源码分析之View事件分发机制

什么叫事件分发机制

事件分发是:当发生了一个事件时,在屏幕上找到一个合适的控件来处理这个事件的过程。因为一个界面上控件如此之多,发生一个事件后总要寻找一个合适来处理事件吧。这个过程就叫做事件分发的机制。

事件分发流程

Activity是Android应用程序的门面和载体,它代表一个完整的用户界面。Activity提供了一个窗口来绘制各种视图,即PhoneWindow类。该类继承自顶层窗口类Window,并且包含一个DecorView类对象。DecorView继承自FrameLayout(帧布局),所以本质上是一个ViewGroup,而且是当前活动所放置的全部View的根视图(RootView)。当我们创建一个活动时,在活动的onCreate()方法中调用 setContentView(layoutID) 方法就是为该活动的ContentView部分指定布局内容从而完成GUI的渲染。

当用户点击屏幕产生一个事件,事件通过底层硬件捕获,然后交给ViewRootImpl处理,ViewRootImpl通过Window将事件交给Activity。事件要传递给Activity那么它就必须持有Activity的引用,Window在Activity的attach方法中通过mWindow.setCallback(this)调用持有了Activity的引用,Activity实现了Window.Callback的接口方法。所以最终事件是通过Window.Callback.dispatchTouchEvent把时间交给Acitivity的。

/**
 * 从窗口返回到调用方的API。这允许客户端拦截密钥调度、面板和菜单等。
 */
public interface Callback {
    public boolean dispatchKeyEvent(KeyEvent event);
    public boolean dispatchKeyShortcutEvent(KeyEvent event);
    public boolean dispatchTouchEvent(MotionEvent event);
    ...........
}

事件发生时,ViewRootImpl通过Window将事件交给Activity,然后再一层层的向下层传递,直到找到合适的处理控件。大致如下:

硬件 -> ViewRootImpl -> Window -> Activity -> PhoneWindow -> DecorView -> VIewGroup -> View

但是如果事件传递到最后的View还是没有找到合适的View消费事件,那么事件就会向相反的方向传递,最终传递给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:

Activity <- PhoneWindow <- DecorView <- ViewGroup <- View

事件的类型

事件主要分为触摸事件和点击事件。

触摸事件

触摸事件对应的是MotionEvent类,主要有以下三种类型

  • ACTION_DOWN:表示用户手指按下的动作,标志着触摸事件的开始。
  • ACTION_UP:表示用户手指离开屏幕的动作,标志着触摸事件的结束。
  • ACTION_CANCEL:如果某一个子View处理了Down事件,那么随之而来的Move和Up事件也会交给它处理。但是交给它处理之前,父View还是可以拦截事件的,如果拦截了事件,那么子View就会收到一个Cancel事件,并且不会收到后续的Move和Up事件。
  • ACTION_MOVE:表示用户手指移动的动作。当用户手指按下屏幕后,在松开之前,只要移动的距离超过了一定的阈值即判定为ACTION_MOVE动作。实际上,即使是手指非常 轻微的移动也会被系统监测到从而判定为ACTION_MOVE动作。

用户触摸屏幕操作由ACTION_DOWN事件开始,结束于ACTION_UP事件,可以有0次或多次ACTION_MOVE事件。

点击事件

用户手指按下→停留若干时间(可长可短)→用户手指松开,这一完整的过程视为一次点击事件。可以看出,触摸事件先于点击事件执行。

事件的分发

在我们平时的使用或写自定义View时,都会直接或间接的使用View的事件分发,View的事件分发主要与View源码中的3个方法有关:

  • dispatchTouchEvent()
  • onTouch()
  • onTouchEvent()

当事件发生时,ViewGroup会在dispatchTouchEvent方法中先看自己能否处理事件,如果不能再去遍历子View查找合适的处理控件。如果到最后result还是false,表示所有的子View都不能处理,才会调用自身的onTouchEvent来处理。View的事件分发我们需要看一个方法dispatchTouchEvent。我们通过View的事件分发的实现源码来分析分发流程。

/**
* 将触摸屏运动事件向下传递到目标视图,如果它是目标视图,则传递此视图。
* @param 事件要调度的运动事件。
* @return 如果事件由视图处理,则为True;否则为false。
*/
public boolean dispatchTouchEvent(MotionEvent event) {
    //省略代码
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //ListenerInfo是View的内部类,这个类定义了各种的监听以及事件,
        //包括焦点变化监听,滚动变化监听,点击事件监听等
        ListenerInfo li = mListenerInfo;
        //如果li.mOnTouchListener.onTouch(this, event)返回true,
        //并且view的状态是enable状态下,该方法的result就直接返回true
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //如果result=false,才会走下面的onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    //省略代码
    return result;
}

注释写得很清楚了,主要概括几个要点:

  • 如果我们传入的OnTouchListener中的onTouch返回true的话,并且在enable=true情况下,就不会执行到onTouchEvent方法
  • view.setEnable(false)的时候,不会执行onTouchListener的onTouch方法,但是会执行View自身的onTouchEvent方法
  • view.setEnable(false)的时候,OnClickLisener.onClick方法不会执行(下面提到)

dispatchTouchEvent中没有发现view的onClick方法的调用,其实onClick在onTouchEvent中,在判断事件类型的swithc中,在ACTION_UP擡起的事件中,我们看到这样的代码:

public boolean onTouchEvent(MotionEvent event) {
	...
	switch (action) {
		case MotionEvent.ACTION_UP:
		...
		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
             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();
                 }
                 if (!post(mPerformClick)) {
                     performClickInternal();
                 }
             }
         }
}

在performClickInternal中调用了 performClick()方法,就是在这个方法中执行了view的onClick方法

/**
 * Entry point for {@link #performClick()} - other methods on View should call it instead of
 * {@code performClick()} directly to make sure the autofill manager is notified when
 * necessary (as subclasses could extend {@code performClick()} without calling the parent's
 * method).
 */
private boolean performClickInternal() {
    // Must notify autofill manager before performing the click actions to avoid scenarios where
    // the app has a click listener that changes the state of views the autofill service might
    // be interested on.
    notifyAutofillManagerOnClick();
    return performClick();
}

在performClick中,可以看到如果设置了监听的话就会调用view的onClick方法。

 public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    notifyEnterOrExitForAutoFillIfNeeded(true);
    return result;
}

所以我们可以得出一个初步的结论。也就是说在view的onTouchEventListener中的onTouch()如果返回了true,表示此次事件被处理,也就是按下的ACTION_DOWN事件被消耗,但是ACTION_UP事件无法传递执行,所以onClick不会执行。
如果onTouchEventListener中的onTouch返回false。则会执行View自身的onTouchEvent方法,表示onTouch不会消耗事件,所以onClick点击事件会响应。
有一点需要特别注意:setClickable(false)需要在setOnClickListener之后调用才起作用,因为在setOnClickListener中将view设置setClickable(true)了

/**
  * Register a callback to be invoked when this view is clicked. If this view is not
  * clickable, it becomes clickable.
  * @param l The callback that will run
  * @see #setClickable(boolean)
  */
 public void setOnClickListener(@Nullable OnClickListener l) {
     if (!isClickable()) {
         setClickable(true);
     }
     getListenerInfo().mOnClickListener = l;
 }

ViewGroup事件传递

前面分析了View的事件分发,但在实际开发过程中真正要使用View事件分发时,基本都是因为ViewGroup的嵌套导致的内外滑动问题,所以对ViewGroup的事件分发更需要深入了解,和View的事件分发一样,ViewGroup事件分发一样与几个重要方法有关:

  • dispatchTouchEvent() -> 用来分派事件
  • onInterceptTouchEvent() -> 用来拦截事件
  • onTouchEvent() -> 用来处理事件

使用一段伪代码来表述上面三个方法在ViewGroup事件分发中的作用,代码如下:

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

从上面代码中看出,事件传递到ViewGroup时首先传递到dispatchTouchEvent(MotionEvent event)中,然后执行以下逻辑,首先在ViewGroup.dispatchTouchEvent() 中调用onInterceptTouchEvent() 方法:

  • 返回true,表示拦截事件 -> onTouchEvent() -> 返回true 表示消耗
  • 返回false,表示不拦截事件 -> child.dispatchTouchEvent(event) 事件向下传递,如此反复传递分发

在onInterceptTouchEvent() 返回false时,表明当前ViewGroup不消耗事件,此事件会向下传递给子View,此子View可能是View也可能是ViewGroup,如果是View则按照上面的事件分发消耗事件。
事件的传递首先是从手指触摸屏幕开始,所以我们先查看ViewGroup的dispatchTouchEvent()中的ACTION_DOWN方法,剔除剩余复杂的逻辑,方法有一段主要的代码:

public boolean dispatchTouchEvent(MotionEvent event){
	...
	// 标志自身是否拦截此事件
	final boolean intercepted;
	if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
	    // 当子View调用requestDisallowInterceptTouchEvent函数时该变量为true。
	    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  // 返回true表示子View设置了父容器不拦截事件
	    if (!disallowIntercept) {
	    	// 如果子View没有禁止父View拦截事件,父View通过该函数判断是否需要拦截此事件。
	        intercepted = onInterceptTouchEvent(ev);
	        ev.setAction(action); 
	    } else {
	        intercepted = false;
	    }
	} else {
		// 一定拦截事条件:事件类型不为ACTION_DOWN并且mFirstTouchTarget为null。
		// 这说明ACTION_DOWN事件已经被自身消耗,那么该事件序列中的剩余事件也应该被自身消耗。
	    intercepted = true;
	}
	...
}

上述代码虽然简单但ViewGroup的事件分发多半与此处的逻辑有关,里面的每个细节都会影响到最终的事件消耗,总结上面代码执行如下:

  • 首先当一个事件进来的时候,会先判断当前事件是否是down或着mFirstTouchTarget是否为null(当我们第一次进到ViewGroup.dispatchTouchEvent的时候mFirstTouchTarget会为空的),如果当前事件为down或者mFirstTouchTarget为null的时候就会调用ViewGroup.onInterceptTouchEvent方法,接着会将onInterceptTouchEvent的返回进行记录。
  • mFirstTouchTarget:指向处理触摸事件的子View;当ViewGroup子View成功拦截后,mFirstTouchTarget指向子View,此时满足mFirstTouchTarget != null,则在整个事件过程中会不断询问ViewGroup的拦截状况;
  • 如果ViewGroup确定拦截事件,mFirstTouchTarget为null,所以整个触摸事件不会询问ViewGroup的onInterceptedTouchEvent(),且之后的事件直接交给ViewGroup执行;
  • 可能有小伙伴会问,disallowIntercept这个值是什么东西,不知道大家有没有用过getParent().requestDisallowInterceptTouchEvent(true)方法来达到禁止父ViewGroup拦截事件,当我们给这个方法设置什么,disallowIntercept就会是什么值,所以我们在事件拦截的时候,可以在其子View里面调用该方法进行事件拦截。

当intercepted为false也就是不拦截的时候,就会遍历子元素,并将事件向下分发交给子元素进行处理:

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
       if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
             //给mFirstTouchTarget赋值
             newTouchTarget = addTouchTarget(child, idBitsToAssign);
      }
}

可以看到当在遍历子孩子的时候会调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法,,而在dispatchTransformedTouchEvent方法中我们会发现下面代码,通过下面代码会发现,当child不为空的时候他就会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成一轮分发。

if (child == null) {
     handled = super.dispatchTouchEvent(event);
 } else {
     handled = child.dispatchTouchEvent(event);
 }

当子View.dispatchTouchEvent返回为true时,就会调用addTouchTarget(child, idBitsToAssign)方法,该方法就是在给mFirstTouchTarget赋值。
当子View.dispatchTouchEvent返回为false时,就不会调用addTouchTarget(child, idBitsToAssign)方法,故mFirstTouchTarget为null。
那么mFirstTouchTarget为null时会出现什么情况呢,继续向下看,会看到下面的代码,注意这里的view传的是null,也就是说会调用super.dispatchTouchEvent(event)代码,super.dispatchTouchEvent(event)是什么呢?他就是我们自己的dispatchTouchEcent方法。也就是事件将我们自己去处理。

if (mFirstTouchTarget == null) {
     handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
 }

根据上面的View和ViewGroup的事件分发学习,这里给出几个View事件传递的结论(以下结论针对系统自动分发),并根据学习内容进行逐条分析

  • 事件序列指的是从手指接触屏幕那一刻起,到手指离开屏幕那一刻为止产生的所有事件。
  • ViewGroup默认不拦截任何事件。
  • 事件分发过程中ViewGroup会考虑多点触控的问题,例如在一个布局中有两个子控件,如果两个手指同时对它们进行操作,控件是可以正常响应的。
  • 正常情况下一个事件序列只能被一个View拦截或消耗,除非使用特殊方法控制事件传递;
  • 对于View一旦决定拦截事件即onTouchEvent()返回true,那后续的整个事件序列都会交给它消耗;
  • 如果View不消耗ACTION_DOWN事件,则后续的事件序列都不会再给他处理
  • 如果View在ACTION_DOWN时返回false,那系统的mFirstTouchTarget为null,在后续的MOVE、UP事件中onInterceptTouchEvent()不会再被调用,直接拦截事件

View事件拦截案例分析

事件拦截最经典的使用示例和场景就是滑动冲突,按照View的冲突场景分,滑动冲突可以分为3类:

  • 外部滑动和内部滑动方向不一致
  • 外部滑动和内部滑动方向一致
  • 以上两种情况嵌套

一般处理滑动冲突有两种拦截方法:外拦截和内拦截

外部拦截

外拦截顾名思义是在View的外部拦截事件,对View来说外部就是其父类容器,即在父容器中拦截事件,通过上面的代码我们知道,ViewGroup的事件拦截取决与onInterceptTouchEvent()的返回值,所以我们在ViewGroup中重写onInterceptTouchEvent()方法,在父类需要的时候返回true拦截事件,具体需要的场景要按照自己的业务逻辑判断:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
	switch (event.getAction()) {
	    case MotionEvent.ACTION_DOWN:
	    case MotionEvent.ACTION_UP:
	        break;
	    case MotionEvent.ACTION_MOVE:
		    if(isParentNeed()){
		    	//父容器的逻辑
		    	return true;
		    }
	        break;
		default:
		        break;
	}
	super.onInterceptTouchEvent(event)
    return false;
}

从上面代码中看出:在onInterceptTouchEvent()的ACTION_DOWN中必须返回false,即不拦截ACTION_DOWN事件,因为如果ACTION_DOWN一但拦截,事件后面的事件都会默认给ViewGroup处理,也不会再调用onInterceptTouchEvent()询问拦截,那子View将没有获取事件的机会;在ACTION_DOWN中,根据自己需要的时候返回true,那此时事件就会被父ViewGroup消耗。

内部拦截

内部拦截法父View拦截除ACTION_DOWN以外的其它事件。子View在ACTION_DOWN中调用getParent().requestDisallowInterceptTouchEvent(true)方法接管事件并在ACTION_MOVE中根据业务逻辑决定事件是否教给父View处理。如需交给父View处理则调用requestDisallowInterceptTouchEvent(false)方法。内部拦截法不符合事件分发流程,是通过子VIew反向控制父View拦截,规则:

  • 父元素要默认拦截除了ACTION_DOWN以外的其他事件
  • 子元素调用parent.requestDisallowInterceptTouchEvent(false/true)来控制父元素是否拦截事件
  • 父元素不能拦截ACTION_DOWN因为它不受FLAG_DISALLOW_INTERCEPT标志位控制,一旦父容器拦截ACTION_DOWN那么所有的事件都不会传递给子View
/**
 * 内部拦截法
 * 父View需拦截除DOWN以外的其他事件
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(ev);
        return false;
    } else {
        return true;
    }
}

/**
 * 内部拦截法
 * 子View.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 (isParentNeed()) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

上述代码是内部拦截的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章