Android觸摸事件分發(fā)機制

用戶在操作的時候,不可避免地就會觸發(fā)觸摸事件。Android把觸摸過程分成很多個動作(Action),而開發(fā)中最常見也最主要考慮的觸摸過程就是:從ACTION_DOWN(觸摸落下)開始、到ACTION_UP(觸摸彈起)/ACTION_CANCEL(觸摸取消,譬如在按下控件,將控件移動到外層控件的時候,就會觸發(fā),而不是UP)結束,在這之間的是ACTION_MOVE(觸摸移動,非必然存在)。

同時,我們在界面上觸發(fā)觸摸事件的時候,同樣不可避免地會涉及到這三部分:Activity(可視界面當然還有Fragment,Dialog這些,但它們都依附著Activity),ViewGroup(通常來說做布局容器的LinearLayout、RelativeLayout等都算是),View(Button、ImageView等等)。一個簡單的情況就是我們在點擊界面(Activity)上在布局(ViewGroup)中的Button(View)的時候,這必然觸發(fā)了觸摸事件,但這具體是怎樣的一個過程?我們可以在這些過程中做些什么?這就涉及到了Android觸摸事件分發(fā)機制,先看一張簡略的分發(fā)流程圖(圖片來源于從Android源碼的角度理解應用開發(fā)(1)-Touch機制,感謝,侵刪):

簡單的Touch事件傳遞過程

從流程圖可以得知,Touch事件的分發(fā)情況是這樣的,Activity將事件分發(fā)到ViewGroup中,而ViewGroup層層分發(fā)直到找到需要處理Touch事件的子元素(可能是View也可能是ViewGroup),將事件傳遞下去。這里也可以提前告知一個邏輯,就算傳遞到了,但如果子元素不能處理觸摸事件,會將事件交回上一級處理,最后可以到Activity去處理,總之觸摸事件最終會被消費掉。
先讓我們來了解一下Touch事件分發(fā)和處理的三個重要方法。

1.public boolean dispatchTouchEvent(MotionEvent ev)

MotionEvent-手勢事件,它里面就包含了上面說的Action。這個方法從字面上的意思都很好理解,調(diào)度觸摸事件,這個方法是Activity、ViewGroup、View都有的,Touch事件都從它開始,也就是說Touch事件的分發(fā)和處理過程中,dispatchTouchEvent()是第一個被調(diào)用的方法。

2.public boolean onInterceptTouchEvent(MotionEvent ev)

也是從字面意思上就很好理解了,攔截觸摸事件。如果返回值為true,就攔截當前事件,不分發(fā)給子元素。很明顯這個方法View是沒有的,可以說三者中只有ViewGroup才有,因為Activity肯定要把事件分發(fā)下去(后面有說到),而View下面是沒有子元素的,要么處理觸摸事件要么交回ViewGroup處理。

3.public boolean onTouchEvent(MotionEvent ev)

這個方法表示對事件進行處理,在dispatchTouchEvent方法內(nèi)部調(diào)用,如果返回true表示消耗當前事件,如果返回false表示不消耗當前事件。
為了更好地理解整個流程,我們從View——>ViewGroup——>Activity的順序展開,讓我們先來看看是怎么處理Touch事件的,再看具體是怎么分發(fā)的。就以上面說到的按鈕點擊的情況來說一說。

View

上面的按鈕點擊中,View就是Button,剛剛說到整個過程最先被調(diào)用的就是dispatchTouchEvent(),Button的dispatchTouchEvent()是調(diào)用View的,我們來看一下這個方法:

View的dispatchTouchEvent()實現(xiàn)

可以看到,if語句的判斷條件有三個部分,只要有一部分條件是false,那么就會執(zhí)行onTouchEvent()。

1.判斷是否有注冊觸摸監(jiān)聽事件;

2.判斷控件是否可用(ENABLE),一般控件是默認可用的,除非通過setEnable(false)禁用控件,不然這部分條件一般為true;

3.監(jiān)聽事件里面的onTouch()的返回值。

所以很簡單的,控件監(jiān)聽觸摸事件讓onTouch()返回true,就不會再執(zhí)行下去了,當然我們是要看下去的,看一下onTouchEvent()里面實現(xiàn)了什么?

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;
            }
        }
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    ...
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            ...
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;
                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;
                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }
            return true;
        }
        return false;
    }

這里我們可以簡單分析一下,這里面主要有三塊if語句:

1.如果控件是禁用(DISABLED)的,控件可以點擊(CLICKABLE)或長按點擊(LONG_CLICKABLE),返回true,消費事件但不做任何操作,如果控件不可點擊,onTouchEvent()返回false;

2.如果存在觸摸委托對象(TouchDelegate),交由其onTouchEvent()處理;

3.首先可以看到,只要控件是不可點擊的,不滿足if語句條件,直接返回false;反之,再處理不同的ACTION,都會返回true。

有意思的是,繼續(xù)深入查看源碼可以發(fā)現(xiàn),控件的onLongClick事件是在ACTION_DOWN里面觸發(fā)的(postDelayed())、onClick事件是在ACTION_UP里面觸發(fā)的(performClick()),這里不展開說。

上面說了那么多,好像和分發(fā)沒什么關系???別急,我們可以知道View的Touch事件其實都是為了知道View的dispatchTouchEvent()的返回值是什么,而這個值與分發(fā)事件大有關聯(lián),請看ViewGroup。

ViewGroup

從上面簡略的流程圖可以看出,觸摸事件由ViewGroup傳遞給View,很顯然,這就是一個事件分發(fā)過程,那么,這個過程是怎么做的呢?
ViewGroup首先調(diào)用dispatchTouchEvent(),讓我們來看一看具體的實現(xiàn):

public boolean dispatchTouchEvent(MotionEvent ev) {  
    ...
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
           ...
            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;  
                        }  
                    }  
                }  
            }  
        }  
    }  
    ...
    if (target == null) {  
        ...
        return super.dispatchTouchEvent(ev);  
    }  
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
        ... 
        return true;  
    }  
    if (isUpOrCancel) {  
        mMotionTarget = null;  
    }  
    ...
    return target.dispatchTouchEvent(ev);  
}  

我們可以看到,首先對ACTION_DOWN做了一大堆處理,來看一下具體在干嘛。

首先清除手勢目標;
第二個是重點:
disallowIntercept-禁止攔截,這個變量是一個Boolean值,默認值是false,意味著一般情況下是不禁止攔截功能的;
onIntercepTouchEvent()-這個方法在上面也提到過,攔截事件分發(fā)下去,將觸摸事件交給ViewGroup自己處理。

很好,那么

if (disallowIntercept || !onInterceptTouchEvent(ev))

這個條件語句的意思是只要不攔截,就進入條件判斷內(nèi)部,它的邏輯運算符是||,只要disallIntercept為true或者!onInterceptTouchEvent(ev)為true,也就是onInterceptTouchEvent(ev)為false就好了。

第一個條件,disallIntercept默認為false,但我們可以通過requestDisallowInterceptTouchEvent(Boolean)使得它的值為true;
第二個條件,看一下onInterceptTouchEvent(ev)的源碼:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

看!它的返回值是默認false的!也就是!onInterceptTouchEvent(ev)為true,那么也就是默認情況下,并不攔截事件,進入條件判斷內(nèi)部。

這一大塊代碼具體做了什么?
1.獲得ViewGroup下的子View
2.for循環(huán)判斷,手指落下的范圍在哪個View之中,接著!

if (child.dispatchTouchEvent(ev))  {  
      mMotionTarget = child;  
      return true;  
}  

上面說了那么多View里面獲取dispatchTouchEvent()的返回值,在這里用上了,確定分發(fā)目標為dispatchTouchEvent()返回值為true的子View!同時,ViewGroup的dispatchTouchEvent()的返回值也是true,那么ACTION_DOWN就結束了,再次進來就是其它ACTION的執(zhí)行邏輯了。

所以如果,View的dispatchTouchEvent()返回值為false,target為空,那么可以看到往下的邏輯里面,將自身作為一個View設為target:

 if (target == null) {  
        ···
        return super.dispatchTouchEvent(ev);  
    }  

也就是View的onTouch()返回值為true,View禁用,View的onTouchEvent()返回false等種種情況都會導致把事件重新交回給ViewGroup,然后ViewGroup執(zhí)行super.dispatchTouchEvent(ev),ViewGroup的超類是View,又回到了前面說的View的內(nèi)容了。

總結(來自于從Android源碼的角度理解應用開發(fā)(1)-Touch機制,很精準簡練的總結,感謝,侵刪):
1.在Down并且不攔截的時候會多出一個尋找Target的過程,在這個過程中遍歷子View,如果子View的dispatchTouch為true,則這個子View就是當前ViewGroup的Target。找Target是處理Down事件時候特有的,其他事件不會觸發(fā)找Target;

2.如果沒有Target,則發(fā)送把自己當做一個View去處理這個事件(super.dispatchTouch());

3.如果有Target并且攔截,則發(fā)送Cancel給子View ;

4.如果有Target并且不攔截,則調(diào)用Target的dispatchTouch;

5.可以利用requestDisallowInterceptTouchEvent(boolean)來強制viewparent不攔截事件。但是作用域限于一個Touch的過程(Down->Up/Cancel)。

接著就剩下最后一塊了,我們來看一下Activity又是怎么樣把事件分發(fā)給ViewGroup的。

Activity

還是先看dispatchTouchEvent():

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

代碼很簡單,但是其實經(jīng)過一系列的步驟之后,傳遞過程是這樣的:
Activity->Window->DecorView->ViewGroup,同理,如果最后Touch事件沒有被消費,也會交回由Activity的onTouchEvent()里面去處理。

補充: ACTION的傳遞過程:
對于在onTouchEvent消費事件的情況:
在哪個View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件從上往下傳到這個View后就不再往下傳遞了,而直接傳給自己的onTouchEvent 并結束本次事件傳遞過程。

對于ACTION_MOVE、ACTION_UP總結:
ACTION_DOWN事件在哪個控件消費了(return true), 那么ACTION_MOVE和ACTION_UP就會從上往下(通過dispatchTouchEvent)做事件分發(fā)往下傳,就只會傳到這個控件,不會繼續(xù)往下傳,如果ACTION_DOWN事件是在dispatchTouchEvent消費,那么事件到此為止停止傳遞,如果ACTION_DOWN事件是在onTouchEvent消費的,那么會把ACTION_MOVE或ACTION_UP事件傳給該控件的onTouchEvent處理并結束傳遞。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容