事件分發與消費機制

參考郭霖博客:
http://blog.csdn.net/guolin_blog/article/details/9097463
http://blog.csdn.net/guolin_blog/article/details/9153747

標簽(空格分隔): Android


onTouch與onClick的關系,調用時機###

button.setOnClickListener(new OnClickListener() {  
    @Override  
    public void onClick(View v) {  
        Log.d("TAG", "onClick execute");  
    }  
});  
button.setOnTouchListener(new OnTouchListener() {  
    @Override  
    public boolean onTouch(View v, MotionEvent event) {  
        Log.d("TAG", "onTouch execute, action " + event.getAction());  
        return false;  
    }  
}); 

onTouch是優先于onClick執行的,并且onTouch執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(你還可能會有多次ACTION_MOVE的執行,如果你手抖了一下)。因此事件傳遞的順序是先經過onTouch,再傳遞到onClick。

【要注意的是onTouch與onTouchEvent都可以監控ACTION_DOWN、ACTION_MOVE、ACTION_UP等手勢,而且是持續監聽,即每個ACTION都會進入這兩個方法進行監聽】

結論:onTouch方法是有返回值的,這里我們返回的是false,那么onClick()還會執行,如果我們嘗試把onTouch方法里的返回值改成true,那么onClick()就不會執行

應用場景:為什么給ListView引入了一個滑動菜單的功能,ListView就不能滾動了?
滑動菜單的功能是通過給ListView注冊了一個touch事件來實現的。如果你在onTouch方法里處理完了滑動邏輯后返回true,那么ListView本身的滾動事件就被屏蔽了,自然也就無法滑動(原理同前面例子中按鈕不能點擊),因此解決辦法就是在onTouch方法里返回false。

滾動事件也像點擊事件一樣,跟onTouch的返回值有關???


【任何控件本身是沒有dispatchTouchEvent方法的,是從view類繼承的】
首先你需要知道一點,只要你觸摸到了任何一個控件,首先會去調用該控件所在布局的dispatchTouchEvent方法【該dispatchTouchEvent方法是繼承于ViewGroup】,然后在布局的dispatchTouchEvent方法中找到被點擊的相應控件,再去調用該控件的dispatchTouchEvent方法。所有view控件的dispatchTouchEvent方法都是繼承于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正是在setOnTouchListener方法里賦值的,也就是說只要我們給控件注冊了touch事件,mOnTouchListener就一定被賦值了。

//條件二:(mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點擊的控件是否是enable的,按鈕默認都是enable的

//條件三:mOnTouchListener.onTouch(this, event),其實也就是去回調控件注冊touch事件時的onTouch方法。

讓這三個條件全部成立,從而dispatchTouchEvent方法直接返回true
而且onClick的調用肯定是在onTouchEvent(event)方法中的,所以當在onTouch方法里返回了true【而且既然可以調用onTouch方法,那么就一定滿足該控件注冊了touch事件,而且是可以點擊的】,就會讓dispatchTouchEvent方法直接返回true,所以不會走return onTouchEvent(event),當然就不會調用到onClick方法了

1. onTouch和onTouchEvent有什么區別,又該如何使用?
從源碼中可以看出,這兩個方法都是在View的dispatchTouchEvent中調用的,onTouch優先于onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。
另外需要注意的是,onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點擊的控件必須是enable的。因此如果你有一個控件是非enable的,那么給它注冊onTouch事件將永遠得不到執行。對于這一類控件,如果我們想要監聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實現。

因為onTouchEvent()方法的代碼有點長,所以就不列出來了,在onTouchEvent()可以判斷ACTION_DOWN、ACTION_MOVE、ACTION_UP等手勢。還有里面的performClick()會調用到onClick()

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;  
}  

touch事件的ACTION層級傳遞###

我們都知道如果給一個控件注冊了touch事件,每次點擊它的時候都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這里需要注意,如果你在執行ACTION_DOWN的時候返回了false,后面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發后一個action。

這里要補充一句,即使在onTouch事件里面返回了false。所以就一定會進入到onTouchEvent方法中其中有一個if用于判斷是否可以點擊,如果可以點擊則返回一個true。是不是有一種被欺騙的感覺?明明在onTouch事件里返回了false,系統還是在onTouchEvent方法中幫你返回了true。就因為這個原因,才使得ACTION_UP可以得到執行。
所以將按鈕替換成ImageView,然后給它也注冊一個touch事件,并返回false。在ACTION_DOWN執行完后,后面的一系列action都不會得到執行了。因為ImageView和按鈕不同,它是默認不可點擊的,所以不能進入onTouchEvent方法中其中的那個用于判斷是否可以點擊的if,直接在onTouchEvent中返回false,所以最終的返回還是false,所以就導致后面其它的action都無法執行了。

應用場景:為什么圖片輪播器里的圖片使用Button而不用ImageView?
主要就是因為Button是可點擊的,而ImageView是不可點擊的。如果想要使用ImageView,可以有兩種改法。第一,在ImageView的onTouch方法里返回true,這樣可以保證ACTION_DOWN之后的其它action都能得到執行,才能實現圖片滾動的效果。第二,在布局文件里面給ImageView增加一個android:clickable="true"的屬性,這樣ImageView變成可點擊的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到執行的。

布局嵌套時的事件分發##

分發順序:
Acivity->Window->DecorView->ViewGroup->View

ViewGroup中有一個onInterceptTouchEvent方法

ViewGroup中有一個dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent ev) {  
    final int action = ev.getAction();  
    final float xf = ev.getX();  
    final float yf = ev.getY();  
    final float scrolledXFloat = xf + mScrollX;  
    final float scrolledYFloat = yf + mScrollY;  
    final Rect frame = mTempRect;  
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
            ev.setAction(MotionEvent.ACTION_DOWN);  
            final int scrolledXInt = (int) scrolledXFloat;  
            final int scrolledYInt = (int) scrolledYFloat;  
            final View[] children = mChildren;  
            final int count = mChildrenCount;  
            for (int i = count - 1; i >= 0; i--) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                        || child.getAnimation() != null) {  
                    child.getHitRect(frame);  
                    if (frame.contains(scrolledXInt, scrolledYInt)) {  
                        final float xc = scrolledXFloat - child.mLeft;  
                        final float yc = scrolledYFloat - child.mTop;  
                        ev.setLocation(xc, yc);  
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                        if (child.dispatchTouchEvent(ev))  {  
                            mMotionTarget = child;  
                            return true;  
                        }  
                    }  
                }  
            }  
        }  
    }  
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
            (action == MotionEvent.ACTION_CANCEL);  
    if (isUpOrCancel) {  
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
    }  
    final View target = mMotionTarget;  
    if (target == null) {  
        ev.setLocation(xf, yf);  
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        }  
        return super.dispatchTouchEvent(ev);  
    }  
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
        final float xc = scrolledXFloat - (float) target.mLeft;  
        final float yc = scrolledYFloat - (float) target.mTop;  
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        ev.setLocation(xc, yc);  
        if (!target.dispatchTouchEvent(ev)) {  
        }  
        mMotionTarget = null;  
        return true;  
    }  
    if (isUpOrCancel) {  
        mMotionTarget = null;  
    }  
    final float xc = scrolledXFloat - (float) target.mLeft;  
    final float yc = scrolledYFloat - (float) target.mTop;  
    ev.setLocation(xc, yc);  
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        mMotionTarget = null;  
    }  
    return target.dispatchTouchEvent(ev);  
}  
//在ViewGroup中的dispatchTouchEvent方法,第13行可以看到一個條件判斷,如果disallowIntercept和!onInterceptTouchEvent(ev)兩者有一個為true,就會進入到這個條件判斷中。
//disallowIntercept是指是否禁用掉事件攔截的功能,默認是false,也可以通過調用requestDisallowInterceptTouchEvent方法對這個值進行修改。那么當第一個值為false的時候就會完全依賴第二個值來決定是否可以進入到條件判斷的內部,第二個值是什么呢?竟然就是對onInterceptTouchEvent方法的返回值取反!也就是說如果我們在onInterceptTouchEvent方法中返回false,就會讓第二個值為true,從而進入到條件判斷的內部,如果我們在onInterceptTouchEvent方法中返回true,就會讓第二個值為false,從而跳出了這個條件判斷。
//而子view的時事件分發就是在這個條件判斷中,這個條件判斷的內部是怎么實現的?在第19行通過一個for循環,遍歷了當前ViewGroup下的所有子View,然后在第24行判斷當前遍歷的View是不是正在點擊的View,如果是的話就會進入到該條件判斷的內部,然后在第29行調用了該View的dispatchTouchEvent,之后的流程就跟不嵌套時的子view事件分發一樣。
//所以說當跳出了這個條件判斷后,即在onInterceptTouchEvent方法中返回true,就會讓第二個值!onInterceptTouchEvent為false子View的事件分發將得不到執行,所以子View就會被屏蔽。
//而當子View沒有被屏蔽時,即子View的dispatchTouchEvent得到執行,而子View是可點擊的,子View的dispatchTouchEvent一定返回True,就會導致ViewGroup的dispatchTouchEvent第29行的條件判斷成立,于是在第31行給ViewGroup的dispatchTouchEvent方法直接返回了true。這樣就導致后面的代碼無法執行到了,所以導致后面的代碼中ViewGroup的touch事件就沒有辦法執行了
//

而當我們點擊的只是ViewGroup的空白區域而不是子View時,首先也是會進入ViewGroup的dispatchTouchEvent,就算ViewGroup的onInterceptTouchEvent返回的是true,令ViewGroup的dispatchTouchEvent中的13行的條件判斷的!onInterceptTouchEvent變成false,不進入這個條件判斷里面,即攔截子View的事件。但是這時點擊的不是子View控件,所以不會在dispatchTouchEvent的31行返回true,令方法立馬結束,不會使ViewGroup的touch事件沒有執行


此處輸入圖片的描述
此處輸入圖片的描述

***結論:

  • 要想攔截子View的事件,就重寫ViewGroup的onInterceptTouchEvent方法,使其返回true。***默認是false不攔截子View事件

  • 而且子View沒有onInterceptTouchEvent

  • 如果ViewGroup的onInterceptTouchEvent方法,使其返回true即攔截子View事件,那么在攔截了同一序列事件中的ACTION_DOWN后,onInterceptTouchEvent不會在被調用,并且繼續攔截剩下的ACTION_MOVE,ACTION_UP

  • 另外注意下如果子View設置了FLAG_DISALLOW_INTERCEPT這個標記位,具體看一下《開發藝術探索》,可以讓ViewGroup只能攔截ACTION_DOWN,不能攔截ACTION_MOVE,ACTION_UP

  • 攔截方式有外部攔截法于內部攔截法,一般推薦使用較為簡單的外部攔截法

  • 所有函數的返回值都是顯而易見的,True:攔截、消費處理。False:不攔截、不消費處理。只有onTouch()的返回值是怪怪的,返回的是false的時候onClick()才會執行?。。?/p>


事件消費傳遞###

如果子View的onTouchEvent返回的是false,那么就會交給他的父容器onTouchEvent處理,如果所有的元素都不處理這個事件的話,就會交給Activity的onTouchEvent處理。

只要找到了事件處理者的話,只要當前ACTION會有去找處理者的過程,而之后的每個ACTION會直接被之前找到的處理者消費掉,不會有那個找處理者的那個查找過程。

【那onTouch的返回值有對消費傳遞有影響嗎?】
  答案是有影響的,即使onTouch的返回值是false,就會調用onTouchEvent,所以事件傳遞消費還是要看onTouchEvent;如果onTouch的返回值是true的話,事件就會被處理掉,而且onTouchEvent不會被調用。

【在onTouchEvent的返回值指的是每個case判斷中對每個ACTION的處理后的返回值,還是指onTouchEvent最底下的返回值?】
一般指的是每個case判斷中對每個ACTION的處理后的返回值,但是onTouchEvent最底下的返回值是必須要寫的,因為函數必須要有返回值。

View的onTouchEvent默認返回的是true,即處理消費事件

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容