1. 程式人生 > >Android開發——事件分發機制詳解

Android開發——事件分發機制詳解

0. 前言

深入學習事件分發機制,是為了解決在Android開發中遇到的滑動衝突問題做準備。事件分發機制描述了使用者的手勢一系列事件是如何被Android系統傳遞並消費的。

首先對事件分發機制進行概述:如果當一個點選事件發生時,事件最先傳遞給當前Activity,再傳遞給Window,接著傳遞給頂級View,最後按照事件分發機制去分發事件。事件的傳遞過程可以用以下虛擬碼進行描述:

public boolean dispatchTouchEvent(MotionEvent ev){//事件傳遞到,那麼該方法一定會被呼叫
boolean consume = false;
//onInterceptTouchEvent只存在於ViewGroup,判斷是否攔截該事件
//但不是每次都呼叫該方法,後面會詳細介紹
if(onInterceptTouchEvent(ev)){ 
    if(!OnTouchListener.onTouch(ev)){// OnTouchListener優先順序較onTouchEvent高
        consume = onTouchEvent(ev);//處理點選事件
}
}else{
    consume = child. dispatchTouchEvent(ev);
}
    return consume;
}

對於事件傳遞後的事件消費,如果一個View設定了OnTouchListener,則OnTouchListeneronTouch會首先被呼叫。若onTouch返回false,最後才輪到onTouchEvent去消費該事件(我們平時設定的OnclickListener就在onTouchEvent方法中,優先順序較低)。

onTouchEvent返回了true,則表示事件已經被消費了,否則它的父容器的onTouchEvent將會呼叫,以此類推,直至由ActivityonTouchEvent被呼叫。

整個過程的簡要流程圖如下所示:


1. 事件的分發詳解

如果需要再進一步分析事件的分發機制,那麼必須閱讀原始碼。

一個點選事件發生時,事件第一步最先傳遞給當前Activity

1. 1  Activity對事件的分發

public boolean dispatchTouchEvent(MotionEvent ev) {  
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {  
            onUserInteraction();  
        }  
        if (getWindow().superDispatchTouchEvent(ev)) {  
            return true;  
        }  
        return onTouchEvent(ev);  
}

從原始碼裡可以看出,事件交給了Activity所附屬的Window進行分發,返回true則結束事件分發,否則代表所有的ViewonTouchEvent返回了false(均不處理),這時是由ActivityonTouchEvent來處理。

1. 2  Window對事件的分發

WindowsuperDispatchTouchEvent()是一個抽象方法Window的唯一實現是PhoneWindow

下面程式碼便來自於PhoneWindow對事件的分發邏輯。

@Override  
public boolean superDispatchTouchEvent(MotionEvent event) {  
    return mDecor.superDispatchTouchEvent(event);  
}

從原始碼裡可以看出,事件交給了DecorView處理。我們繼續檢視DecorView的定義。

1. 3  DecorView的定義

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker{…}

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

從原始碼裡可以看出,DecorView繼承自FrameLayout的,毫無疑問FrameLayout又繼承了ViewGroup,那麼剩下的工作就是研讀 GroupViewdispatchTouchEvent方法了。

1. 4  ViewGroup

ViewGroupActivityView比,多了一個onInterceptTouchEvent()事件攔截方法,事件傳遞到ViewGrouponInterceptTouchEvent返回true,則事件由ViewGroup處理(OnTouchListeneronTouchEvent優先順序要高,這一點前面也介紹過了)。若返回falseViewdispatchTouchEvent會被呼叫。

但是onInterceptTouchEvent並不是每次都會呼叫並判斷是否攔截事件,ViewGroup.dispatchTouchEvent()的原始碼分析如下:

// 檢查是否進行事件攔截  
final boolean intercepted;  
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {  
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (!disallowIntercept) {  
    //回撥onInterceptTouchEvent(),返回false表示不攔截touch,否則攔截touch事件 
    intercepted = onInterceptTouchEvent(ev);  
    ev.setAction(action);
  } else {  
      intercepted = false;  
     }  
} else {  
   //沒有touch事件的傳遞物件,同時該動作不是初始動作down,ViewGroup繼續攔截事件  
   intercepted = true;
}

從原始碼裡可以看出,ViewGrouponInterceptTouchEvent判斷是否去攔截事件的前提ACTION_DOW或者mFirstTouchTarget != null,關於mFirstTouchTarget,如果事件由ViewGroup的子View成功處理,mFirstTouchTarget會指向該子View不為空。

當面對ACTION_DOW事件時,ViewGroup總是會呼叫自己的onInterceptTouchEvent來詢問是否去攔截事件,因此若ViewGroup攔截了ACTION_DOWN事件,mFirstTouchTarget一定為空。當後續ACTION_MOVEACTION_UP事件到來時,ViewGrouponInterceptTouchEvent不會呼叫,直接攔截。那麼有什麼辦法可以修改這種預設機制呢?

我們還注意到原始碼中的標記位FLAG_DISALLOW_INTERCEPT,該標記位通過子View的getParent().requestDisallowInterceptTouchEvent方法來設定,作用是ViewGroup將無法攔截除ACTION_DOW以外的點選事件。這為我們後面如何處理滑動衝突提供思路。至於為什麼無法攔截ACTION_DOW可以由以下原始碼證明。

//處理初始的down事件  
if (actionMasked == MotionEvent.ACTION_DOWN) {  
// ACTION_DOWN到來時的重置操作 
//當app切換、 ANR或一些其他的touch狀態發生時,framework會丟棄或取消先前的touch狀態  
  cancelAndClearTouchTargets(ev);  
  resetTouchState();//該方法中會重置FLAG_DISALLOW_INTERCEPT標記位
} 

1. 5  View對事件的處理

ViewGrouponInterceptTouchEvent返回false,會首先遍歷所有的子元素,判斷子元素是否能夠接收點選事件(通過判斷子元素是否在播放動畫並且點選座標落在該子元素區域內)。若子元素具備接收事件的條件,那麼它的dispatchTouchEvent會被呼叫,若遍歷完所有的子元素均返回false,那麼只能ViewGroup自己去處理該事件。子元素的該方法返回true終止遍歷子元素

事件傳遞就來到了子View的“手裡”,處理過程如下。

public boolean dispatchTouchEvent(MotionEvent event) {  
boolean result = false;
//...
    if (onFilterTouchEventForSecurity(event)) {  
       //noinspection SimplifiableIfStatement  
       ListenerInfo li = mListenerInfo;  
       if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED  
                    && li.mOnTouchListener.onTouch(this, event)) {  
         return true; }  
  
       if (onTouchEvent(event)) {  
        return true; }  
}
    //…
    return result;  
}

View無法繼續向下傳遞事件,只能處理之。從原始碼第8行可以看出會執行ViewOnTouchListener.onTouch這個函式,若返回trueonTouchEvent便不會再被呼叫了。可見OnTouchListeneronTouchEvent優先順序更高

1. 6  ViewonTouchEvent

首先當View處於不可用狀態時的處理過程如下:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    // 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));
}

可以看出Viewenable屬性不影響onTouchEvent的返回值ViewonTouchEvent預設消耗事件並返回true,除非其CLICKABLELONG_CLICKABLE均為false。後者在View中預設為false,前者根據控制元件本身來決定。通過setClickablesetLongClickable可以改變View的這兩個屬性。setOnClickListenersetOnLongClickListener本質上也是通過setClickablesetLongClickable來改變View的這兩個屬性。

View對事件的具體處理如下:

public boolean onTouchEvent(MotionEvent event) {    
    //...  
    if (((viewFlags & CLICKABLE) == CLICKABLE ||    
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {    
        switch (event.getAction()) {    
            case MotionEvent.ACTION_UP:  
                ...  
                performClick();                  
                break;    
            case MotionEvent.ACTION_DOWN:  
                break;    
            case MotionEvent.ACTION_CANCEL:    
                break;    
            case MotionEvent.ACTION_MOVE:    
                break;    
        }
        // 
        return true;    
    }    
    return false;    
}  

從原始碼中可以看出,如果一個控制元件是clickablelongclickable的,那麼就會執行ACTION_UPACTION_DOWNcase裡面,並最終返回true。需要說明的是,如果在上一個case(比如:ACTION_UP)中返回了false,那麼下面所有的case(比如:ACTION_CANCELACTION_MOVE)都不會得到執行。

ACTION_UP發生時會呼叫performClick方法,原始碼如下所示:

public boolean performClick() {    
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);    
    if (mOnClickListener != null) {    
        playSoundEffect(SoundEffectConstants.CLICK);    
        mOnClickListener.onClick(this);    
        return true;    
    }    
    return false;    
}    

performClick的實現來看,如果View設定了OnClickListener,在performClick方法中會呼叫它的onClick方法

至此關於事件分發機制就介紹完畢了