Android 進階(三)--Android事件分發機制

1.前言

Android事件分發機制在Android開發者必須要了解的知識,並且是解決View的滑動衝突的基礎。

2.事件分發的基礎

要想了解事件分發的機制,我們必須要知道事件分發的基礎,即,事件分發的對象和流程。

2.1 事件分發的定義

將點擊事件(MotionEvent)向某個View進行傳遞並最終得到處理。

2.2 事件分發的對象

當用戶觸摸屏幕時(View或ViewGroup派生的控件),將產生點擊事件(Touch事件)。這個事件就是分發的對象。

主要發生的Touch事件有如下四種:

  1. MotionEvent.ACTION_DOWN:按下View(所有事件的開始)

  2. MotionEvent.ACTION_MOVE:滑動View

  3. MotionEvent.ACTION_CANCEL:非人爲原因結束本次事件

  4. MotionEvent.ACTION_UP:擡起View(與DOWN對應)

2.3 事件的傳遞流程

一個點擊事件產生後,傳遞順序是:Activity(Window) -> ViewGroup -> View

1.View是所有UI組件的基類

一般Button、ImageView、TextView等控件都是繼承父類View

2.ViewGroup是容納UI組件的容器,即一組View的集合(包含很多子View和子VewGroup)

  1. 其本身也是從View派生的,即ViewGroup是View的子類
  2. 是Android所有佈局的父類或間接父類:項目用到的佈局(LinearLayout、RelativeLayout等),都繼承自ViewGroup,即屬於ViewGroup子類。
  3. 與普通View的區別:ViewGroup實際上也是一個View,只不過比起View,它多了可以包含子View和定義佈局參數的功能。

3. 事件分發的主要方法介紹

事件分發主要涉及到三個主要方法:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()。
下面用張圖來說明這三個方法的含義及作用。

事件分發

這三個方法的聯繫可以用下面的僞代碼來表示:

// 點擊事件產生後,會直接調用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {
    //代表是否消耗事件
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
    //如果onInterceptTouchEvent()返回true則代表當前View攔截了點擊事件
    //則該點擊事件則會交給當前View進行處理
    //即調用onTouchEvent ()方法去處理點擊事件
      consume = onTouchEvent (ev) ;
    } else {
      //如果onInterceptTouchEvent()返回false則代表當前View不攔截點擊事件
      //則該點擊事件則會繼續傳遞給它的子元素
      //子元素的dispatchTouchEvent()就會被調用,重複上述過程
      //直到點擊事件被最終處理爲止
      consume = child.dispatchTouchEvent (ev) ;
    }
    return consume;
   }

上面的僞代碼很好的總結了一個事件的分發機制。
這裏總結如下:

  1. 點擊事件產生後,首先傳到的是ViewGroup,然後ViewGroup的dispatchTouchEvent()方法被調用。

  2. 如果onInterceptTouchEvent()返回true則代表當前View攔截了點擊事件,時候調用ViewGroup的onTouchEvent()方法進行自我消耗,不往子View進行傳遞,如果onInterceptTouchEvent()返回false則往子View進行傳遞。

  3. 第二點onInterceptTouchEvent()返回false,事件往子View傳遞,這個時候子View的dispatchTouchEvent()方法被調用。如果子View是一個ViewGroup,重複上面的步驟。直到事件被最終處理。

4.從源碼分析Android事件優先級

View的點擊事件不但和ViewGroup的onInterceptTouchEvent()的返回值有關,還和事件的優先級有關,優先級高的事件能夠屏蔽優先級低的事件,讓優先級低的事件無法接收事件。

4.1 demo引入

下面我們以一個demo來引入事件優先級:

    //點擊事件
  button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.i(TAG,"onclick");
            }
        });


    //觸摸事件
        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                Log.i(TAG,"onTouch"+motionEvent.getAction());
                return false;//重點,返回值。
            }
        });

這是一個簡單的demo,我們設置了一個button的點擊事件和觸摸事件,然後我們看下打印的log

11-28 00:07:24.336 4876-4876/com.wind.androiddispatchevent I/MainActivity: onTouch0
11-28 00:07:26.984 4876-4876/com.wind.androiddispatchevent I/MainActivity: onTouch1
11-28 00:07:26.987 4876-4876/com.wind.androiddispatchevent I/MainActivity: onclick

我們可以看到,先打印的是觸摸事件,然後打印的是點擊事件。

motionEvent.getAction()返回的是一個整型, 0表示ACTION_DOWN,1表示ACTION_UP。

到這裏,我們是否可以認爲觸摸事件的優先級要高於點擊事件呢?

顯然還不夠充分,我們修改下代碼,把onTouch事件的返回值改爲true。

我們再來看看打印的log:

11-28 00:22:35.219 16129-16129/com.wind.androiddispatchevent I/MainActivity: onTouch0
11-28 00:22:35.279 16129-16129/com.wind.androiddispatchevent I/MainActivity: onTouch1

我們可以看出來,onClick方法不再執行了!當onTouch()返回爲真,我們就認爲onTouch自己消耗了這個事件,所以不再往下傳遞。這下我們就可以認爲onTouch優先級高於onClick。

4.2 源碼分析

首先我們需要知道一點,只要你觸摸到了任何一個控件,就一定會調用該控件的dispatchTouchEvent方法。那當我們去點擊按鈕的時候,就會去調用Button類裏的dispatchTouchEvent方法,可是你會發現Button類裏並沒有這個方法,那麼就到它的父類TextView裏去找一找,你會發現TextView裏也沒有這個方法,那沒辦法了,只好繼續在TextView的父類View裏找一找,這個時候你終於在View裏找到了這個方法。

然後我們來看一下View中dispatchTouchEvent方法的源碼:

現在源碼已經有了非常大的改變,我也沒有具體看。但是個人覺得以前的版本已經把精髓給實現了,後面多是添加了一些不常見的或者是提高用戶體驗或者是安全性。所以接下來分析基於以前的源碼。

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);   }

這個方法非常的簡潔,只有短短几行代碼!我們可以看到,在這個方法內,首先是進行了一個判斷,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都爲真,就返回true,否則就去執行onTouchEvent(event)方法並返回。

下面我們詳細看看這三個條件:

第一個條件:mOnTouchListener!= null

//mOnTouchListener是在View類下setOnTouchListener方法裏賦值的
public void setOnTouchListener(OnTouchListener l) { 

//即只要我們給控件註冊了Touch事件,mOnTouchListener就一定被賦值(不爲空)
    mOnTouchListener = l;  
}  

第二個條件:(mViewFlags & ENABLED_MASK) == ENABLED

  1. 該條件是判斷當前點擊的控件是否enable
  2. 由於很多View默認是enable的,因此該條件恆定爲true

第三個條件:mOnTouchListener.onTouch(this, event)

回調控件註冊Touch事件時的onTouch方法

//手動調用設置
button.setOnTouchListener(new OnTouchListener() {  

    @Override  
    public boolean onTouch(View v, MotionEvent event) {  

        return false;  
    }  
});

如果在onTouch方法返回true,就會讓上述三個條件全部成立,從而整個方法直接返回true。
如果在onTouch方法裏返回false,就會去執行onTouchEvent(event)方法。

這下我們就清楚了爲什麼onTouch返回true,dispatchTouchEvent()方法就會直接返回true,下面我們再來看看onTouch返回false的情況,即dispatchTouchEvent()方法將會 return onTouchEvent(event),我們再來看看onTouchEvent(event)方法。

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  
    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));  
    }  
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }  
     //如果該控件是可以點擊的就會進入到下兩行的switch判斷中去;

    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
    //如果當前的事件是擡起手指,則會進入到MotionEvent.ACTION_UP這個case當中。

        switch (event.getAction()) {  
            case MotionEvent.ACTION_UP:  
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
               // 在經過種種判斷之後,會執行到關注點1的performClick()方法。
               //請往下看關注點1
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                    // take focus if we don't have it already and we should in  
                    // touch mode.  
                    boolean focusTaken = false;  
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                        focusTaken = requestFocus();  
                    }  
                    if (!mHasPerformedLongPress) {  
                        // 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)) {  
            //關注點1
            //請往下看performClick()的源碼分析
                                performClick();  
                            }  
                        }  
                    }  
                    if (mUnsetPressedState == null) {  
                        mUnsetPressedState = new UnsetPressedState();  
                    }  
                    if (prepressed) {  
                        mPrivateFlags |= PRESSED;  
                        refreshDrawableState();  
                        postDelayed(mUnsetPressedState,  
                                ViewConfiguration.getPressedStateDuration());  
                    } else if (!post(mUnsetPressedState)) {  
                        // If the post failed, unpress right now  
                        mUnsetPressedState.run();  
                    }  
                    removeTapCallback();  
                }  
                break;  
            case MotionEvent.ACTION_DOWN:  
                if (mPendingCheckForTap == null) {  
                    mPendingCheckForTap = new CheckForTap();  
                }  
                mPrivateFlags |= PREPRESSED;  
                mHasPerformedLongPress = false;  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  
            case MotionEvent.ACTION_CANCEL:  
                mPrivateFlags &= ~PRESSED;  
                refreshDrawableState();  
                removeTapCallback();  
                break;  
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  
                // Be lenient about moving outside of buttons  
                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    // Outside button  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        // Remove any future long press/tap checks  
                        removeLongPressCallback();  
                        // Need to switch from pressed to not pressed  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  
//如果該控件是可以點擊的,就一定會返回true
        return true;  
    }  
//如果該控件是可以點擊的,就一定會返回false
    return false;  
}  

關注點1:
performClick()的源碼分析

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  

    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}  

只要mOnClickListener不爲null,就會去調用onClick方法;
那麼,mOnClickListener又是在哪裏賦值的呢?請繼續看:

public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
} 

當我們通過調用setOnClickListener方法來給控件註冊一個點擊事件時,就會給mOnClickListener賦值(不爲空),即會回調onClick()。

到這裏我們就能完全明白onTouch()方法爲什麼優先級高於onClick()了。

4.3 onTouch和onTouchEvent有什麼區別,又該如何使用?

從源碼中可以看出,這兩個方法都是在View的dispatchTouchEvent中調用的,onTouch優先於onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。

另外需要注意的是,onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能爲空,第二當前點擊的控件必須是enable的。因此如果你有一個控件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行。對於這一類控件,如果我們想要監聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實現。

4.4 總結

  1. onTouch 優先級高於onClick。

  2. 如果onTouch()方法返回TRUE,dispatchTouchEvent()方法將會直接返回true,而不會再走onTouchEvent(event)方法了。

  3. onClick方法位於onTouchEvent()方法中,所以,如果onTouch()方法返回TRUE,onClick()將不會執行。

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