自定義控制元件(二) 從原始碼分析事件分發機制

飯小龍發表於2017-12-01

系列文章傳送門 (持續更新中..) :

自定義控制元件(一) Activity的構成(PhoneWindow、DecorView)

自定義控制元件(三) 原始碼分析measure流程

自定義控制元件(四) 原始碼分析 layout 和 draw 流程


  • 很多安卓初學者都對 View 的事件分發機制感到困惑,但是這是務必要掌握的知識點。日常開發中要處理複雜的滑動衝突問題,就需要對事件分發的流程足夠熟悉。在上一篇文章裡, 我們瞭解了 Activity 的視窗結構, 今天我們看一下 View 的點選事件具體是怎樣分發。自定義控制元件(一) Activity的構成(PhoneWindow、DecorView)

話不多說, 先上圖

事件分發機制原理圖
圖片取自 - 圖解 Android 事件分發機制

  • 這裡分析的點選事件, 首先我們要明白分析的物件就是 MotionEvent。當一個點選事件產生時, 它的傳遞順序是 Activity -> Window(PhoneWindow) -> View(DecorView),即事件先傳遞給 Activity,Activity再傳給PhoneWindow,最後再傳遞給頂級View即DecorView。DecorView 接收到事件後,就會按照事件分發機制去分發事件。

事件分發的過程由三個很重要的方法來共同完成:

  • public boolean dispatchTouchEvent(MotionEvent event) { }

    • 用來事件的分發。如果 view 能接收到事件,那麼此方法一定會呼叫。返回的結果受當前 view 的 onTouchEvent 和下級 view 的 onInterceptTouchEvent 的結果影響,表示是否分發當前事件
  • public boolean onInterceptTouchEvent(MotionEvent ev) { }

    • 在上面方法內部呼叫,用來決定是否攔截某個事件。如果當前 view 攔截了某個事件,那麼在同一事件序列中,此方法不會再次呼叫。返回結果表示是否攔截事件
  • public boolean onTouchEvent(MotionEvent event) { }

    • 在 dispatchTouchEvent 方法中呼叫,同來處理點選事件。返回值表示是否消耗當前事件,如果不消耗,那麼在同一個事件序列中,當前 view 無法再次接收到事件。

這三個方法的關係可以用下面的虛擬碼來直觀的表示:

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(), 進行下一輪分發, 如此反覆直到事件最終被處理。

下面,讓我大致看一下原始碼中的分發流程。

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;
 }
複製程式碼

相關文章