3.4 View的事件分發(fā)機制(一)

1. 事件分發(fā)最重要的三個方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
}
  • dispatchTouchEvent(MotionEvent ev)
    用來進(jìn)行事件分發(fā),如果事件能到達(dá)當(dāng)前View楣黍,那么此方法一定會被調(diào)用,而且是先調(diào)用钉凌。返回值表示是否消耗當(dāng)前事件遏考。
  • onInterceptTouchEvent(MotionEvent ev)
    在dispatchTouchEvent方法內(nèi)部調(diào)用,用來判斷是否攔截某個事件鸟款,如果當(dāng)前View攔截了某個事件膏燃,那么在同一個事件序列當(dāng)中,此方法不會再被調(diào)用何什。返回值表示是否攔截當(dāng)前事件蹄梢。
  • onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中調(diào)用,用來處理點擊事件富俄,返回結(jié)果表示是否消耗當(dāng)前事件。
    三者關(guān)系用偽代碼說明:
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

2. 一些結(jié)論

  1. 同一個事件序列是指從手指接觸屏幕的那一刻開始而咆,到手指離開屏幕的那一刻結(jié)束霍比,在這個過程中所產(chǎn)生的一系列事件。這個事件以down事件開始暴备,中間含有數(shù)量不定的move事件悠瞬,最終以up事件結(jié)束。
  2. 正常情況下,一個事件序列只能被一個View攔截且消耗浅妆。因為一旦一個元素攔截了某事件(down事件)望迎,那么同一事件序列內(nèi)的所有事件都會交給它處理。但是可以通過其他特殊手段凌外,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理辩尊。
  3. 某個View一旦決定攔截(onInterceptTouchEvent),那么一個事件序列都只能由它來處理康辑,并且它的oninterceptTouchEvent不會再被調(diào)用摄欲。
  4. 某個View一旦開始處理(onTouchEvent)事件,如果不消耗ACTION_DOWN事件疮薇,那么同一事件序列的其他事件都不會再交給它來處理胸墙,并且事件將重新交由它的父元素處理,即父元素的onTouchEvent方法會被調(diào)用按咒。
  5. 如果View不消耗ACTION_DOWN以外的其他事件迟隅,那么這個點擊事件會消失,此時父元素的onTouchEvent并不會被調(diào)用励七,并且當(dāng)前View可以持續(xù)收到后續(xù)事件智袭,最終這些消失的點擊事件會傳遞給Activity處理。
  6. ViewGroup默認(rèn)不攔截任何事件呀伙,源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false补履。
  7. View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它剿另,那么它的onTouchEvent方法就會被調(diào)用箫锤。
  8. View的onTouchEvent默認(rèn)都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時為false)雨女。View的longClickable屬性默認(rèn)都為false谚攒,clickable屬性要分情況,比如button的clickable屬性默認(rèn)為true氛堕,而TextView的clickable屬性默認(rèn)為false馏臭。
  9. View的enable屬性不影響onTouchEvent的默認(rèn)返回值。哪怕一個View是disable狀態(tài)讼稚,只要它的clickable或longClickable有一個為true括儒,那么它的onTouchEvent返回的就是true。
  10. onClick會發(fā)生的前提是當(dāng)前View是可點擊的锐想,并且它收到了down和up事件帮寻。
  11. 事件傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素赠摇,然后再由父元素分發(fā)給子元素固逗,子元素可以通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過程浅蚪,但是ACTION_DOWN事件除外。

3. Activity對點擊事件的分發(fā)過程

點擊事件用MotionEvent來表示烫罩,當(dāng)一個點擊操作發(fā)生時惜傲,事件最先傳遞給當(dāng)前Activity,又Activity的dispatchTouchEvent來進(jìn)行分發(fā)贝攒,具體工作由Activity內(nèi)部的Windwo來完成盗誊。Window會將事件傳遞給decor view,decor view一般就是當(dāng)前界面的底層容器(即setContentView所設(shè)置的View的父容器)饿这,通過Activity.getWindow().getDecorView()獲得浊伙。

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

window是個抽象類,window的唯一實現(xiàn)是PhoneWindow长捧,看PhoneWindow的分發(fā)事件方法

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
DecorView extends FrameLayout implements RootViewSurfaceTaker{}

我們在activity中可以獲取DecorView

getWindow().getDecorView()

我們通過setContentView方法設(shè)置的Veiw是DecorView的子View∠桑現(xiàn)在事件已經(jīng)到ViewGroup了,繼續(xù)看ViewGroup的分發(fā)串结。

4. 頂級View對點擊事件的分發(fā)過程

事件到達(dá)頂級View后哑子,肯定會進(jìn)入dispatchTouchEvent方法中,該方法中首先判斷是否攔截肌割,攔截則當(dāng)前Veiw自己處理卧蜓,處理方式要先看是否有onTouchListener,有則執(zhí)行onTouchListener并根據(jù)其返回值看是否執(zhí)行OnTouchEvent把敞。不攔截則找到當(dāng)前點擊位置的子View繼續(xù)分發(fā)弥奸。View中dispatchTouchEvent方法的源碼:

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

繼續(xù)看ViewGroup的dispatchTouchEvent方法的攔截處理

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

ViewGroup在兩個條件下會判斷是否攔截當(dāng)前事件,ACTION_DOWN事件時或者mFirstTouchTarget不為空時奋早。mFirstTouchTarget會在事件由ViewGroup子元素成功處理時盛霎,被賦予子元素的值。
也就是事件被子元素處理了耽装,mFirstTouchTarget有值愤炸,沒被子元素處理,也就是被當(dāng)前ViewGroup攔截了掉奄,則mFirstTouchTarget就沒有值规个,就不滿足條件了。
假如down已經(jīng)被當(dāng)前viewGroup攔截姓建,當(dāng)move和up事件到來時诞仓,mFirstTouchTarget是空,所以會直接執(zhí)行intercepted=true速兔,也就是直接攔截move和up事件都交給當(dāng)前View處理狂芋。否則intercepted的值是onInterceptTouchEvent方法的返回值。
判斷了上面兩個條件憨栽,下面還有一個判斷:

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action); // restore action in case it was changed
} else {
    intercepted = false;
}

這里有一個標(biāo)志位FLAG_DISALLOW_INTERCEPT,在子view中可以通過下面方法設(shè)置:

 getParent().requestDisallowInterceptTouchEvent(true);

一旦設(shè)置后,ViewGroup將無法攔截除了ACTION_DOWN以外的其他點擊事件屑柔。
為什么除了ACTION_DOWN呢屡萤?因為在執(zhí)行上面的代碼前,還有一些代碼

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

如果是ACTION_DOWN掸宛,會重置標(biāo)記位死陆。由此我們知道兩點:

  • 第一點,onInterceptTouchEvent不是每次事件都會被調(diào)用唧瘾,如果我們想提前處理所有的點擊事件措译,要選擇dispatchTouchEvent方法,只有這個方法能確保每次都調(diào)用饰序。
  • 另外一點领虹,F(xiàn)LAG_DISALLOW_INTERCEPT標(biāo)記位能幫我們解決滑動沖突。

攔截或者不攔截由intercepted決定求豫,上面的條件判斷最后都會給intercepted賦值塌衰。然后看攔截和不攔截的代碼如下:

if (!canceled && !intercepted) {
    // 不攔截
}
if (mFirstTouchTarget == null) {
    // 攔截注意這里的dispatchTransformedTouchEvent方法.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

然后繼續(xù)看ViewGroup在不攔截時的詳細(xì)代碼。

for (int i = childrenCount - 1; i >= 0; i--) {
    ...

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    ...
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        ...
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
    ...
}

遍歷所有元素蝠嘉,判斷子元素是否能夠接受到點擊事件最疆。下面兩個方法就是判斷標(biāo)準(zhǔn),第一個表示是否可見以及是否有動畫蚤告。第二個表示點擊事件是否落在子元素的區(qū)域內(nèi)努酸。

private static boolean canViewReceivePointerEvents(View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}
protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

如果有滿足的元素則執(zhí)行dispatchTransformedTouchEvent方法

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

    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...
}

因為傳入的child不為null,所以調(diào)用子View的dispatchTouchEvent繼續(xù)分發(fā)杜恰。
如果子元素的分發(fā)返回了true获诈,則上面的代碼繼續(xù)執(zhí)行addTouchTarget方法

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

發(fā)現(xiàn)如果子View消耗掉了點擊事件,則給mFirstTouchTarget賦值箫章。它有了值烙荷,后續(xù)的move和up還會判斷是否要攔截。它沒有值檬寂,則直接由當(dāng)前View處理终抽。
如果遍歷所有的子View都沒有消耗事件。則調(diào)用

if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

注意第三個參數(shù)child為null桶至,則依據(jù)上面的源碼知道會調(diào)用super的dispatchTouchEvent方法昼伴。super是View,下面看View的dispatchTouchEvent方法

5. View的事件處理

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

首先是onTouchListener的判斷镣屹,然后執(zhí)行的onTouchEvent方法圃郊,在onTouchEvent的ACTION_UP時,會判斷并調(diào)用click方法女蜈。

if (!focusTaken) {
    if (mPerformClick == null) {
        mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) {
        performClick();
    }
}

這里注意ViewGroup和View的dispatchTouchEvent方法是不同的持舆,ViewGroup中的分發(fā)有攔截判斷色瘩;View中的分發(fā)只有onTouchListener的判斷,接著就調(diào)用了onTouchEvent方法逸寓。而ViewGroup是沒有重寫onTouchEvent方法的居兆,在事件攔截后,ViewGroup會調(diào)super的dispatchTouchEvent竹伸,也就是View的dispatchTouchEvent泥栖,在里面調(diào)onTouchEvent方法,當(dāng)然我們可以自己重寫onTouchEvent方法勋篓。

讀完這一章吧享,覺得作者自己很清楚,但寫出來還是覺得混亂譬嚣,連個流程圖都沒有钢颂。全是文字堆積,讓人看的昏昏欲睡孤荣。這里推薦兩篇郭霖的文章甸陌,相比之下還是比較清楚的,如果兩個結(jié)合來學(xué)習(xí)大有益處盐股。
Android事件分發(fā)機制完全解析钱豁,帶你從源碼的角度徹底理解(上)
Android事件分發(fā)機制完全解析,帶你從源碼的角度徹底理解(下)
附加一篇簡書上的另一片文章疯汁,他總結(jié)的比我好:
Android View 事件分發(fā)機制源碼詳解(ViewGroup篇)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牲尺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子幌蚊,更是在濱河造成了極大的恐慌谤碳,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溢豆,死亡現(xiàn)場離奇詭異蜒简,居然都是意外死亡,警方通過查閱死者的電腦和手機漩仙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門搓茬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人队他,你說我怎么就攤上這事卷仑。” “怎么了麸折?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵锡凝,是天一觀的道長。 經(jīng)常有香客問我垢啼,道長窜锯,這世上最難降的妖魔是什么张肾? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮衬浑,結(jié)果婚禮上捌浩,老公的妹妹穿的比我還像新娘。我一直安慰自己工秩,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布进统。 她就那樣靜靜地躺著助币,像睡著了一般。 火紅的嫁衣襯著肌膚如雪螟碎。 梳的紋絲不亂的頭發(fā)上眉菱,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音掉分,去河邊找鬼俭缓。 笑死,一個胖子當(dāng)著我的面吹牛酥郭,可吹牛的內(nèi)容都是我干的华坦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼不从,長吁一口氣:“原來是場噩夢啊……” “哼惜姐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起椿息,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤歹袁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后寝优,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體条舔,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年乏矾,在試婚紗的時候發(fā)現(xiàn)自己被綠了孟抗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡妻熊,死狀恐怖夸浅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情扔役,我是刑警寧澤帆喇,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站亿胸,受9級特大地震影響坯钦,放射性物質(zhì)發(fā)生泄漏预皇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一婉刀、第九天 我趴在偏房一處隱蔽的房頂上張望吟温。 院中可真熱鬧,春花似錦突颊、人聲如沸鲁豪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽爬橡。三九已至,卻和暖如春棒动,著一層夾襖步出監(jiān)牢的瞬間糙申,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工船惨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留柜裸,地道東北人。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓粱锐,卻偏偏與公主長得像疙挺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子卜范,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359

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