天天看點

從源碼的角度解析View的事件分發

有好多朋友問過我各種問題,比如:onTouch和onTouchEvent有什麼差別,又該如何使用?為什麼給ListView引入了一個滑動菜單的功能,ListView就不能滾動了?為什麼圖檔輪播器裡的圖檔使用Button而不用ImageView?等等……對于這些問題,我并沒有給出非常詳細的回答,因為我知道如果想要徹底搞明白這些問題,掌握Android事件分發機制是必不可少的,而Android事件分發機制絕對不是三言兩語就能說得清的。

在我經過較長時間的籌備之後,終于決定開始寫這樣一篇文章了。目前雖然網上相關的文章也不少,但我覺得沒有哪篇寫得特别詳細的(也許我還沒有找到),多數文章隻是講了講理論,然後配合demo運作了一下結果。而我準備帶着大家從源碼的角度進行分析,相信大家可以更加深刻地了解Android事件分發機制。

閱讀源碼講究由淺入深,循序漸進,是以我們也從簡單的開始,本篇先帶大家探究View的事件分發,下篇再去探究難度更高的ViewGroup的事件分發。

那我們現在就開始吧!比如說你目前有一個非常簡單的項目,隻有一個Activity,并且Activity中隻有一個按鈕。你可能已經知道,如果想要給這個按鈕注冊一個點選事件,隻需要調用:

1
2
3
4
5
6      
button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "onClick execute"); } });       

這樣在onClick方法裡面寫實作,就可以在按鈕被點選的時候執行。你可能也已經知道,如果想給這個按鈕再添加一個touch事件,隻需要調用:

1
2
3
4
5
6
7      
button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("TAG", "onTouch execute, action " + event.getAction()); return false; } });       

onTouch方法裡能做的事情比onClick要多一些,比如判斷手指按下、擡起、移動等事件。那麼如果我兩個事件都注冊了,哪一個會先執行呢?我們來試一下就知道了,運作程式點選按鈕,列印結果如下:

從源碼的角度解析View的事件分發

可以看到,onTouch是優先于onClick執行的,并且onTouch執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(你還可能會有多次ACTION_MOVE的執行,如果你手抖了一下)。是以事件傳遞的順序是先經過onTouch,再傳遞到onClick。

細心的朋友應該可以注意到,onTouch方法是有傳回值的,這裡我們傳回的是false,如果我們嘗試把onTouch方法裡的傳回值改成true,再運作一次,結果如下:

從源碼的角度解析View的事件分發

我們發現,onClick方法不再執行了!為什麼會這樣呢?你可以先了解成onTouch方法傳回true就認為這個事件被onTouch消費掉了,因而不會再繼續向下傳遞。

如果到現在為止,以上的所有知識點你都是清楚的,那麼說明你對Android事件傳遞的基本用法應該是掌握了。不過别滿足于現狀,讓我們從源碼的角度分析一下,出現上述現象的原理是什麼。

首先你需要知道一點,隻要你觸摸到了任何一個控件,就一定會調用該控件的dispatchTouchEvent方法。那當我們去點選按鈕的時候,就會去調用Button類裡的dispatchTouchEvent方法,可是你會發現Button類裡并沒有這個方法,那麼就到它的父類TextView裡去找一找,你會發現TextView裡也沒有這個方法,那沒辦法了,隻好繼續在TextView的父類View裡找一找,這個時候你終于在View裡找到了這個方法,示意圖如下:

從源碼的角度解析View的事件分發

然後我們來看一下View中dispatchTouchEvent方法的源碼:

1
2
3
4
5
6
7      
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }       

這個方法非常的簡潔,隻有短短幾行代碼!我們可以看到,在這個方法内,首先是進行了一個判斷,如果mOnTouchListener != null,(mViewFlags & ENABLED_MASK) == ENABLED和mOnTouchListener.onTouch(this, event)這三個條件都為真,就傳回true,否則就去執行onTouchEvent(event)方法并傳回。

先看一下第一個條件,mOnTouchListener這個變量是在哪裡指派的呢?我們尋找之後在View裡發現了如下方法:

1
2
3      
public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }       

Bingo!找到了,mOnTouchListener正是在setOnTouchListener方法裡指派的,也就是說隻要我們給控件注冊了touch事件,mOnTouchListener就一定被指派了。

第二個條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷目前點選的控件是否是enable的,按鈕預設都是enable的,是以這個條件恒定為true。

第三個條件就比較關鍵了,mOnTouchListener.onTouch(this, event),其實也就是去回調控件注冊touch事件時的onTouch方法。也就是說如果我們在onTouch方法裡傳回true,就會讓這三個條件全部成立,進而整個方法直接傳回true。如果我們在onTouch方法裡傳回false,就會再去執行onTouchEvent(event)方法。

現在我們可以結合前面的例子來分析一下了,首先在dispatchTouchEvent中最先執行的就是onTouch方法,是以onTouch肯定是要優先于onClick執行的,也是印證了剛剛的列印結果。而如果在onTouch方法裡傳回了true,就會讓dispatchTouchEvent方法直接傳回true,不會再繼續往下執行。而列印結果也證明了如果onTouch傳回true,onClick就不會再執行了。

根據以上源碼的分析,從原理上解釋了我們前面例子的運作結果。而上面的分析還透漏出了一個重要的資訊,那就是onClick的調用肯定是在onTouchEvent(event)方法中的!那我們馬上來看下onTouchEvent的源碼,如下所示:

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92      
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