Android學(xué)習(xí)筆記12 事件分發(fā)機(jī)制完全解析

事件分發(fā)機(jī)制,是Android提供的一套完善的對(duì)觸摸事件進(jìn)行處理的機(jī)制,熟悉整個(gè)事件分發(fā)流程很有必要,因?yàn)樗彩茿ndroid中常見的滑動(dòng)沖突問題解決的理論基礎(chǔ)。這幾天閱讀了《Android開發(fā)藝術(shù)探索》等書籍,總結(jié)如下。

一、引入
二、事件分發(fā)機(jī)制
   1.概述
   2.詳細(xì)
三、源碼解析
   1.ViewGroup事件分發(fā)
   2.View事件分發(fā)
四、滑動(dòng)沖突解決
五、總結(jié)

一、引入

在介紹Android事件分發(fā)機(jī)制之前,我們先看生活中的一個(gè)例子。公司里有三個(gè)角色,老板,項(xiàng)目經(jīng)理,程序員。有一天老板接到一個(gè)任務(wù),他將任務(wù)分配給項(xiàng)目經(jīng)理完成,項(xiàng)目經(jīng)理又把任務(wù)分給程序員。程序員完成任務(wù)后,告訴項(xiàng)目經(jīng)理任務(wù)完成了,項(xiàng)目經(jīng)理再向老板報(bào)告任務(wù)完成了。從老板接到任務(wù),到老板最終去交付任務(wù),這是個(gè)完整的過程。

在這個(gè)過程中,可能會(huì)有其它情況。假如在一開始老板接到任務(wù)時(shí),決定自己完成,不需要把任務(wù)往下分配,那么老板就自己做,項(xiàng)目經(jīng)理和程序員就沒事。同樣,如果項(xiàng)目經(jīng)理決定自己去做,那么就沒有程序員的事。上面的這個(gè)例子其實(shí)就是任務(wù)在老板、項(xiàng)目經(jīng)理和程序員這三個(gè)角色間的傳遞過程,Android中屏幕上的觸摸事件就相當(dāng)于這個(gè)任務(wù),事件分發(fā)就類似于這個(gè)傳遞過程。

二、事件分發(fā)機(jī)制

我們知道,Android的界面可能是由多個(gè)視圖層層嵌套構(gòu)成,一個(gè)ViewGroup視圖組合中可以包含其它的ViewGroup以及View,當(dāng)一個(gè)觸摸事件發(fā)生時(shí),系統(tǒng)需要把這個(gè)事件傳遞給一個(gè)具體的View,由它來完成處理。從事件發(fā)生,到傳遞給具體的View去完成,這個(gè)傳遞的過程就是View的事件分發(fā)。

概述

在事件分發(fā)機(jī)制中,涉及到的幾個(gè)關(guān)鍵部分分別是:TouchEvent(觸摸事件)、ViewGroup(視圖組合)、View(視圖)。下面先對(duì)這幾個(gè)部分做個(gè)介紹。

  • TouchEvent(觸摸事件)

觸摸事件就是觸摸屏幕產(chǎn)生的動(dòng)作事件,比如常見的手指按下,移動(dòng),抬起等等,Android為我們提供了一個(gè)專門的MotionEvent類,它包含了發(fā)生的動(dòng)作事件以及相關(guān)坐標(biāo)信息,利用MotionEvent,我們可以處理很多與動(dòng)作相關(guān)的工作。

  • View

我們經(jīng)常提到View事件分發(fā)機(jī)制,其實(shí)這里指的是View以及ViewGroup,我們知道View是Android中所有控件的基類,而ViewGroup翻譯為視圖組合,它是繼承自View的,可以包含子控件。我們?cè)诮酉聛淼挠懻撝?,?huì)把ViewGroup和View分開討論。

詳解

上面介紹了一些事件分發(fā)的基本概念,下面對(duì)分發(fā)流程有個(gè)總體的把握。Android中事件分發(fā)機(jī)制主要涉及到三個(gè)重要方法,如下:

  • dispatchTouchEvent ( MotionEvent event ) 事件分發(fā)
  • onInterceptTouchEvent 決定是否攔截事件
  • onTouchEvent 處理事件

上面三個(gè)方法之間的關(guān)系大概如下,當(dāng)事件傳遞到某個(gè)View時(shí),先執(zhí)行dispatchTouchEvent方法進(jìn)行事件分發(fā),在這個(gè)方法內(nèi)會(huì)調(diào)用方法onInterceptTouchEvent方法來決定是否攔截,如果返回true表示攔截,則調(diào)用onTouchEvent進(jìn)行事件處理,否則繼續(xù)往下傳遞,執(zhí)行子View的dispatchTouchEvent方法。

需要注意一點(diǎn),View沒有onInterceptTouchEvent方法,一旦有事件傳遞給它,那么它的onTouchEvent方法就會(huì)被調(diào)用。ViewGroup默認(rèn)不攔截任何事件,因?yàn)閺脑创a中可以看到ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false.

我們知道,四大組件中,Activity通常提供界面用于交互,我們會(huì)通過setContentView來設(shè)置界面布局,一般如果我們不希望布局頂部出現(xiàn)一個(gè)標(biāo)題欄,我們可能會(huì)調(diào)用requestWindowFeature(Window.FEATURE_NO_TITLE);方法,這里我們簡(jiǎn)單了解一下Android的界面架構(gòu)。

界面上一個(gè)點(diǎn)擊事件發(fā)生時(shí),它最先被傳遞的是給當(dāng)前的Activity,由Activity的dispatchTouchEvent來進(jìn)行事件分發(fā),而Activity內(nèi)部其實(shí)是包含一個(gè)Window的,這個(gè)抽象Window的實(shí)現(xiàn)是PhoneWindow,Activity把事件傳遞給PhoneWindow,PhoneWindow里又包含DecorView,PhoneWindow繼續(xù)把事件傳遞給DecorView,DecorWindow里包含有我們?cè)O(shè)置的布局,DecorView繼承自FrameLayout,事件最終傳遞給我們?cè)O(shè)置的布局,一般來說設(shè)置的布局是一個(gè)ViewGroup。所以,觸摸事件最后就是在ViewGroup中的分發(fā)過程。

三、源碼解析

前面我們已經(jīng)提到,事件分發(fā)機(jī)制其實(shí)是觸摸事件在ViewGroup和View兩種情況下的分發(fā)過程,下面我們結(jié)合源碼來分析,因?yàn)閂iew的過程相對(duì)來說較為簡(jiǎn)單,我們先看ViewGroup事件分發(fā)。

ViewGroup事件分發(fā)

ViewGroup事件分發(fā)過程簡(jiǎn)述主要如下,事件到達(dá)ViewGroup后會(huì)調(diào)用方法dispatchTouchEvent,在其中會(huì)調(diào)用onInterceptTouchEvent進(jìn)行判斷是否攔截,如果返回true表示攔截則事件由ViewGroup處理,如果返回false不攔截,則事件會(huì)傳遞給子View,子View的dispatchTouchEvent會(huì)被調(diào)用。默認(rèn)情況下,onInterceptTouchEvent返回false.

下面我們看下源碼。

1、首先是dispatchTouchEvent方法里判斷是否攔截。

final boolean intercepted;

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {

    //默認(rèn)是false 允許攔截
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    if (!disallowIntercept) {
        
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); 

    } else {
        intercepted = false;
    }
}else {
    intercepted = true;
}

這里可以看到,ViewGroup會(huì)在兩種情況下進(jìn)行是否攔截的判斷,第一種是發(fā)生ACTION_DOWN事件,第二種是mFirstTouchTarget != null。第二種情況是指,ViewGroup是否不攔截事件并把事件交由子View處理,如果是,那么mFirstTouchTarget != null就成立。

進(jìn)行判斷時(shí),會(huì)看變量disallowIntercept的值,這個(gè)值默認(rèn)是false不允許攔截,所以!disallowIntercept為true,然后調(diào)用onInterceptTouchEvent為false,即不攔截。有種情況,如果ACTION_DOWN判斷時(shí)被ViewGroup攔截,那么mFirstTouchTarget!=null就不成立,那么同一事件序列中的剩余事件ACTION_MOVE或者ACTION_UP來臨時(shí),不進(jìn)行判斷,直接攔截。

這里有兩條結(jié)論,某個(gè)View一旦決定攔截一個(gè)事件后,那么系統(tǒng)會(huì)把同一個(gè)事件序列的其它方法都交給這個(gè)View處理。某個(gè)View如果不消耗ACTION_DOWN事件交給了子View處理,那么同一個(gè)事件序列的其它方法都不會(huì)交給它處理。

2、當(dāng)ViewGroup不攔截事件,事件分發(fā)給子View處理。

                        
//子View
final View[] children = mChildren;
//循環(huán)遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
                            
     ... ...

     //如果子View接收不到事件 或者 不在播動(dòng)畫 就不分發(fā)
     if (!canViewReceivePointerEvents(child)
           || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
     }
                            
     //分發(fā)事件給子View
     newTouchTarget = getTouchTarget(child);
     if (newTouchTarget != null) {
                                
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
     }

     resetCancelNextUpFlag(child);
     //調(diào)用子元素的dispatchTouchEvent
     if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
        // childIndex points into presorted list, find original index
           for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
           }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
     }
                         
     ev.setTargetAccessibilityFocus(false);
}

可以看到大概流程如下,循環(huán)遍歷子View,判斷子元素能否接收到點(diǎn)擊事件。能否接收到事件主要由兩點(diǎn)衡量,一是是否在播放動(dòng)畫,二是點(diǎn)擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)。如果子元素滿足條件,則事件傳遞給子View處理。dispatchTransformedTouchEvent方法里調(diào)用了子View的dispatchTouchEvent方法。

如果子View的dispatchTouchEvent返回true,那么終止子元素的遍歷,如果返回false,則繼續(xù)分發(fā)給下個(gè)子元素。如果遍歷所有的子元素后事件都沒處理,那么ViewGroup就自己處理事件。


**綜上,觸摸事件傳遞到ViewGroup時(shí),會(huì)執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā),如果事件是Down類型(或者同一事件序列沒被攔截已經(jīng)交由子元素處理),那么就調(diào)用方法onInterceptTouchEvent進(jìn)行攔截判斷,默認(rèn)情況下不會(huì)攔截事件。ViewGroup不攔截的話,那么就會(huì)遍歷它的子View,判斷能否接收到事件,如果接收到那么就調(diào)用子View的dispatchTouchEvent方法繼續(xù)進(jìn)行分發(fā)。如果遍歷子View后都沒處理事件,那么ViewGroup自己處理事件。
**


View事件分發(fā)

View的事件分發(fā)比ViewGroup簡(jiǎn)單,因?yàn)閂iew不包含子View,所以它只能自己處理事件。

下面是它的dispatchTouchEvent方法內(nèi)的部分源碼。

    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)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ...

        return result;
    }

View對(duì)點(diǎn)擊事件的處理,首先會(huì)判斷有沒有設(shè)置OnTouchListener,因?yàn)镺nTouchListener的優(yōu)先級(jí)高于onTouchEvent。

onTouchEvent中,即使View處于不可用狀態(tài),照樣會(huì)消耗點(diǎn)擊事件。下面代碼可以看出來。

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

A disabled view that is clickable still consumes the touch events, it just doesn't respond to them,一個(gè)不可用的View仍然可以消耗事件,只是不做任何響應(yīng)。

onTouchEvent中對(duì)點(diǎn)擊事件的具體處理流程大概如下,只要View的CLICKABLE和LONG_CLICKABLE有一個(gè)為true,那么它就會(huì)消耗事件,返回true??偟膩碚f,View的可不可用不影響是否消耗事件,只要clickable或者longClickable有一個(gè)為true,那么它就會(huì)消耗事件。


**綜上,觸摸事件傳遞到View時(shí),會(huì)執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā),這里會(huì)判斷有沒有設(shè)置OnTouchListener,如果OnTouchListener的onTouch方法返回true,那么onTouchEvent就不會(huì)被調(diào)用。View的onTouchEvent默認(rèn)都會(huì)消耗事件,除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false),而View的enable屬性并不影響onTouchEvent的返回值。
**


四、滑動(dòng)沖突解決

上面主要主要介紹了View的事件分發(fā)機(jī)制的整個(gè)過程,在平常的開發(fā)中,在熟悉整個(gè)分發(fā)過程后,滑動(dòng)沖突問題應(yīng)該就不再是難題了。下面主要以一個(gè)典型的例子,介紹下滑動(dòng)沖突問題的解決。

滑動(dòng)沖突的產(chǎn)生主要是因?yàn)榻缑嬷袃?nèi)外兩層都可以滑動(dòng),比如一個(gè)界面外部可以左右滑動(dòng),內(nèi)部可以上下滑動(dòng)。這時(shí)就可以采取外部攔截法,前面我們提到分發(fā)過程中方法onInterceptTouchEvent主要是用于判斷是否攔截,那么外部攔截中我們可以重寫父容器的onInterceptTouchEvent方法,根據(jù)需要決定是否攔截。

public boolean onInterceptHoverEvent(MotionEvent event) {

        boolean intercepted = false;

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                break;

            case MotionEvent.ACTION_MOVE:
                if(父容器需要當(dāng)前點(diǎn)擊事件){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;

            case MotionEvent.ACTION_UP:
                break;

            default:
                break;
        }
        
        mLastXIntercept = x;
        mLastYIntercept = y;
        
        return intercepted;
    }

五、總結(jié)

到這里關(guān)于Android中View的事件分發(fā)機(jī)制就介紹的差不多了,歡迎指正批評(píng)。

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

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

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