Android事件分發(fā)機制

事件分發(fā)機制主要是指觸摸事件在Activity螟加、ViewGroup直焙、View之間傳遞并消費的機制景东,分發(fā)順序為 Activity > ViewGroup > View奔誓;

主要方法:
VIewGroup相關:onInterceptTouchEvent()、dispatchTouchEvent()厨喂、onTouchEvent()
View相關:dispatchTouchEvent()和措、onTouchEvent()

1. Activity事件分發(fā)機制

一般情況下用戶按下Activity時,Activity會執(zhí)行dispatchTouchEvent方法蜕煌,開始分發(fā)流程,我們看下dispatchTouchEvent()方法源碼:

    //activity的事件分發(fā)方法
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

dispatchTouchEvent方法里就兩個處理:

(1)MotionEvent.ACTION_DOWN事件執(zhí)行時會調用onUserInteraction()方法贫母;我們看看這個方法做了啥盒刚;

    public void onUserInteraction() {
    }

這個方法實現(xiàn)是空的,不過我們可以從注釋和其他途徑可以了解到誓酒,該方法主要的作用是實現(xiàn)屏保功能,并且當此 Activity 在棧頂?shù)臅r候贮聂,觸屏點擊 Home、Back歼冰、Recent 鍵等都會觸發(fā)這個方法耻警。

(2)接下來是if()語句執(zhí)行了getWindow().superDispatchTouchEvent(ev)方法甸怕,能看出來他是獲取了Window并調用了其superDispatchTouchEvent方法腮恩,但你會發(fā)現(xiàn)Window是個抽象類,superDispatchTouchEvent是個抽象方法武契,看不出任何東西荡含,然后通過源碼追蹤你會發(fā)現(xiàn)其實getWindow獲取的是Window的子類PhoneWindow;

    final void attach(){
    ...
    mWindow = new PhoneWindow(this, window,     activityConfigCallback);
    ...
    }

所以我們來看看PhoneWindow的superDispatchTouch方法:

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

直接調用了DecorView的superDispatchTouchEvent方法全释,DecorView繼承自FrameLayout误债,是頂層View,所有界面的父類,而FrameLayout是ViewGroup的子類躺盛,所以最終調用的是ViewGroup的dispatchTouchEvent方法。

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    ...
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    ...
}
2. ViewGroup的事件分發(fā)機制

我們來看看ViewGroup的dispatchTouchEvent方法(Android5.0前的方法槽惫,5.0之后的方法比較復雜辩撑,但原理一樣):

public boolean dispatchTouchEvent(MotionEvent ev) { 
    ... // 僅貼出關鍵代碼
        // 重點分析1:ViewGroup每次事件分發(fā)時,都需調用onInterceptTouchEvent()詢問是否攔截事件
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {  

            // 判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認是false)各薇,可通過調用requestDisallowInterceptTouchEvent()修改
            // 判斷值2: !onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反
                    // a. 若在onInterceptTouchEvent()中返回false(即不攔截事件)君躺,就會讓第二個值為true棕叫,從而進入到條件判斷的內部
                    // b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會讓第二個值為false俺泣,從而跳出了這個條件判斷
                    // c. 關于onInterceptTouchEvent() ->>分析1

                ev.setAction(MotionEvent.ACTION_DOWN);  
                final int scrolledXInt = (int) scrolledXFloat;  
                final int scrolledYInt = (int) scrolledYFloat;  
                final View[] children = mChildren;  
                final int count = mChildrenCount;  

        // 重點分析2
            // 通過for循環(huán),遍歷了當前ViewGroup下的所有子View
            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);  

                    // 判斷當前遍歷的View是不是正在點擊的View横漏,從而找到當前被點擊的View
                    // 若是,則進入條件判斷內部
                    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;  

                        // 條件判斷的內部調用了該View的dispatchTouchEvent()
                        // 即 實現(xiàn)了點擊事件從ViewGroup到子View的傳遞(具體請看下面的View事件分發(fā)機制)
                        if (child.dispatchTouchEvent(ev))  { 

                        mMotionTarget = child;  
                        return true; 
                        // 調用子View的dispatchTouchEvent后是有返回值的
                        // 若該控件可點擊铝宵,那么點擊時华畏,dispatchTouchEvent的返回值必定是true,因此會導致條件判斷成立
                        // 于是給ViewGroup的dispatchTouchEvent()直接返回了true侣夷,即直接跳出
                        // 即把ViewGroup的點擊事件攔截掉

                                }  
                            }  
                        }  
                    }  
                }  
            }  
            boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                    (action == MotionEvent.ACTION_CANCEL);  
            if (isUpOrCancel) {  
                mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
            }  
            final View target = mMotionTarget;  

        // 重點分析3
        // 若點擊的是空白處(即無任何View接收事件) / 攔截事件(手動復寫onInterceptTouchEvent()百拓,從而讓其返回true)
        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);
            // 調用ViewGroup父類的dispatchTouchEvent()晰甚,即View.dispatchTouchEvent()
            // 因此會執(zhí)行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己處理該事件蓖捶,事件不會往下傳遞(具體請參考View事件的分發(fā)機制中的View.dispatchTouchEvent())
            // 此處需與上面區(qū)別:子View的dispatchTouchEvent()
        } 

        ... 
}

(1)ViewGroup分發(fā)事件時首先判斷當前Viewgroup有沒有攔截事件onInterceptTouchEvent(ev)扁远,若返回值為true則表明已經攔截了此事件,跳出判斷并闲,事件不會往下傳遞谷羞,若是flase則相反,事件會往下傳遞购公。
(2)獲取到ViewGroup的所有子View及子ViewGroup雁歌,通過for循環(huán)遍歷;先判斷次子View是否是正在點擊的View(通過位置判斷)比庄,如果是的話調用此子View的dispatchTouchEvent方法,這個方法是有返回值的制恍,如果返回值為true就表明事件已被消費神凑,跳出整個循環(huán),不再往下傳遞鹃唯,事件到此結束瓣喊。
(3)如果點擊的是空白處或事件被攔截的情況下會調用父類的dispatchTouchEvent()方法,即View.dispatchTouchEvent()洪橘,自己處理該事件棵帽,不會再往下傳遞。

分發(fā)流程:
ViewGroup分發(fā)流程
3. View事件的分發(fā)機制

View的分發(fā)機制也是從dispatchTouchEvent()開始的弟晚,我們分析下源碼指巡;

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

方法里有一個if判斷隶垮,里面有三個條件秘噪,若滿足了這三個條件會返回true,即消費了該事件蹋偏,否則執(zhí)行onTouchEvent()至壤,我們逐一分析這三個條件;
(1)mOnTouchListener != null黎棠,需要再View.setOnTouchListener()里注冊Touch事件;
(2)(mViewFlags & ENABLED_MASK) == ENABLED木西,判斷當前View是否enable随静,很多View默認enable;
(3)mOnTouchListener.onTouch(this, event) 回調以注冊的Touch方法:

button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
     
            return false;  
        }  
    });

若onTouch返回了true恋捆,上述三個條件全部成立扛门,從而使得View.dispatchTouchEvent()直接返回true,事件分發(fā)結束
若onTouch返回了false星立,上述三個條件不成立葬凳,執(zhí)行onTouchEvent(event);

4. View.onTouchEvent()源碼分析
public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  

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

                switch (event.getAction()) { 

                    // a. 若當前的事件 = 抬起View(主要分析)
                    case MotionEvent.ACTION_UP:  
                        boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  

                            ...// 經過種種判斷占业,此處省略

                            // 執(zhí)行performClick() ->>分析1
                            performClick();  
                            break;  

                    // b. 若當前的事件 = 按下View
                    case MotionEvent.ACTION_DOWN:  
                        if (mPendingCheckForTap == null) {  
                            mPendingCheckForTap = new CheckForTap();  
                        }  
                        mPrivateFlags |= PREPRESSED;  
                        mHasPerformedLongPress = false;  
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                        break;  

                    // c. 若當前的事件 = 結束事件(非人為原因)
                    case MotionEvent.ACTION_CANCEL:  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                        removeTapCallback();  
                        break;

                    // d. 若當前的事件 = 滑動View
                    case MotionEvent.ACTION_MOVE:  
                        final int x = (int) event.getX();  
                        final int y = (int) event.getY();  
        
                        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() {  

        if (mOnClickListener != null) {  
            playSoundEffect(SoundEffectConstants.CLICK);  
            mOnClickListener.onClick(this);  
            return true;  
            // 只要我們通過setOnClickListener()為控件View注冊1個點擊事件
            // 那么就會給mOnClickListener變量賦值(即不為空)
            // 則會往下回調onClick() & performClick()返回true
        }  
        return false;  
    }  

如果View是可點擊的就返回true晚顷,否則返回false。


控件被點擊

可以看出onTouch()的執(zhí)行優(yōu)先于onClick()瞳氓,所以一旦在onTouch()消費了事件onClick()就不會執(zhí)行栓袖。

5. 分發(fā)流程
分發(fā)流程
6. 總結

事件分發(fā)從activity的dispatchTouchEvent方法開始往下遍歷子View逐個尋找消費控件,如果有控件或布局消費了此事件恋沃,會執(zhí)行這個控件或布局的onTouchEvent()方法囊咏,到此事件結束,不會再往下傳遞梅割,如果沒有控件或布局消費此事件,view層級從里到外逐個調用onTouchEvent()方法泌类,直到activity自己消費此事件為止底燎。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末双仍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子朱沃,更是在濱河造成了極大的恐慌逗物,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件契邀,死亡現(xiàn)場離奇詭異莲祸,居然都是意外死亡椭迎,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門缴阎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來简软,“玉大人,你說我怎么就攤上這事建炫。” “怎么了艺配?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵转唉,是天一觀的道長稳捆。 經常有香客問我,道長乔夯,這世上最難降的妖魔是什么末荐? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮茂蚓,結果婚禮上剃幌,老公的妹妹穿的比我還像新娘。我一直安慰自己牍白,他們只是感情好抖棘,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著最岗,像睡著了一般朝捆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上驯用,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天蝴乔,我揣著相機與錄音,去河邊找鬼薇正。 笑死,一個胖子當著我的面吹牛钠怯,可吹牛的內容都是我干的曙聂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼断国,長吁一口氣:“原來是場噩夢啊……” “哼榆苞!你這毒婦竟也來了坐漏?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤街夭,失蹤者是張志新(化名)和其女友劉穎躏筏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體埃碱,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡酥泞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年婶博,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片名党。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡挠轴,死狀恐怖,靈堂內的尸體忽然破棺而出欧啤,到底是詐尸還是另有隱情启上,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布倒慧,位于F島的核電站包券,受9級特大地震影響,放射性物質發(fā)生泄漏付秕。R本人自食惡果不足惜侍郭,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望汰寓。 院中可真熱鬧苹粟,春花似錦、人聲如沸毛好。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吼驶。三九已至,卻和暖如春蟹演,著一層夾襖步出監(jiān)牢的瞬間酒请,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工布朦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昼窗,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓右遭,卻偏偏與公主長得像缤削,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子滚婉,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容