自定义控件(二) 从源码分析事件分发机制

系列文章传送门 (持续更新中..) :bash

自定义控件(一) Activity的构成(PhoneWindow、DecorView)源码分析

自定义控件(三) 源码分析measure流程post

自定义控件(四) 源码分析 layout 和 draw 流程动画


  • 不少安卓初学者都对 View 的事件分发机制感到困惑,可是这是务必要掌握的知识点。平常开发中要处理复杂的滑动冲突问题,就须要对事件分发的流程足够熟悉。在上一篇文章里, 咱们了解了 Activity 的窗口结构, 今天咱们看一下 View 的点击事件具体是怎样分发。自定义控件(一) Activity的构成(PhoneWindow、DecorView)

话很少说, 先上图ui

事件分发机制原理图
图片取自 - 图解 Android 事件分发机制

  • 这里分析的点击事件, 首先咱们要明白分析的对象就是 MotionEvent。当一个点击事件产生时, 它的传递顺序是 Activity -> Window(PhoneWindow) -> View(DecorView),即事件先传递给 Activity,Activity再传给PhoneWindow,最后再传递给顶级View即DecorView。DecorView 接收到事件后,就会按照事件分发机制去分发事件。

事件分发的过程由三个很重要的方法来共同完成:

  • public boolean dispatchTouchEvent(MotionEvent event) { }this

    • 用来事件的分发。若是 view 能接收到事件,那么此方法必定会调用。返回的结果受当前 view 的 onTouchEvent 和下级 view 的 onInterceptTouchEvent 的结果影响,表示是否分发当前事件
  • public boolean onInterceptTouchEvent(MotionEvent ev) { }spa

    • 在上面方法内部调用,用来决定是否拦截某个事件。若是当前 view 拦截了某个事件,那么在同一事件序列中,此方法不会再次调用。返回结果表示是否拦截事件
  • public boolean onTouchEvent(MotionEvent event) { }代理

    • 在 dispatchTouchEvent 方法中调用,同来处理点击事件。返回值表示是否消耗当前事件,若是不消耗,那么在同一个事件序列中,当前 view 没法再次接收到事件。

这三个方法的关系能够用下面的伪代码来直观的表示:rest

public boolean dispatchTouchEvent(MotionEvent ev) {
	boolean result=false;
	if(onInterceptTouchEvent(ev)){
	      result=super.onTouchEvent(ev);
	 }else{
	      result=child.dispatchTouchEvent(ev);
	}
return result;
复制代码

经过上面的伪代码, 咱们能够直观的明白事件分发的大致流程。当一个点击事件产生时,根 View 接收到事件并调用 dispatchTouchEvent() 来对事件进行分发, 在方法内部先调用 onInterceptTouchEvent() ,若是返回 true 就表示要拦截这个事件,那么接下来这个事件就会交给这个 ViewGroup 经过调用本身的 onTouchEvent() 来处理。若是这个 ViewGroup 的 onInterceptTouchEvent 返回 false,表示它本身不拦截当前事件,事件就会分发给它的子view,即调用子view 的 dispatchTouchEvent(), 进行下一轮分发, 如此反复直到事件最终被处理。code

下面,让我大体看一下源码中的分发流程。

1. Activity 分发过程:

#Activity
public boolean dispatchTouchEvent(MotionEvent ev) { 
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
复制代码

Activity 接收到事件后, 首先交给 Activity 所属的 Window 分发, 若是返回 true,整个事件的循环就结束了,返回 false 则表示事件没人处理,全部view 的 onTouchEvent 都返回了 false, 则调用 Activity 本身的 onTouchEvent 来处理事件。从上一篇文章里了解到这个 Window 实现子类就是 PhoneWindow。

#PhoneWindow
public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
    return mDecor.superDispatchKeyShortcutEvent(event);
}
复制代码

PhoneWindow 接着把事件分发给 DecorView, 也证明了以前说分的发过程 Activity -> PhoneWindow -> DecorView

2. 顶级 View 的分发过程:

在这里, 事件分发到了顶级View之后, 会调用 ViewGroup 的 dispatchTouchEvent() 方法, 从这里开始, 后面就是View 之间的事件分发了.

#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
复制代码

咱们继续看 ViewGroup 中的 dispatchTouchEvent() 方法, 方法有点长, 大致的代码解释我都标明在代码里面了, 方便你们理解:

public boolean dispatchTouchEvent(MotionEvent ev) {

	...
	
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
		
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            // 在 resetTouchState ()内部会重置标记位 FLAG_DISALLOW_INTERCEPT, 
            // 所以 requestDisallow...() 不能影响 ViewGroup 对 ACTION_DOWN 的拦截
            resetTouchState(); 
        }
        
        final boolean intercepted;
        
        /**
         * 注意 if 的判断条件: 
         * 1. 若是是 ACTION_DOWN , 则 if 判断的条件为true
         * 2. 若是 ViewGroup 不拦截 ACTION_DOWN 而且交给子view 处理了事件 , 则会在后面的方法中给 
         *    mFirstTouchTarget 赋值,即 mFirstTouchTarget != null。此时 if 的判断条件为 true
         * 3. 若是 ViewGroup 拦截了事件,则 mFirstTouchTarget = null, if 的判断条件为 false 
         *    后面的 move、up都直接进 else{} 里面, 就不会再调用本身的onInterceptTouchEvent() 
         *    方法,该事件序列的其它事件都会交给本身处理
         */
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            // 此处的标志位 FLAG_DISALLOW_INTERCEPT 由子类的requestDisallowInterceptTouchEvent 
            // 决定可是在 ACTION_DOWN 来临时,会在 resetTouchState() 中将该标记位重置, 因此子类不能影
            // 响父类对 ACTION_DOWN 的处理
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);    // 判断是否拦截事件: ViewGroup 默认是不拦截的
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

	    ...
	    
	    // 若是事件没有被拦截, 则会进入这里面
        if (!canceled && !intercepted) {                     
        
           ...
           
           final View[] children = mChildren;
           // 遍历子集
           for (int i = childrenCount - 1; i >= 0; i--) {                   
               final int childIndex = getAndVerifyPreorderedIndex(
                       childrenCount, i, customOrder);
               final View child = getAndVerifyPreorderedView(
                       preorderedList, children, childIndex);
				
               // 判断子view 可否接受到点击事件, 若是能够, 则把事件交给该 子view 处理
               // 1. 子view 是否在执行动画且是 VISIBLE 状态; 
               // 2.点击事件的坐标是否在 子view 的区域内
               if (!canViewReceivePointerEvents(child) 
		               || !isTransformedTouchPointInView(x, y, child, null)) { 
                   ev.setTargetAccessibilityFocus(false);
                   continue;
               }
               
               ...
               
			   // 方法内部把事件分发给子view 的 dispatchTouchEvent() 方法处理
               if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){  

	               ...
	                 
	               // 若是子元素开始处理事件,即它的 dispatchTouchEvent() 返回 true,会走到这里, 
	               // 而且在 addTouchTarget() 方法内部会给 mFirstTouchTarget 赋值
	               newTouchTarget = addTouchTarget(child, idBitsToAssign); 
	               alreadyDispatchedToNewTouchTarget = true;               
	               break; 
               }				 
        }

        // 若是该事件没有被处理, 会走到这里面
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);  // 注意这里参数 child 传入的是 null , 内部则会调用 View 的 dispatchTouchEvent()方法, 而后调用 onTouchEvent 本身处理事件
        } else {}
        
       ...
           
    return handled;
}
复制代码

上面 dispatchTouchEvent() 中调用的几个方法:

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // 重置标记位 FLAG_DISALLOW_INTERCEPT
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}


public boolean onInterceptTouchEvent(MotionEvent ev) {	
    return false; // ViewGroup 默认返回 false
}


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
	        // 等于null时, 会调用 View 的dispatchTouchEvent() 方法, 由于View没有子元素, 因此会直接交给本身处理
            handled = super.dispatchTouchEvent(event);  
        } else {
	        // 传递的参数 child 不等于 null, 则会调用子元素的 dispatchTouchEvent(),从而完成了一轮事件的分发
            handled = child.dispatchTouchEvent(event);  
        }
        event.setAction(oldAction);
        return handled;
    }
}

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;     // 给 mFirstTouchTarget 赋值
    return target;
}
复制代码
  • 从 ViewGroup 的事件分发方法中能够看出,:
  1. 先判断是否要调用本身的 onInterceptTouchEvent() 方法有两个条件: 事件为 ACTION_DOWNmFirstTouchTarget != null, 而 mFirstTouchTarget 是在当前的 ViewGroup 不拦截事件, 而且子元素开始处理事件时会被赋值并指向子元素。因此一旦当前的 ViewGroup 拦截了事件,则 mFirstTouchTarget != null 就不成立, 然后续的 move、up 事件,因为 if 的判断条件为 false,致使都不会再调用 ViewGroup 的 onInterceptTouchEvent, 而且同一事件序列的其它事件都会默认交给它处理
  2. FLAG_DISALLOW_INTERCEPT 这标记是由子类的 requestDisallowInterceptTouchEvent 来决定的,可是在 ACTION_DOWN 发生时,会在 resetTouchState() 中重置这个标志位,。因此当子view 设置了 FLAG_DISALLOW_INTERCEPT ,它的 ViewGroup 将没法拦截除了 ACTION_DOWN 之外的事件。所以,当面对 ACTION_DOWN 事件来临时,ViewGroup 老是会调用本身的 onInterceptTouchEvent 方法来决定是否拦截事件。
  3. 当 ViewGroup 决定拦截事件后,后序的点击事件默认会交给它本身处理, 而不会重复再调用 onInterceptTouchEvent。因此,onInterceptTouchEvent 不是老是被调用的,当咱们要提早处理点击事件时,要使用 dispatchTouchEvent
  4. 若是子元素的 dispatchTouchEvent 返回 true, 那么就会跳出 ViewGroup 遍历子集的循环,并在 addTouchTarget()mFirstTouchTarget 赋值
  5. 在遍历全部子元素后事件没有被合适的处理, 有两种状况: 1. ViewGroup 没有子元素; 2. 或者 子view 处理了点击事件可是在 dispatchTouchEvent() 方法里面返回 false, 通常是 onTouchEvent() 返回了false

3. View 对点击事件的处理过程:

  • View 对点击事件的处理过程简单一点, 由于 View 是一个单独的元素, 没有子元素因此没法向下分发事件, 所以只能本身处理事件

先看它的 dispatchTouchEvent :

public boolean dispatchTouchEvent(MotionEvent event) {
        
        ...

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            // 首先判断有没有设置 onTouchListener, 若是 onTouch() 返回 true, 则不会再走 onTouchEvent()
            // 说明 mOnTouchListener.onTouch() 的优先级要比 onTouchEvent() 高
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED       
                    && li.mOnTouchListener.onTouch(this, event)) {  
                result = true;
            }

            if (!result && onTouchEvent(event)) {   // 这里处理和消费事件
                result = true;
            }
        }
        return result;
    }
复制代码

从这段源码能够看出,:View 对点击事件的处理过程, 首先判断有没有设置 OnTouchListener ,若是 OnTouchListener 中的 onTouch 中返回了 true, 那么 onTouchEvent 不会被调用, 可见 OnTouchListener 的优先级高于 onTouchEvent , 这样作的好处是方便在外界处理点击事件

继续看 onTouchEvent :

public boolean onTouchEvent(MotionEvent event) {
 // 从这里能够看出来, 即便当view 处于 DISABLED 不可用的状态时, 它依然能够消耗点击事件
 if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } // 若是 View 设置有代理, 那么会执行 mTouchDelegate.onTouchEvent(event), 工做机制相似 onTouchListener if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 此处能够看出来只要 clickable 和 long_clickable 有一个为 true , 就能够消费这个事件 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in
                   // touch mode.
                   boolean focusTaken = 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
                       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)) {
                            // 这里内部会调用 onClick() (若是设置了 mOnClickListener)
                               performClick();
                           }
                       }
                   }

        return true;
    }

    return false;
}

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    // 这里能够看到若是设置了 mOnClickListener, 则会调用 onClick()
    if (li != null && li.mOnClickListener != null) {    
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

复制代码
  • View 的 LONG_CLICKABLE 属性默认是 false, 而 CLICKABLE 属性的值和具体的 view 有关, 确切来讲可点击的 view 为 true, 不可点击的为 false. 例如 TextView 是不可点击的, Button 是可点击的。能够经过 setOnClickListener 和 setOnLongClickListener设置 CLICKABLE 和 LONG_CLICKABLE 为 false。
public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
     if (!isLongClickable()) {
         setLongClickable(true);
     }
     getListenerInfo().mOnLongClickListener = l;
 }
复制代码
相关文章
相关标签/搜索