Android事件分發(fā)機(jī)制從0開始

1.?事件基礎(chǔ)

1.?MotionEvent

  1. 手指在屏幕的動(dòng)作被封裝成了MotionEvent
  2. 常用事件類型分為如下幾種:
  • MotionEvent.ACTION_DOWN---->手指剛剛接觸屏幕
  • MotionEvent.ACTION_MOVE---->手指在屏幕上移動(dòng)
  • MotionEvent.ACTION_UP------>手指從屏幕上松開的瞬間
  • MotionEvent.ACTION_CANCEL-->這個(gè)比較復(fù)雜,下文詳細(xì)分析
  1. 每個(gè)手勢操作都是以ACTION_DOWN開始,以ACTION_UP結(jié)束,中間夾雜著多個(gè)或者零個(gè)ACTION_MOVE。

2. 所謂的事件分發(fā)機(jī)制,就是對(duì)MotionEvent對(duì)象的分發(fā)過程。當(dāng)我們的手勢操作產(chǎn)生了一個(gè)MotionEvent對(duì)象時(shí),系統(tǒng)需要把它傳遞給一個(gè)具體的View。

3. 涉及到的三個(gè)主要方法:

1. public boolean dispatchTouchEvent(MotionEvent ev)

如果一個(gè)事件可以傳遞到當(dāng)前View,那么此View的這個(gè)方法一定可以被調(diào)用。此方法用來對(duì)事件進(jìn)行分發(fā),它的返回結(jié)果表示當(dāng)前事件是否被處理,也就是此事件是否被消耗。

2. public boolean onInterceptTouchEvent(MotionEvent event)

這個(gè)方法表示當(dāng)前View是或否攔截此事件,在dispatchTouchEvent(MotionEvent ev)中被調(diào)用。這個(gè)方法只在ViewGroup中存在,但是默認(rèn)返回false,也就是View默認(rèn)攔截傳遞到它的事件,而ViewGroup默認(rèn)不攔截任何事件。

3. public boolean onTouchEvent(MotionEvent event)

用來處理點(diǎn)擊事件,我們設(shè)置的點(diǎn)擊事件最終是在這個(gè)方法中被調(diào)用。它同樣在dispatchTouchEvent(MotionEvent ev)中被調(diào)用。

2.?事件分發(fā)的源碼解析

  1. 一個(gè)事件最先傳遞給Activity然后向下進(jìn)行事件分發(fā)。我們從Activity的dispatchTouchEvent()開始分析
/***Activity.dispatchTouchEvent(MotionEvent ev)***/
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
public Window getWindow() {
        return mWindow;
    }

通過在Activity中查找我們可以發(fā)現(xiàn)mWindow = new PhoneWindow(this, window, activityConfigCallback);

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

這個(gè)mDecor是DecorView的一個(gè)對(duì)象

/***DecorView***/
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}

我們知道,DecorView就是通過setContentView()這個(gè)方法設(shè)置的View的父View。而它繼承自FrameLayout顯然是一個(gè)View,更進(jìn)一步說是一個(gè)ViewGroup,這樣我們的事件就從Activity傳遞到了View。</br>
所以我們說一個(gè)事件總是從ViewGruop開始分發(fā),期間經(jīng)歷多個(gè)或零個(gè)ViewGroup,最終以ViewGroup或者View終結(jié)。

  1. ViewGroup的事件分發(fā)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }
    if (!canceled && !intercepted) {
        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            final View[] children = mChildren;
            for (int i = childrenCount - 1; i >= 0; i--) {
                //如果子元素成功處理了事件,則mFirstTouchTarget就會(huì)被賦值
                dispatchTransformedTouchEvent(ev, false, children[i], idBitsToAssign)
            }
        }
    }
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        // 調(diào)用的這個(gè)方法會(huì)去調(diào)用view.dispatchTouchEvent(),這樣onTouchEvent()就會(huì)被調(diào)用了
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                TouchTarget.ALL_POINTER_IDS);
    } else {
        // Dispatch to touch targets, excluding the new touch target if we already
        // dispatched to it.  Cancel touch targets if necessary.
        dispatchTransformedTouchEvent(ev, cancelChild,
                                      target.child, target.pointerIdBits);
    }
    return handled;
}

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

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    return handled;
}

以上源碼經(jīng)過了精簡,只能表示分發(fā)流程,幫助理解。詳細(xì)分析過程就不寫了。ViewGroup是個(gè)抽象類,我們不能直接用,干活的都是它的子類,但是我看了一圈貌似常用的幾種布局LinearLayout、FrameLayout等等都沒有重寫上述方法,也就是說它們的分發(fā)過程都是上面這個(gè)過程。

  1. View的事件分發(fā)
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    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;
}

public boolean onTouchEvent(MotionEvent event) {
    final int action = event.getAction();
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                               || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                              || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return clickable;
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        performClickInternal()-->performClick()-->onClick();
        return true;
    }
    return false;
}

3.?一些結(jié)論

1.某個(gè)View一旦決定攔截,那么這個(gè)事件序列都只能由它來處理。如果它是ViewGroup,那么它的onInterceptTouchEvent()不會(huì)被調(diào)用。因?yàn)闂l件actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null為false。

2.正常情況下,一個(gè)事件序列只能被一個(gè)View攔截消耗。因?yàn)橐坏┮粋€(gè)元素?cái)r截了此事件,那么同一個(gè)事件序列中的所有事件都會(huì)被直接交給它處理,因此同一個(gè)事件序列中的事件不能分別由兩個(gè)View同時(shí)處理,但是通過特殊手段可以做到,比如一個(gè)View將本該自己處理的事件通過onTouchEvent()強(qiáng)行傳遞給其它View處理。

3.某個(gè)View一旦開始處理事件,如果它不能消耗ACTION_DOWN事件,那么同一事件序列中的其它事件都不會(huì)再交給它來處理,并且事件將重新交給它的父元素去處理,即父元素的的onTouchEvent()會(huì)被調(diào)用。意思是事件一旦交給了一個(gè)View處理,那么它就必須消耗掉,否則同一事件序列中剩下的事件就不會(huì)再交給它來處理了。

4.如果View不消耗ACTION_DOWN以外的其它事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的onTouchEvent()并不會(huì)被調(diào)用,并且當(dāng)前View可以持續(xù)收到后續(xù)的事件,最終這些消失的事件會(huì)傳遞給Activity處理。

5.View的onTouchEvent()默認(rèn)消耗事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable都是false)。View的longClickable默認(rèn)都為false,clickable分情況。

6.onClick發(fā)生的前提是當(dāng)前View可點(diǎn)擊并且接收到了Down和up事件。

7.事件分發(fā)總是從父元素傳遞給子元素,但是在子元素中可以通過requestDisallowInterceptTouchEvent()來干預(yù)父元素中除了ACTION_DOWN之外的事件。

4.?ACTION_CANCEL

當(dāng)控件收到前驅(qū)事件(什么叫前驅(qū)事件?一個(gè)從DOWN一直到UP的所有事件組合稱為完整的手勢,中間的任意一次事件對(duì)于下一個(gè)事件而言就是它的前驅(qū)事件)之后,后面的事件如果被父控件攔截,那么當(dāng)前控件就會(huì)收到一個(gè)CANCEL事件,并且把這個(gè)事件會(huì)傳遞給它的子事件。(注意:這里如果在控件的onInterceptTouchEvent中攔截掉CANCEL事件是無效的,它仍然會(huì)把這個(gè)事件傳給它的子控件)之后這個(gè)手勢所有的事件將全部攔截,也就是說這個(gè)事件對(duì)于當(dāng)前控件和它的子控件而言已經(jīng)結(jié)束了。
在設(shè)計(jì)設(shè)置頁面的滑動(dòng)開關(guān)時(shí),如果不監(jiān)聽ACTION_CANCEL,在滑動(dòng)到中間時(shí),如果你手指上下移動(dòng),就是移動(dòng)到開關(guān)控件之外,則此時(shí)會(huì)觸發(fā)ACTION_CANCEL,而不是ACTION_UP,造成開關(guān)的按鈕停頓在中間位置。意思就是,當(dāng)用戶保持按下操作,并從你的控件轉(zhuǎn)移到外層控件時(shí),會(huì)觸發(fā)ACTION_CANCEL當(dāng)前的手勢被中斷,不會(huì)再接收到關(guān)于它的記錄。推薦將這個(gè)事件作為 ACTION_UP 來看待,但是要區(qū)別于普通的 ACTION_UP

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容