Android View事件傳遞機制


最近在開發中遇到view滑動沖突的問題,由於一開始就知道這個問題與view事件分發有關,之后在網上看了幾篇關於事件分發的資料后,開發中遇到的問題很快便得到解決。 
在這里總結一下我對view事件分發的理解。

首先,看下事件分發流程圖: 
這里寫圖片描述

Button事件演示

在對view的事件分發機制進行分析前,我們可以通過一個demo看看Button事件處理的流程。

protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.e("zhangke", "button->onClick"); } }); button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.e("zhangke", "button->onTouch " + event.getAction()); return false; } }); } 
  • 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
  • 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
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.touchdemo.MainActivity"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="button事件演示" /> </LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

當OnTouchListener.onTouch方法 return false,日志如下:

11-09 09:02:52.811 2241-2241/com.example.touchdemo E/zhangke: button->onTouch 0 11-09 09:02:52.811 2241-2241/com.example.touchdemo E/zhangke: button->onTouch 1 11-09 09:02:52.818 2241-2241/com.example.touchdemo E/zhangke: button->onClick
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

當OnTouchListener.onTouch方法 return true:日志如下:

11-09 09:05:02.931 2340-2340/com.example.touchdemo E/zhangke: button->onTouch 0 11-09 09:05:02.936 2340-2340/com.example.touchdemo E/zhangke: button->onTouch 1
  • 1
  • 2
  • 1
  • 2

這里可以得出一下結論:

  • 如果view為disenable,則:onTouchListener里面不會執行,但是會執行onTouchEvent(event)方法
  • 如果onTouchListener的onTouch方法返回了true,那么view里面的onTouchEvent就不會被調用了。
  • onTouchEvent方法中的ACTION_UP分支中觸發onclick事件監聽

View事件傳遞源碼解析

在分析View的事件傳遞時,我們可以明確一下View事件傳遞的順序:

  1. dispatchTouchEvent()
  2. onTouchListener–>onTouch方法
  3. onTouchEvent()
  4. onClickListener–>onClick方法

所以我們首先通過dispatchTouchEvent方法開始分析(代碼給予sdk22)

dispatchTouchEvent方法解析

以下是View中dispatchTouchEvent方法代碼:

   public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (!isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; /** * 1、View的事件機制 * mOnTouchListener != null :是否設置了OnTouchListener監聽事件 * (mViewFlags & ENABLED_MASK) == ENABLED :判斷控件是否是enable,默認true * li.mOnTouchListener.onTouch(this, event):OnTouchListener.onTouch方法返回值是否為true * * 當這三個條件成立時,onTouchEvent方法不會被執行。 * * 總結: * 1、OnTouchListener的優先級別高於onTouchEvent(便於開發者自己通過OnTouchListener處理事件) * 2、與demo中的結論相符合,View首先執行OnTouchListener.onTouch方法,如果該方法return false, * 那么將執行onClick方法,但是在dispatchTouchEvent方法中並沒有onClick方法相關的代碼, * 這說明onClick方法是在onTouchEvent方法中執行的。 * */ if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
  • 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
  • 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

onTouchEvent源碼解析:

    public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); /** * 1、首先判斷View是否是可用的 * 如果View是不可用,同樣會響應事件,但是此時是否消耗事件有View是否可點擊決定 */ if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // 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) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } /** * 2、當View的CLICKABLE或LONG_CLICKABLE為true時,View將消耗此事件,該邏輯判斷內return true; * 當View不能點擊時,View將不消耗此事件,return false * * */ if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_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 (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 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)) { /** * 3、ACTION_UP事件發生時,會調用performClick方法,該方法內部執行onClick方法 */ performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
  • 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
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 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
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
    public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在該方法中可以看到mOnClickListener.onClick(this)被調用了,到此處我們可以確認Button的click事件就是在onTouchEvent方法中調用的

通過onTouchEvent方法,我們可以得出結論:

  1. View的onClick方法是在ACTION_UP事件中被執行的。
  2. 當View為可點擊時,onTouchEvent方法才會返回true,這表示該View會消耗此事件,否則返回false,表示View不消耗此事件。

TextView事件傳遞分析

在上面的源碼解析中,我們反復提到事件被消耗了,那么應該怎么理解這個消耗的概念呢?

其實通過上面的分析,我們不難得出結論只有當dispatchTouchEvent方法的返回值為true時,就表示事件被View消耗了。 
而決定dispatchTouchEvent的返回值有兩種情況:

  • OnTouchListener.onTouch的返回值
  • onTouchEvent方法的返回值

OnTouchListener.onTouch的返回值我們可以手動控制,而onTouchEvent方法的返回值我們經過上面分析,我們可以狹義的理解成是由View的CLICKABLE和LONG_CLICKABLE決定的。

因為TextView默認是不可點擊的,這里我們通過TextView來詳細說明一下這個問題。

        TextView textView = (TextView) findViewById(R.id.textview);

        textView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.e("zhangke", "textview->onTouch " + event.getAction()); return true; } }); textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.e("zhangke", "textview->onClick "); } });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

首先,給TextView設置OnTouchListener方法(未設置OnClickListener監聽,此時默認TextView不可點擊),並改變onTouch方法的返回值,我們可以看下Log的具體輸出:

    // 1、當onTouch方法return true,未設置onClick事件,日志如下 11-10 10:46:24.128 23049-23049/? E/zhangke: textview->onTouch 0 11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 2 11-10 10:46:24.195 23049-23049/? E/zhangke: textview->onTouch 1 // 2、當onTouch方法return false,未設置onClick事件,日志如下 11-10 10:47:27.093 23849-23849/? E/zhangke: textview->onTouch 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

給TextView設置OnClickListener監聽,並改變並改變onTouch方法的返回值,我們可以看下Log的具體輸出:

    // 3、當onTouch方法return true,設置onClick事件后,日志如下 11-10 11:03:23.596 31065-31065/? E/zhangke: textview->onTouch 0 11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 2 11-10 11:03:23.642 31065-31065/? E/zhangke: textview->onTouch 1 // 4、當onTouch方法return false,設置onClick事件后,日志如下 11-10 11:02:20.387 30674-30674/? E/zhangke: textview->onTouch 0 11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 2 11-10 11:02:20.450 30674-30674/? E/zhangke: textview->onTouch 1 11-10 11:02:20.451 30674-30674/? E/zhangke: textview->onClick 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

現在我們對上面日志的四種情況具體分析一下:

  1. onTouch 返回 true,未設置onClick事件,此時會執行DOWN、MOVE、UP事件–>dispatchTouchEvent返回true
  2. onTouch 返回 false,未設置onClick事件,此時會執行DOWN事件–>dispatchTouchEvent返回false
  3. onTouch 返回 true,設置onClick事件,此時會執行DOWN、MOVE、UP事件–>dispatchTouchEvent返回true
  4. onTouch 返回 true,設置onClick事件,此時會先執行DOWN、UP事件,然后執行onClick事件–>dispatchTouchEvent返回true

對於1、3、4這三種情況我們通過源碼分析都能理解,但是對於情況2 僅僅響應了DOWN,而沒有響應MOVE和UP事件,是不是感覺很奇怪?

這里可以給大家一個結論: 
如果View響應了DOWN事件,那么它必須要消耗該事件(dispatchTouchEvent返回true),否則它將不會接收到MOVE和UP事件。

具體原因: 
這是因為dispatchTouchEvent的返回值引起的,具體就是當View在響應DOWN事件時,dispatchTouchEvent的返回值將決定View的父控件是否攔截MOVE、UP事件。

  • 返回值為 true,不攔截MOVE、UP事件。
  • 返回值為 false,攔截MOVE、UP事件。

關於ViewGroup的事件傳遞機制,將在下一節中講解,這里不做過多討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM