最近在开发中遇到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事件传递的顺序:
- dispatchTouchEvent()
- onTouchListener–>onTouch方法
- onTouchEvent()
- 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方法,我们可以得出结论:
- View的onClick方法是在ACTION_UP事件中被执行的。
- 当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
现在我们对上面日志的四种情况具体分析一下:
- onTouch 返回 true,未设置onClick事件,此时会执行DOWN、MOVE、UP事件–>dispatchTouchEvent返回true
- onTouch 返回 false,未设置onClick事件,此时会执行DOWN事件–>dispatchTouchEvent返回false
- onTouch 返回 true,设置onClick事件,此时会执行DOWN、MOVE、UP事件–>dispatchTouchEvent返回true
- 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的事件传递机制,将在下一节中讲解,这里不做过多讨论。