【源碼解析】View的事件分發(fā)

一、引言

View的事件分發(fā)一直都是塊難啃的骨頭,每次都是在遇到問(wèn)題時(shí)才在網(wǎng)上找一下事件分發(fā)的流程宾毒,而每次看的時(shí)候當(dāng)時(shí)都以為懂了,但是過(guò)了一段時(shí)間卻又忘了殿遂。如此反復(fù)诈铛,對(duì)View的事件分發(fā)一直沒(méi)有搞懂乙各,所以這次決定結(jié)合源碼來(lái)分析View的事件分發(fā)機(jī)制。


二幢竹、源碼分析

在進(jìn)行源碼分析前耳峦,我們先來(lái)看一下View的事件分發(fā)的流程圖。



看一下這個(gè)事件分發(fā)流程圖焕毫,是不是覺(jué)得View的事件分發(fā)也沒(méi)有那么復(fù)雜吧蹲坷。

下面我們開(kāi)始通過(guò)源碼來(lái)驗(yàn)證上面流程的正確性,Let‘s go~

2.1 Activity對(duì)事件進(jìn)行分發(fā)

當(dāng)一個(gè)點(diǎn)擊事件發(fā)生時(shí)邑飒,事件最先傳遞到當(dāng)前View所在的Activity中循签,由Activity的dispatchTouchEvent進(jìn)行分發(fā)。

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

首先調(diào)用了getWindow(). superDispatchTouchEvent方法幸乒,返回true表示事件被消耗掉了懦底;返回false表示事件交給Activity的onTouchEvent方法處理。

getWindow()返回的是Window類(lèi)罕扎。在Android中聚唐,只有PhoneWindow是Window的唯一實(shí)現(xiàn)類(lèi),所以執(zhí)行的正是PhoneWindow里的方法腔召。

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

在PhoneWindow中杆查,也沒(méi)有復(fù)雜的處理邏輯,轉(zhuǎn)而調(diào)用了變量mDecor的superDispatchTouchEvent方法臀蛛。
這里的mDecor是DecorView類(lèi)型亲桦。DecorView我們挺熟悉了,它是Activity的頂級(jí)View浊仆。在Activity中客峭,我們通過(guò)setContentView方法設(shè)置了自定義View,而DecorView就是我們自定義View的父容器抡柿。

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

DecorView對(duì)于superDispatchTouchEvent方法的實(shí)現(xiàn)邏輯就更奇葩了舔琅,直接調(diào)用父類(lèi)的dispatchTouchEvent方法,而DecorView的父類(lèi)是ViewGroup洲劣。

注意了备蚓,事件這里已經(jīng)傳遞到了第一個(gè)頂級(jí)ViewGroup的dispatchTouchEvent方法了。

2.2 ViewGroup對(duì)事件進(jìn)行分發(fā)

ViewGroup中的dispatchTouchEvent方法的處理邏輯比較復(fù)雜囱稽,那我們就化整為零郊尝,一段一段的看。

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...省略

    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        //由于事件為ACTION_DOWN時(shí)战惊,重置了mGroupFlags流昏,所以一定會(huì)調(diào)用onInterceptTouchEvent方法。
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            //調(diào)用onInterceptTouchEvent方法,判斷是否需要攔截該事件
            intercepted = onInterceptTouchEvent(ev); 
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        //當(dāng)事件不為ACTION_DOWN横缔,而且mFirstTouchTarget == null時(shí)铺遂,intercepted置為true。
        intercepted = true;
    }
    ...省略
        
}

actionMasked == MotionEvent.ACTION_DOWN茎刚,我們比較好理解襟锐,表示按下事件。
但是mFirstTouchTarget != null膛锭,是什么意思呢粮坞?


其實(shí)看完下面的代碼就會(huì)知道,當(dāng)點(diǎn)擊事件被ViewGroup中的子View消費(fèi)后初狰,那么mFirstTouchTarget != null莫杈。反過(guò)來(lái)說(shuō),當(dāng)點(diǎn)擊事件被當(dāng)前ViewGroup消費(fèi)了奢入,那么mFirstTouchTarget == null筝闹。

我們知道,事件的最開(kāi)始一定是ACTION_DOWN事件腥光,然后伴隨著一系列的ACTION_MOVE事件关顷,最后是ACTION_UP事件。

當(dāng)事件為ACTION_DOWN時(shí)武福,mGroupFlags總是被重置了议双。

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

當(dāng)事件為ACTION_DOWN時(shí),由于mGroupFlags的重置捉片,所以disallowIntercept的值為false平痰,也就是說(shuō)onInterceptTouchEvent方法一定會(huì)執(zhí)行。

注意了伍纫,事件這里已經(jīng)傳遞到了第一個(gè)頂級(jí)ViewGroup的onInterceptTouchEvent方法了宗雇。

onInterceptTouchEvent方法的返回值決定著事件分發(fā)的走向,分為如下兩個(gè)分支:
1莹规、當(dāng)返回為true時(shí)逾礁,表示當(dāng)前ViewGroup攔截了該事件。
2访惜、當(dāng)返回為false時(shí),表示當(dāng)前ViewGroup不攔截該事件腻扇。該事件轉(zhuǎn)而傳遞到該ViewGroup的子View债热。

2.2.1 onInterceptTouchEvent返回true

這里先看第一個(gè)分支,當(dāng)返回為true時(shí)幼苛,intercepted的賦值為true窒篱。

if (!canceled && !intercepted) {
    //事件傳遞到子View才會(huì)走下面的邏輯
    ...省略
}

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    ...省略
}

intercepted的賦值為true,說(shuō)明事件被當(dāng)前ViewGroup攔截了,mFirstTouchTarget == null條件判斷成立墙杯,所以調(diào)用了dispatchTransformedTouchEvent方法配并。注意,這里的第三個(gè)參數(shù)為null高镐。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    
    ...省略

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}

上面提到的第三個(gè)參數(shù)為null溉旋,也就是說(shuō)child == null 成立,所以這里執(zhí)行了super. dispatchTouchEvent方法嫉髓。ViewGroup的父類(lèi)是View观腊,所以這里執(zhí)行的是View的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
    ...省略
    boolean result = false;
    ...省略

    if (onFilterTouchEventForSecurity(event)) {
        ...省略
        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;
}

這里判斷了當(dāng)前View(也就是上面的ViewGroup)的狀態(tài)是否是Enable的算行,并且當(dāng)前View是否通過(guò)setOnTouchListener方法設(shè)置了OnTouchListener接口方法并在onTouch方法中是否返回true梧油。

當(dāng)上面提到的條件都返回true時(shí),result的賦值為true州邢。當(dāng)result的賦值為true時(shí)儡陨,onTouchEvent方法并不會(huì)執(zhí)行。

這里得出一個(gè)結(jié)論:
當(dāng)View設(shè)置了OnTouchListener接口量淌,并在接口方法onTouch中返回true時(shí)骗村,View的onTouchEvent方法不會(huì)執(zhí)行,也就是說(shuō)事件不會(huì)傳遞到View的onTouchEvent方法中类少。

這里討論一般的情況叙身,當(dāng)result為false時(shí),onTouchEvent方法就被執(zhí)行了硫狞。

注意了信轿,事件這里已經(jīng)傳遞到了第一個(gè)頂級(jí)ViewGroup的onTouchEvent方法了。

當(dāng)onTouchEvent返回true時(shí)残吩,表示該事件被當(dāng)前View消耗了财忽,后續(xù)的一系列事件默認(rèn)都會(huì)傳遞給當(dāng)前View。

平時(shí)我們經(jīng)常給View設(shè)置的OnClickListener是在onTouchEvent中觸發(fā)的(源碼中調(diào)用了performClick方法)泣侮。結(jié)合上面得出的結(jié)論即彪,我們可以得到另外一個(gè)結(jié)論:
OnTouch比onTouchEvent先執(zhí)行,onTouchEvent比onClick先執(zhí)行活尊。我們平時(shí)給View設(shè)置的點(diǎn)擊事件是處于最末端執(zhí)行的隶校。

當(dāng)onTouchEvent返回false時(shí),表示當(dāng)前View不處理該事件蛹锰,我們回溯上面的方法深胳,這就相當(dāng)于最開(kāi)始的dispatchTouchEvent方法返回false,繼續(xù)回溯到Activity的dispatchTouchEvent方法铜犬。

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

也就是說(shuō)舞终,(getWindow().superDispatchTouchEvent(ev)返回false轻庆,Activity的onTouchEvent方法會(huì)被執(zhí)行。

2.2.2 onInterceptTouchEvent返回false

我們?cè)賮?lái)看第二個(gè)分支敛劝,當(dāng)ViewGroup的onInterceptTouchEvent方法返回false時(shí)余爆,則執(zhí)行如下邏輯。

if (!canceled && !intercepted) {
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        ...省略
        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            ...省略
            final View[] children = mChildren;
            //遍歷當(dāng)前ViewGroup的每一個(gè)子View夸盟,將該事件分發(fā)下去
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = getAndVerifyPreorderedIndex(
                        childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(
                        preorderedList, children, childIndex);
                ...省略
                resetCancelNextUpFlag(child);
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    // Child wants to receive touch within its bounds.
                    mLastTouchDownTime = ev.getDownTime();
                    if (preorderedList != null) {
                        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);
            }
            if (preorderedList != null) preorderedList.clear();
        }
        ...省略
    }
}

遍歷了當(dāng)前ViewGroup的每一個(gè)子View蛾方,將事件分發(fā)下去。對(duì)于ViewGroup的子View满俗,調(diào)用了dispatchTransformedTouchEvent方法转捕。

還有沒(méi)有印象?前面ViewGroup攔截事件時(shí)也調(diào)用了該方法唆垃,只不過(guò)當(dāng)時(shí)的第三個(gè)參數(shù)為null五芝。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    
    ...省略

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}

可以看到,當(dāng)child不為null時(shí)辕万,調(diào)用了child的dispatchTouchEvent方法枢步。

如果child是一個(gè)ViewGroup的話,那么事件分發(fā)的邏輯就和上面分析ViewGroup一樣了渐尿。所以醉途,回去對(duì)照上面的事件分發(fā)流程圖。事件分發(fā)到子ViewGroup時(shí)砖茸,邏輯和其父容器的一毛一樣的0妗!凉夯!

當(dāng)dispatchTransformedTouchEvent方法返回true時(shí)货葬,表示事件由該子View消耗了,所以addTouchTarget方法就會(huì)被執(zhí)行劲够。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

而mFirstTouchTarget就是在這里賦值的震桶。這也驗(yàn)證了上面那個(gè)結(jié)論:
當(dāng)mFirstTouchTarget != null 時(shí),表示事件由當(dāng)前ViewGroup的子View消耗了征绎。

ViewGroup對(duì)于事件分發(fā)的邏輯驗(yàn)證就告一段落了蹲姐。假設(shè)ViewGroup的子View不是一個(gè)容器,而是一個(gè)純粹的View人柿,那么事件分發(fā)邏輯又是怎樣的呢柴墩?

2.3 View對(duì)事件進(jìn)行分發(fā)

接著上面的邏輯,事件繼續(xù)傳遞凫岖,在dispatchTransformedTouchEvent方法中拐邪,如果child是一個(gè)View,那么調(diào)用的就是View的dispatchTouchEvent方法隘截。

public boolean dispatchTouchEvent(MotionEvent event) {
    ...省略
    boolean result = false;
    ...省略

    if (onFilterTouchEventForSecurity(event)) {
        ...省略
        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的dispatchTouchEvent方法扎阶。這里就只說(shuō)一下結(jié)論:
dispatchTouchEvent方法調(diào)用了onTouchEvent方法,方法的返回值表示事件是否被消耗了婶芭。當(dāng)事件沒(méi)有被消耗东臀,那么mFirstTouchTarget 沒(méi)有被賦值,即mFirstTouchTarget == null犀农。
此時(shí)事件回傳到了子View的父容器惰赋。如果父容器不處理該事件,事件會(huì)繼續(xù)往上傳呵哨,直到事件被消耗赁濒。
當(dāng)事件被消耗了,后面一系列事件都會(huì)傳遞到該View孟害。當(dāng)然在傳遞的過(guò)程中拒炎,ACTION_MOVE和ACTION_UP事件默認(rèn)會(huì)經(jīng)過(guò)父容器的onInterceptTouchEvent方法,父容器可以攔截在該方法中攔截事件挨务。
如果所有View都不消耗該事件击你,那么該事件會(huì)回傳到Activity。Activity如果也不消耗該事件谎柄,那么該事件就消失了丁侄。

三、總結(jié)

本文從源碼的角度分析了View的事件分發(fā)流程朝巫。事件從Activity開(kāi)始傳遞鸿摇,中間經(jīng)過(guò)ViewGroup的分發(fā)、攔截劈猿、消耗或者是View的分發(fā)拙吉、消耗等一系列方法的處理,事件或被ViewGroup糙臼、View消耗庐镐,或繼續(xù)往上傳遞給Activity處理,而這就是一個(gè)完整的事件分發(fā)流程变逃。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末必逆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子揽乱,更是在濱河造成了極大的恐慌名眉,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凰棉,死亡現(xiàn)場(chǎng)離奇詭異损拢,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)撒犀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)福压,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)掏秩,“玉大人,你說(shuō)我怎么就攤上這事荆姆∶苫茫” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵胆筒,是天一觀的道長(zhǎng)邮破。 經(jīng)常有香客問(wèn)我,道長(zhǎng)仆救,這世上最難降的妖魔是什么抒和? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮彤蔽,結(jié)果婚禮上摧莽,老公的妹妹穿的比我還像新娘。我一直安慰自己铆惑,他們只是感情好范嘱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著员魏,像睡著了一般丑蛤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撕阎,一...
    開(kāi)封第一講書(shū)人閱讀 51,698評(píng)論 1 305
  • 那天受裹,我揣著相機(jī)與錄音,去河邊找鬼虏束。 笑死棉饶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的镇匀。 我是一名探鬼主播照藻,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼汗侵!你這毒婦竟也來(lái)了幸缕?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晰韵,失蹤者是張志新(化名)和其女友劉穎发乔,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體雪猪,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡栏尚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了只恨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片译仗。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抬虽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出古劲,到底是詐尸還是另有隱情斥赋,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布产艾,位于F島的核電站,受9級(jí)特大地震影響滑绒,放射性物質(zhì)發(fā)生泄漏闷堡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一疑故、第九天 我趴在偏房一處隱蔽的房頂上張望杠览。 院中可真熱鬧,春花似錦纵势、人聲如沸踱阿。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)软舌。三九已至,卻和暖如春牛曹,著一層夾襖步出監(jiān)牢的瞬間佛点,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工黎比, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留超营,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓阅虫,卻偏偏與公主長(zhǎng)得像演闭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颓帝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

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