Android從源碼的角度徹底理解事件分發(fā)機(jī)制的解析(上)
其實(shí)我一直準(zhǔn)備寫一篇關(guān)于Android事件分發(fā)機(jī)制的文章,從我的第一篇博客開始,就零零散散在好多地方使用到了Android事件分發(fā)的知識(shí)。也有好多朋友問過我各種問題,比如:onTouch和onTouchEvent有什么區(qū)別,又該如何使用?為什么給ListView引入了一個(gè)滑動(dòng)菜單的功能,ListView就不能滾動(dòng)了?為什么圖片輪播器里的圖片使用Button而不用ImageView?等等……對(duì)于這些問題,我并沒有給出非常詳細(xì)的回答,因?yàn)槲抑廊绻胍獜氐赘忝靼走@些問題,掌握Android事件分發(fā)機(jī)制是必不可少的,而Android事件分發(fā)機(jī)制絕對(duì)不是三言兩語就能說得清的。
在我經(jīng)過較長時(shí)間的籌備之后,終于決定開始寫這樣一篇文章了。目前雖然網(wǎng)上相關(guān)的文章也不少,但我覺得沒有哪篇寫得特別詳細(xì)的(也許我還沒有找到),多數(shù)文章只是講了講理論,然后配合demo運(yùn)行了一下結(jié)果。而我準(zhǔn)備帶著大家從源碼的角度進(jìn)行分析,相信大家可以更加深刻地理解Android事件分發(fā)機(jī)制。
閱讀源碼講究由淺入深,循序漸進(jìn),因此我們也從簡單的開始,本篇先帶大家探究View的事件分發(fā),下篇再去探究難度更高的ViewGroup的事件分發(fā)。
那我們現(xiàn)在就開始吧!比如說你當(dāng)前有一個(gè)非常簡單的項(xiàng)目,只有一個(gè)Activity,并且Activity中只有一個(gè)按鈕。你可能已經(jīng)知道,如果想要給這個(gè)按鈕注冊(cè)一個(gè)點(diǎn)擊事件,只需要調(diào)用:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});
這樣在onClick方法里面寫實(shí)現(xiàn),就可以在按鈕被點(diǎn)擊的時(shí)候執(zhí)行。你可能也已經(jīng)知道,如果想給這個(gè)按鈕再添加一個(gè)touch事件,只需要調(diào)用:
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
onTouch方法里能做的事情比onClick要多一些,比如判斷手指按下、抬起、移動(dòng)等事件。那么如果我兩個(gè)事件都注冊(cè)了,哪一個(gè)會(huì)先執(zhí)行呢?我們來試一下就知道了,運(yùn)行程序點(diǎn)擊按鈕,打印結(jié)果如下:

可以看到,onTouch是優(yōu)先于onClick執(zhí)行的,并且onTouch執(zhí)行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(你還可能會(huì)有多次ACTION_MOVE的執(zhí)行,如果你手抖了一下)。因此事件傳遞的順序是先經(jīng)過onTouch,再傳遞到onClick。
細(xì)心的朋友應(yīng)該可以注意到,onTouch方法是有返回值的,這里我們返回的是false,如果我們嘗試把onTouch方法里的返回值改成true,再運(yùn)行一次,結(jié)果如下:

我們發(fā)現(xiàn),onClick方法不再執(zhí)行了!為什么會(huì)這樣呢?你可以先理解成onTouch方法返回true就認(rèn)為這個(gè)事件被onTouch消費(fèi)掉了,因而不會(huì)再繼續(xù)向下傳遞。
如果到現(xiàn)在為止,以上的所有知識(shí)點(diǎn)你都是清楚的,那么說明你對(duì)Android事件傳遞的基本用法應(yīng)該是掌握了。不過別滿足于現(xiàn)狀,讓我們從源碼的角度分析一下,出現(xiàn)上述現(xiàn)象的原理是什么。
首先你需要知道一點(diǎn),只要你觸摸到了任何一個(gè)控件,就一定會(huì)調(diào)用該控件的dispatchTouchEvent方法。那當(dāng)我們?nèi)c(diǎn)擊按鈕的時(shí)候,就會(huì)去調(diào)用Button類里的dispatchTouchEvent方法,可是你會(huì)發(fā)現(xiàn)Button類里并沒有這個(gè)方法,那么就到它的父類TextView里去找一找,你會(huì)發(fā)現(xiàn)TextView里也沒有這個(gè)方法,那沒辦法了,只好繼續(xù)在TextView的父類View里找一找,這個(gè)時(shí)候你終于在View里找到了這個(gè)方法,示意圖如下:

然后我們來看一下View中dispatchTouchEvent方法的源碼:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
這個(gè)方法非常的簡潔,只有短短幾行代碼!我們可以看到,在這個(gè)方法內(nèi),首先是進(jìn)行了一個(gè)判斷,如果
mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和
mOnTouchListener.onTouch(this, event)這三個(gè)條件都為真,就返回true,否則就去執(zhí)行onTouchEvent(event)方法并返回。
先看一下第一個(gè)條件,mOnTouchListener這個(gè)變量是在哪里賦值的呢?我們尋找之后在View里發(fā)現(xiàn)了如下方法:
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
Bingo!找到了,mOnTouchListener正是在setOnTouchListener方法里賦值的,也就是說只要我們給控件注冊(cè)了touch事件,mOnTouchListener就一定被賦值了。
第二個(gè)條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當(dāng)前點(diǎn)擊的控件是否是enable的,按鈕默認(rèn)都是enable的,因此這個(gè)條件恒定為true。
第三個(gè)條件就比較關(guān)鍵了,mOnTouchListener.onTouch(this, event),其實(shí)也就是去回調(diào)控件注冊(cè)touch事件時(shí)的onTouch方法。也就是說如果我們?cè)趏nTouch方法里返回true,就會(huì)讓這三個(gè)條件全部成立,從而整個(gè)方法直接返回true。如果我們?cè)趏nTouch方法里返回false,就會(huì)再去執(zhí)行onTouchEvent(event)方法。
現(xiàn)在我們可以結(jié)合前面的例子來分析一下了,首先在dispatchTouchEvent中最先執(zhí)行的就是onTouch方法,因此onTouch肯定是要優(yōu)先于onClick執(zhí)行的,也是印證了剛剛的打印結(jié)果。而如果在onTouch方法里返回了true,就會(huì)讓dispatchTouchEvent方法直接返回true,不會(huì)再繼續(xù)往下執(zhí)行。而打印結(jié)果也證實(shí)了如果onTouch返回true,onClick就不會(huì)再執(zhí)行了。
根據(jù)以上源碼的分析,從原理上解釋了我們前面例子的運(yùn)行結(jié)果。而上面的分析還透漏出了一個(gè)重要的信息,那就是onClick的調(diào)用肯定是在onTouchEvent(event)方法中的!那我們馬上來看下onTouchEvent的源碼,如下所示:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
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;
}
return true;
}
return false;
}
相較于剛才的dispatchTouchEvent方法,onTouchEvent方法復(fù)雜了很多,不過沒關(guān)系,我們只挑重點(diǎn)看就可以了。
首先在第14行我們可以看出,如果該控件是可以點(diǎn)擊的就會(huì)進(jìn)入到第16行的switch判斷中去,而如果當(dāng)前的事件是抬起手指,則會(huì)進(jìn)入到MotionEvent.ACTION_UP這個(gè)case當(dāng)中。在經(jīng)過種種判斷之后,會(huì)執(zhí)行到第38行的performClick()方法,那我們進(jìn)入到這個(gè)方法里瞧一瞧:
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
可以看到,只要mOnClickListener不是null,就會(huì)去調(diào)用它的onClick方法,那mOnClickListener又是在哪里賦值的呢?經(jīng)過尋找后找到如下方法:
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
一切都是那么清楚了!當(dāng)我們通過調(diào)用setOnClickListener方法來給控件注冊(cè)一個(gè)點(diǎn)擊事件時(shí),就會(huì)給mOnClickListener賦值。然后每當(dāng)控件被點(diǎn)擊時(shí),都會(huì)在performClick()方法里回調(diào)被點(diǎn)擊控件的onClick方法。
這樣View的整個(gè)事件分發(fā)的流程就讓我們搞清楚了!不過別高興的太早,現(xiàn)在還沒結(jié)束,還有一個(gè)很重要的知識(shí)點(diǎn)需要說明,就是touch事件的層級(jí)傳遞。我們都知道如果給一個(gè)控件注冊(cè)了touch事件,每次點(diǎn)擊它的時(shí)候都會(huì)觸發(fā)一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這里需要注意,如果你在執(zhí)行ACTION_DOWN的時(shí)候返回了false,后面一系列其它的action就不會(huì)再得到執(zhí)行了。簡單的說,就是當(dāng)dispatchTouchEvent在進(jìn)行事件分發(fā)的時(shí)候,只有前一個(gè)action返回true,才會(huì)觸發(fā)后一個(gè)action。
說到這里,很多的朋友肯定要有巨大的疑問了。這不是在自相矛盾嗎?前面的例子中,明明在onTouch事件里面返回了false,ACTION_DOWN和ACTION_UP不是都得到執(zhí)行了嗎?其實(shí)你只是被假象所迷惑了,讓我們仔細(xì)分析一下,在前面的例子當(dāng)中,我們到底返回的是什么。
參考著我們前面分析的源碼,首先在onTouch事件里返回了false,就一定會(huì)進(jìn)入到onTouchEvent方法中,然后我們來看一下onTouchEvent方法的細(xì)節(jié)。由于我們點(diǎn)擊了按鈕,就會(huì)進(jìn)入到第14行這個(gè)if判斷的內(nèi)部,然后你會(huì)發(fā)現(xiàn),不管當(dāng)前的action是什么,最終都一定會(huì)走到第89行,返回一個(gè)true。
是不是有一種被欺騙的感覺?明明在onTouch事件里返回了false,系統(tǒng)還是在onTouchEvent方法中幫你返回了true。就因?yàn)檫@個(gè)原因,才使得前面的例子中ACTION_UP可以得到執(zhí)行。
那我們可以換一個(gè)控件,將按鈕替換成ImageView,然后給它也注冊(cè)一個(gè)touch事件,并返回false。如下所示:
imageView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
運(yùn)行一下程序,點(diǎn)擊ImageView,你會(huì)發(fā)現(xiàn)結(jié)果如下:

在ACTION_DOWN執(zhí)行完后,后面的一系列action都不會(huì)得到執(zhí)行了。這又是為什么呢?因?yàn)镮mageView和按鈕不同,它是默認(rèn)不可點(diǎn)擊的,因此在onTouchEvent的第14行判斷時(shí)無法進(jìn)入到if的內(nèi)部,直接跳到第91行返回了false,也就導(dǎo)致后面其它的action都無法執(zhí)行了。
好了,關(guān)于View的事件分發(fā),我想講的東西全都在這里了。現(xiàn)在我們?cè)賮砘仡櫼幌麻_篇時(shí)提到的那三個(gè)問題,相信每個(gè)人都會(huì)有更深一層的理解。
1. onTouch和onTouchEvent有什么區(qū)別,又該如何使用?
從源碼中可以看出,這兩個(gè)方法都是在View的dispatchTouchEvent中調(diào)用的,onTouch優(yōu)先于onTouchEvent執(zhí)行。如果在onTouch方法中通過返回true將事件消費(fèi)掉,onTouchEvent將不會(huì)再執(zhí)行。
另外需要注意的是,onTouch能夠得到執(zhí)行需要兩個(gè)前提條件,第一mOnTouchListener的值不能為空,第二當(dāng)前點(diǎn)擊的控件必須是enable的。因此如果你有一個(gè)控件是非enable的,那么給它注冊(cè)onTouch事件將永遠(yuǎn)得不到執(zhí)行。對(duì)于這一類控件,如果我們想要監(jiān)聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實(shí)現(xiàn)。
2. 為什么給ListView引入了一個(gè)滑動(dòng)菜單的功能,ListView就不能滾動(dòng)了?
如果你閱讀了Android實(shí)現(xiàn)滑動(dòng)菜單特效的完全解析這篇文章。當(dāng)時(shí)我在圖片輪播器里使用Button,主要就是因?yàn)锽utton是可點(diǎn)擊的,而ImageView是不可點(diǎn)擊的。如果想要使用ImageView,可以有兩種改法。第一,在ImageView的onTouch方法里返回true,這樣可以保證ACTION_DOWN之后的其它action都能得到執(zhí)行,才能實(shí)現(xiàn)圖片滾動(dòng)的效果。第二,在布局文件里面給ImageView增加一個(gè)android:clickable="true"的屬性,這樣ImageView變成可點(diǎn)擊的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到執(zhí)行的。
今天的講解就到這里了,相信大家現(xiàn)在對(duì)Android事件分發(fā)機(jī)制又有了進(jìn)一步的認(rèn)識(shí),在后面的文章中我會(huì)再帶大家一起探究Android中ViewGroup的事件分發(fā)機(jī)制,感興趣的朋友請(qǐng)繼續(xù)閱讀 Android事件分發(fā)機(jī)制完全解析,帶你從源碼的角度徹底理解(下) 。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android?Flutter實(shí)現(xiàn)自由落體彈跳動(dòng)畫效果
粒子運(yùn)動(dòng)是將對(duì)象按照一定物理公式進(jìn)行的自定義軌跡運(yùn)動(dòng),與普通動(dòng)畫不同的是,它沒有強(qiáng)制性的動(dòng)畫開始到結(jié)束的時(shí)間概念。本文將利用Flutter實(shí)現(xiàn)自由落體彈跳動(dòng)畫效果,感興趣的小伙伴可以學(xué)習(xí)一下2022-10-10
AndroidStudio利用android-support-multidex解決64k的各種異常
這篇文章主要為大家詳細(xì)介紹了AndroidStudio利用android-support-multidex解決64k的各種異常,感興趣的小伙伴們可以參考一下2016-09-09
Android實(shí)現(xiàn)圖片異步加載及本地緩存
這篇文章主要介紹了Android實(shí)現(xiàn)圖片異步加載及本地緩存的相關(guān)資料,需要的朋友可以參考下2016-02-02
Android編程實(shí)現(xiàn)獲取新浪天氣預(yù)報(bào)數(shù)據(jù)的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)獲取新浪天氣預(yù)報(bào)數(shù)據(jù)的方法,涉及Android基于新浪接口的調(diào)用及數(shù)據(jù)處理技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
Android自定義ViewGroup嵌套與交互實(shí)現(xiàn)幕布全屏滾動(dòng)
這篇文章主要為大家介紹了Android自定義ViewGroup嵌套與交互實(shí)現(xiàn)幕布全屏滾動(dòng)效果示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
android設(shè)備間實(shí)現(xiàn)無線投屏的示例代碼
Android提供了MediaProjection來實(shí)現(xiàn)錄屏,通過MediaProjection可以獲取當(dāng)前屏幕的視頻流,而視頻流需要通過編解碼來壓縮進(jìn)行傳輸,通過MediaCodec可實(shí)現(xiàn)視頻的編碼和解碼,這篇文章主要介紹了android設(shè)備間實(shí)現(xiàn)無線投屏,需要的朋友可以參考下2022-06-06
Android Studio出現(xiàn)Failed to pull selection: open failed: Permi
本篇文章給大家分享了Android Studio中導(dǎo)出數(shù)據(jù)庫文件的方法以及出現(xiàn)Failed to pull selection: open failed: Permission denied的解決思路,有興趣的學(xué)習(xí)下。2018-05-05
ScrollView與SeekBar綁定實(shí)現(xiàn)滑動(dòng)時(shí)出現(xiàn)小滑塊效果
這篇文章主要為大家詳細(xì)介紹了ScrollView與SeekBar綁定實(shí)現(xiàn)滑動(dòng)時(shí)出現(xiàn)小滑塊效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
從零開始學(xué)android實(shí)現(xiàn)計(jì)算器功能示例分享(計(jì)算器源碼)
這篇文章主要介紹了android實(shí)現(xiàn)的計(jì)算器功能示例,可以加減乘除;可以倒退,可以清空文本,大家參考使用吧2014-02-02

