Android View體系之Touch事件傳遞源碼解析(8.0)

技術背景

從 View 體系中認識 Touch 事件傳遞,暫時留一條線索:
" View 最原始的事件從哪裏來? ”
從 WindowCallbacKWrapper開始的。
那麼,我們開始吧!

tip:閱讀源碼前,建議讀懂 Android View體系之基礎常識及技巧

千里之行,始於Activity

從 window 層開始下發事件後, Activity 開始處理事件,會調用 ViewGroup#dispatchTouchEvent

 

 

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

PhoneWindow.java
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

ViewGroup.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

	...

	}

Activity#dispatchTouchEvent 方法最後會通過 DecorView 觸發 ViewGroup#dispatchTouchEvent 開始分發事件。

總結Activity 下發 Touch 事件到 DecorView 並由 DecorView 開始向下傳遞。

ViewGroup之核心分發

DecorView 調用 dispatchTouchEvent 分發 Touch 事件。代碼很長,可是不難,邏輯比較清晰。

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {

//默認部分下發 Touch 事件
      boolean handled = false;

// 1. 檢測是否分發Touch事件(判斷窗口是否被遮擋住)
// 如果該 Touch 事件沒有被窗口遮擋,則繼續下面邏輯
      if (onFilterTouchEventForSecurity(ev)) {

	// 獲取 Touch Action
          final int action = ev.getAction();
          final int actionMasked = action & MotionEvent.ACTION_MASK;

	// 2. 判斷事件是否是點擊事件
	// 清空所有接收觸摸事件View的引用
	// 設置mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT,默認允許攔截事件
	// 設置mNestedScrollAxes = SCROLL_AXIS_NONE,默認視圖不滾動
          if (actionMasked == MotionEvent.ACTION_DOWN) {
              cancelAndClearTouchTargets(ev);
              resetTouchState();
          }

        	// 3. 判斷事件是否需要攔截 - intercepted 
	// 判斷是否運行不允許攔截
	// 如果允許攔截,則通過 onInterceptTouchEvent 方法返回
          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 {
	//說明傳遞的事件不是ACTION_DOWN,意味着ACTION_DOWN已經被攔截過了。
              intercepted = true;
          }

        	// 4. 判斷事件是否取消事件 - canceled 
	// 如果 view.mPrivateFlags 被設置 FLAG_CANCEL_NEXT_UP_EVENT,則該 view 已經脫離視圖
	// 置 view.mPrivateFlags 標誌
	// 如果 當前標誌爲 FLAG_CANCEL_NEXT_UP_EVENT 或者 接收 MotionEvent.ACTION_CANCEL 事件,返回 true
	// 也就是說,如果View detach時,則calceled返回true
          final boolean canceled = resetCancelNextUpFlag(this)
                  || actionMasked == MotionEvent.ACTION_CANCEL;

          final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
          TouchTarget newTouchTarget = null;
          boolean alreadyDispatchedToNewTouchTarget = false;

	 // 5. 如果事件沒有被取消且沒有被攔截,走下面邏輯判斷是否需要傳遞
          if (!canceled && !intercepted) {

              // 忽略:檢測無障礙焦點
              View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                      ? findChildWithAccessibilityFocus() : null;

		// 6. 如果是點擊事件,區分單指,多指,或者hover(類似鼠標懸浮)
		// actionIndex 返回 0 ,id則:第一指id爲0,第二指id爲1,依次遞增
		// idBitsToassign,第一指爲1,第二指爲2,第三指4,依次指數遞增
              if (actionMasked == MotionEvent.ACTION_DOWN
                      || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                      || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                  final int actionIndex = ev.getActionIndex(); // always 0 for down
                  final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                          : TouchTarget.ALL_POINTER_IDS;

			//清除當前手指觸摸的target引用
                  removePointersFromTouchTargets(idBitsToAssign);
	
                  final int childrenCount = mChildrenCount;
                  if (newTouchTarget == null && childrenCount != 0) {

                      final float x = ev.getX(actionIndex);
                      final float y = ev.getY(actionIndex);
               		
				// 7. 掃描可傳遞的子 View 列表,按照Z軸座標大小排序返回列表 preorderedList 
				// 遍歷子 view 列表並結合 preorderedList  找出符合繪製條件的 view
                      final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                      final boolean customOrder = preorderedList == null
                              && isChildrenDrawingOrderEnabled();

                      final View[] children = mChildren;
                      for (int i = childrenCount - 1; i >= 0; i--) {
                          final int childIndex = getAndVerifyPreorderedIndex(
                                  childrenCount, i, customOrder);
                          final View child = getAndVerifyPreorderedView(
                                  preorderedList, children, childIndex);

                          //忽略:如果該 View 存在無障礙焦點,則跳過
                          if (childWithAccessibilityFocus != null) {
                              if (childWithAccessibilityFocus != child) {
                                  continue;
                              }
                              childWithAccessibilityFocus = null;
                              i = childrenCount - 1;
                          }

					// 8. 判斷子 view 是否不可見或者不在子 view 的範圍內
					// 如果滿足,則跳過
                          if (!canViewReceivePointerEvents(child)
                                  || !isTransformedTouchPointInView(x, y, child, null)) {
                              ev.setTargetAccessibilityFocus(false);
                              continue;
                          }

					// 9. 遍歷 target 鏈表,找到當前 child 對應的 target
					// 如果存在,則把手指的觸摸 id 賦值 pointerIdBits 遍歷
					// 這個比較難理解,舉個栗子。
					// 假如我食指按在一個 view(A) 上,然後中指在食指未起來之前又按在 view(A)
					// 上,則會把後者的 idBitsToAssign 更新到指向view(A) 的 newTouchTarget 對象上
                          newTouchTarget = getTouchTarget(child);
                          if (newTouchTarget != null) {
                              newTouchTarget.pointerIdBits |= idBitsToAssign;
                              break;
                          }

					// 清除子 view.mPrivateFlags PFLAG_CANCEL_NEXT_UP_EVENT 標誌
                          resetCancelNextUpFlag(child);

					// 10. 返回下發 Touch 事件結果。
					// 如果沒有子 view,則返回 view#dispatchTouchEvent 結果,實際上這裏會發生遞歸。
					// 如果子 view 消費了 Touch 事件,則會添加到 target 鏈接
                          if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
  
                              mLastTouchDownTime = ev.getDownTime();
                              if (preorderedList != null) {
                                  // childIndex points into presorted list, find original index
                                  for (int j = 0; j < childrenCount; j++) {
                                      if (children[childIndex] == mChildren[j]) {
                                          mLastTouchDownIndex = j;
                                          break;
                                      }
                                  }
                              } else {
                                  mLastTouchDownIndex = childIndex;
                              }
                              mLastTouchDownX = ev.getX();
                              mLastTouchDownY = ev.getY();
                              newTouchTarget = addTouchTarget(child, idBitsToAssign);
                              alreadyDispatchedToNewTouchTarget = true;
                              break;
                          }

                          // The accessibility focus didn't handle the event, so clear
                          // the flag and do a normal dispatch to all children.
                          ev.setTargetAccessibilityFocus(false);
                      }
                      if (preorderedList != null) preorderedList.clear();
                  }

			// 11. 如果沒有子 view 沒有消費 Touch 事件且 mFirstTouchTarget 卻不爲 null
			// 這就鬱悶了,比較難理解,舉個栗子。
			// 前一個 if 條件是 newTouchTarget == null && childrenCount != 0
			// 如果我第一根手指按在 viewGroup(A)中 textview(a)
			// 同時,另一根手指按在除 textview(a)外空白區域(viewGroup(A)內)
                  if (newTouchTarget == null && mFirstTouchTarget != null) {
                      // Did not find a child to receive the event.
                      // Assign the pointer to the least recently added target.
                      newTouchTarget = mFirstTouchTarget;
                      while (newTouchTarget.next != null) {
                          newTouchTarget = newTouchTarget.next;
                      }
                      newTouchTarget.pointerIdBits |= idBitsToAssign;
                  }
              }
          }

	
	// 12. 如果 mFirstTouchTarget == null 意味着 沒有任何 view 消費該 Touch 事件
	// 則 viewGroup 會調用dispatchTransformedTouchEvent處理,但是child==null,會調用
	// view#dispatchTouchEvent處理。
          if (mFirstTouchTarget == null) {
              // No touch targets so treat this as an ordinary view.
              handled = dispatchTransformedTouchEvent(ev, canceled, null,
                      TouchTarget.ALL_POINTER_IDS);
          } else {
		// 13. 當前有子 view 消費了 Touch 事件
              TouchTarget predecessor = null;
              TouchTarget target = mFirstTouchTarget;
              while (target != null) {
                  final TouchTarget next = target.next;

			//如果當前 target是新加入
                  if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                      handled = true;
                  } else {

				//是否需要向子 view 傳遞 ACTION_CANCEL 事件
                      final boolean cancelChild = resetCancelNextUpFlag(target.child)
                              || intercepted;

				// 14. 遞歸返回事件分發結果
                      if (dispatchTransformedTouchEvent(ev, cancelChild,
                              target.child, target.pointerIdBits)) {
                          handled = true;
                      }
                      if (cancelChild) {
                          if (predecessor == null) {
                              mFirstTouchTarget = next;
                          } else {
                              predecessor.next = next;
                          }
                          target.recycle();
                          target = next;
                          continue;
                      }
                  }
                  predecessor = target;
                  target = next;
              }
          }

          // 14. 檢測是否是取消標誌
          if (canceled
                  || actionMasked == MotionEvent.ACTION_UP
                  || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
              resetTouchState();
          } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
              final int actionIndex = ev.getActionIndex();
              final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
              removePointersFromTouchTargets(idBitsToRemove);
          }
      }

	// 忽略:測試代碼
      if (!handled && mInputEventConsistencyVerifier != null) {
          mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
      }
      return handled;
  }

如果看完上述註釋還有點蒙,一定要多擼幾次源碼。有幾個點想說明一下,可能大家會好理解要一點。

  • TouchTarget mFirstTouchTarget 的作用
    mFirstTouchTarget 貫穿 dispatchTouchEvent 流程,實際上它是一個鏈表,用於記錄所有接受 Touch 事件的 子view 。在日常開發中有沒有遇到過這樣一個邏輯“ 如果一個 view沒有接收過 ACTION_DOWN 的事件,那麼後續 ACTION_MOVE 和 ACTION_UP 也一定不會分發到這個 view。 ”。這個邏輯是基於 mFirstTouchTarget 記錄的 view 實現的。

總結: 根據上述註釋邏輯鏈。

  1. 過濾’不合法’的 Touch 事件;
  2. 如果是 MotionEvent.ACTION_DOWN ,則初始化一些狀態;
  3. 判斷事件是否需要攔截,是否需要取消;
  4. 如果不需要攔截&不是取消事件,則會向子 view 下發 Touch 事件;
  5. 如果沒有任何子 view 消費事件,則會自己處理,如果已有子 view 消費事件,判斷當前新處理的 target 對象是否是 mFirstTouchTarget 鏈表最新一個,如果是則默認爲當前傳遞已經傳遞事件,否則返回子 view 遞歸結果。

上述有兩個遞歸,在 註釋10 和 註釋14,這裏你可能會有疑惑,這兩個的關係是什麼。
註釋10 實際上是返回以 viewGroup 爲根節點的 view 下是否有節點消費點擊事件,如果有則記錄下當前子 view。
註釋14 實際上是返回以 viewGroup 爲根節點的 view 下是否有節點消費事件。  

ViewGroup之遞歸入口

在上一章節,dispatchTouchEvent 多次調用 dispatchTransformedTouchEvent,這裏做下簡單分析。

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
  private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
          View child, int desiredPointerIdBits) {

// 1. 是否下發 Touch 事件
      final boolean handled;

// 2. 如果事件傳遞已經取消 或者當前事件是 ACTION_CANCEL
// 如果沒有子 view,則返回 view#dispatchTouchEvent 
// 如果有子 view,則 返回 child#dispatchTouchEvent
      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;
      }

		// 3. 計算當前新手指id
      final int oldPointerIdBits = event.getPointerIdBits();
      final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
      if (newPointerIdBits == 0) {
          return false;
      }

// 4. 處理事件轉化
      final MotionEvent transformedEvent;
      if (newPointerIdBits == oldPointerIdBits) {
          if (child == null || child.hasIdentityMatrix()) {
              if (child == null) {
                  handled = super.dispatchTouchEvent(event);
              } else {
                  final float offsetX = mScrollX - child.mLeft;
                  final float offsetY = mScrollY - child.mTop;
                  event.offsetLocation(offsetX, offsetY);

                  handled = child.dispatchTouchEvent(event);

                  event.offsetLocation(-offsetX, -offsetY);
              }
              return handled;
          }
          transformedEvent = MotionEvent.obtain(event);
      } else {
          transformedEvent = event.split(newPointerIdBits);
      }

      // 5. 返回是否傳遞 Touch 事件
// 如果沒有子 view,則返回 view#dispatchTouchEvent 
// 如果有子 view,則 返回 child#dispatchTouchEvent
      if (child == null) {
          handled = super.dispatchTouchEvent(transformedEvent);
      } else {
          final float offsetX = mScrollX - child.mLeft;
          final float offsetY = mScrollY - child.mTop;
          transformedEvent.offsetLocation(offsetX, offsetY);
          if (! child.hasIdentityMatrix()) {
              transformedEvent.transform(child.getInverseMatrix());
          }
          handled = child.dispatchTouchEvent(transformedEvent);
      }

      // Done.
      transformedEvent.recycle();
      return handled;
  }

總結dispatchTransformedTouchEvent 會對非 MotionEvent.ACTION_CANCEL 事件做轉化並遞歸返回所有事件的下發結果。

View也可分發事件?

既然不是 view,那麼 dispatchTouchEvent 應該不是屬於下發範疇的,那會是什麼呢?

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
  public boolean dispatchTouchEvent(MotionEvent event) {

//...

      boolean result = false

      //停止滑動
      if (actionMasked == MotionEvent.ACTION_DOWN) {
          // Defensive cleanup for new gesture
          stopNestedScroll();
      }

// 1.過濾不合法的 Touch 事件
      if (onFilterTouchEventForSecurity(event)) {

	// 2. 如果 view 可用且當前事件被當做拖拽事件處理,返回true
          if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
              result = true;
          }
         
	// 3. 如果 view 可用監聽且 OnTouchListener 處理了 touch 事件
          ListenerInfo li = mListenerInfo;
          if (li != null && li.mOnTouchListener != null
                  && (mViewFlags & ENABLED_MASK) == ENABLED
                  && li.mOnTouchListener.onTouch(this, event)) {
              result = true;
          }

	// 4. 如果 onTouchEvent 消費了事件
          if (!result && onTouchEvent(event)) {
              result = true;
          }
      }

	//...

      return result;
  }

上述邏輯表明有三種場景下會返回 true 結果。
兩種場景爲:第一種是拖拽場景,比如listview等控件存在這種邏輯;另一種是開發者設置了 OnTouchListener 對象並在 onTouch 函數中處理並返回 true 結果。
最後一種場景爲普遍場景,及如果沒有上述兩種場景且是當前是最外層 view 時(事件已經無法再傳遞),則會調用自身的 onTouch 方法處理。

總結view#dispatchTouchEvent 會在事件下發鏈末端調用,並把當前 view 的 onTouch 返回值作爲 dispatchTouchEvent 返回值。

回看ViewGroup如何攔截

上述篇章的下發邏輯都需要判斷 Touch 事件是否需要被攔截,先看看代碼。

1
2
3
4
5
6
7
8
9
10
  public boolean onInterceptTouchEvent(MotionEvent ev) {
//判斷是否是鼠標事件且是滾輪滑動
      if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
              && ev.getAction() == MotionEvent.ACTION_DOWN
              && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
              && isOnScrollbarThumb(ev.getX(), ev.getY())) {
          return true;
      }
      return false;
  }

上面的代碼在絕大部分情況下都返回 false 。除非你鼠標事件且在上面滾動,這個場景很像你在滾動網頁一樣,那麼當前的頁面就會攔截滾動事件進行頁面滾動。
值得注意的是:如果你在該方法返回 true 進行攔截,那麼你會走下面的調用邏輯:

  1. viewGroup#dispatchTouchEvent
  2. viewGroup#dispatchTransformedTouchEvent
  3. view#dispatchTouchEvent
  4. view#onTouchEvent

總結viewGroup#onInterceptTouchEvent 是 ViewGroup 特有的方法。默認情況下 ViewGroup 不會攔截 Touch 事件,如果攔截了 Touch 事件,則會交給 View#onTouch 進行處理。

事件的宿命onTouchEvent

這個方法是處理 Touch 事件,並返回結果給 dispatchTouchEvent 的。可以理解爲:它決定了某個 view 是否真正消費 Touch 事件。直接看源碼。

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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

		// 1. 判斷是否是 DISABLED 的 view
		// 如果是,且已經按下之後擡起,則會消費掉 Touch 事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            return clickable;
        }

		//2. 如果設置了 mTouchDelegate,則會直接返回 true
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

		//3. 如果可點擊或者顯示了 toolTip,則會開始判斷處理 Touch 事件
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {

				//4. 處理 MotionEvent.ACTION_UP
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }

					// 5. 顯示 toolTip 時清空對應狀態,返回 true
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
           
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

						// 確保用戶看到已按壓狀態
                        if (prepressed) {
                            setPressed(true, x, y);
                        }

						// 6. 如果調用長按行爲或者且不忽略下一次事件(觸筆)
						// 移出長按回調
						// 如果我們在按壓狀態下,則 post PerformClick 對象(runnable)
						// 值得一提的是,PerformClick 對象的 run 方法實際上也是調用
						// performClick 方法,該方法調用 view 的 onClickListener#onClick 方法
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
              
                            removeLongPressCallback();
                            if (!focusTaken) {
                              
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

						// 設置非按壓狀態
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

				// 7. 處理 MotionEvent.ACTION_DOWN 事件
                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

					// 8. 如果不可點擊,則檢測是否可以長按
					// 如果可以,則發送一個延遲 500 ms 的長按事件
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

					// 9. 檢測是否觸發是鼠標右鍵類行爲,如果是則直接跳過
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

					// 10. 是否在可滑動的容器中
					// 該方法會遞歸查詢每一個 viewGroup 容器是是否會支持當用戶嘗試滑動
					// 內容時阻止pressed state的出現,該 pressed state 會被延遲反饋
                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
						//設置按壓狀態,檢測長按
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

				// 11.處理 MotionEvent.ACTION_CANCEL 事件
				// 設置非按壓狀態,清空tap和長按回調,重置狀態
                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

				// 12. 處理 MotionEvent.ACTION_MOVE
                case MotionEvent.ACTION_MOVE:

					// 如果可以點擊,則需要處理 Hotspot
					// 這個效果是在5.0以後出現,主要是處理 RippleDrawable 的效果
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

        			// 如果移出了 view 的範圍,則需要重置狀態
                    if (!pointInView(x, y, mTouchSlop)) {
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

        return false;
    }

上述代碼中,有兩處 callback 的邏輯你可能還沒有完全明白,一個是 TapCallback,一個是 LongPressCallback,分別對應 mPendingCheckForTap 和 mPendingCheckForLongPress,看下完整的代碼。

// 代碼段1,類 CheckForTap  
// run 內實際上是調用 代碼段5
   private final class CheckForTap implements Runnable {
       public float x;
       public float y;

       @Override
       public void run() {
           mPrivateFlags &= ~PFLAG_PREPRESSED;
           setPressed(true, x, y);
           checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
       }
   }

// 代碼段2,延遲發送 mPendingCheckForTap 
// ViewConfiguration.getTapTimeout() == 100 ms
   if (mPendingCheckForTap == null) {
       mPendingCheckForTap = new CheckForTap();
   }
   mPendingCheckForTap.x = event.getX();
   mPendingCheckForTap.y = event.getY();
   postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

// 代碼段3,移出 mPendingCheckForTap 對象
   private void removeTapCallback() {
       if (mPendingCheckForTap != null) {
           mPrivateFlags &= ~PFLAG_PREPRESSED;
           removeCallbacks(mPendingCheckForTap);
       }
   }

// 代碼段4,類 CheckForLongPress 
// run 內會判斷狀態並簡介調用 view.OnLongClickListener#onLongClick
   private final class CheckForLongPress implements Runnable {
       private int mOriginalWindowAttachCount;
       private float mX;
       private float mY;
       private boolean mOriginalPressedState;

       @Override
       public void run() {
           if ((mOriginalPressedState == isPressed()) && (mParent != null)
                   && mOriginalWindowAttachCount == mWindowAttachCount) {
               if (performLongClick(mX, mY)) {
                   mHasPerformedLongPress = true;
               }
           }
       }
	//...
   }

// 代碼段5,延遲檢測發送處理長按行爲
// ViewConfiguration.getLongPressTimeout() == 500 ms
   private void checkForLongClick(int delayOffset, float x, float y) {
       if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
           mHasPerformedLongPress = false;

           if (mPendingCheckForLongPress == null) {
               mPendingCheckForLongPress = new CheckForLongPress();
           }
           mPendingCheckForLongPress.setAnchor(x, y);
           mPendingCheckForLongPress.rememberWindowAttachCount();
           mPendingCheckForLongPress.rememberPressedState();
           postDelayed(mPendingCheckForLongPress,
                   ViewConfiguration.getLongPressTimeout() - delayOffset);
       }
   }

// 代碼段6,移出 mPendingCheckForTap 對象
   private void removeLongPressCallback() {
       if (mPendingCheckForLongPress != null) {
           removeCallbacks(mPendingCheckForLongPress);
       }
   }

代碼段1 會調用 代碼段5,實際上也是延遲發送 “處理長按行爲”。和直接調用 代碼5 不同,代碼5 中延遲 500-delayOffset ms 執行 “處理長按行爲”,而源碼的調用基本都是默認 delayOffset = 0 。代碼1 則以 delayOffset = 100 先延遲 100 ms之後延遲 400 ms 發送處理長按行爲,同樣需要 500 ms 纔會支持 “處理長按行爲”。那到底爲啥要這麼做呢?
原因是當前處理的 view 位於可滑動的容器內需要延遲處理接收的按壓事件。這樣講有點抽象,你可以這樣理解,android 把 MotionEvent.ACTION_DOWN 場景區分爲 滑動(scroll) 和 輕敲(tap),用延遲的時間來判斷手勢已經發生了位移。如果發生了位移,則還依然需要保持判斷有效長按時間(500 ms)不變,所以會追加 400 ms延遲來 post 一個 “處理長按行爲” 任務。

上述6個代碼段用於加深理解 onTouchEvent 內事件的處理而已。從上上段代碼上看,我們總結下整個流程:

  • 如果 view 不可用則根據是否可點擊來直接消費 MotionEvent.ACTION_UP
  • 如果 view 設置了 mTouchDelegate,則默認消費 Touch 事件
  • 如果 view 可點擊或者在 tooltip 顯示狀態下默認消費事件,否則返回 false 給 dispatchTouchEvent
    • MotionEvent.ACTION_UP 分支會設置按壓狀態,觸發點擊或長按事件,最後重置狀態  
    • MotionEvent.ACTION_DOWN 分支延遲發送“處理長按行爲”  
    • MotionEvent.ACTION_CANCEL 分支重置處理 Touch 過程中設置的狀態  
    • MotionEvent.ACTION_MOVE 分支處理滑動 RippleDrawable 效果並在手勢滑出 View 範圍情況下重置狀態

總結: onTouchEvent 是真正完成對 Touch 事件的處理,並把處理結果作爲dispatchTouchEvent的遞歸結果。

5個案例加強理解

GitHub鏈接上有本次 Touch傳遞測試代碼

測試案例兩個 viewGroup 和 一個 view

  • 場景一:View3#onTouchEvent 返回 true 消費所有事件,上層不攔截。

場景一可知:View3 消費所有事件並返回 true,對於上層下發的任何事件,dispatchTouchEvent都返回 true

  • 場景二:View3#onTouchEvent 返回 false 不消費 Touch 事件,上層不攔截,Linearlayout2 返回 true 消費所有 Touch 事件。

場景二可知:如果末層不消費所有事件,則 ACTION_DOWN 會開始從末層向上傳遞。Linearlayout2消費了ACTION_DOWN之後,其及上層dispatchTouchEvent 都返回 trueACTION_DOWN 之後的事件序列(如ACTION_MOVE,ACTION_UP)都會往Linearlayout2分發,其下層就再也收不到後續事件了。

  • 場景三:Linearlayout2#onInterceptTouchEvent 返回 true 攔截 Touch 事件,但是 Linearlayout2#onTouchEvent 返回 false 不消費事件。

場景三可知:Linearlayout2 攔截了 ACTION_DOWN 之後,其子 View 再也收不到任何事件,其消費結果由 onTouchEvent 決定,如果不消費,則往上層傳,直到找到某層消費事件。如果沒有任何一層消費,則後續事件序列也不會下發了。

  • 場景四:Linearlayout2#onInterceptTouchEvent 返回 true 攔截 Touch 事件,但是 Linearlayout2#onTouchEvent 返回 true 消費事件。

場景四可知:Linearlayout2 攔截了 ACTION_DOWN 之後,其子 View 再也收不到任何事件,如果消費 ACTION_DOWN,則後續事件序列都往Linearlayout2 下發。

  • 場景五:View3#onTouchEvent 返回 true 消費所有除 ACTION_CANCEL 事件,但是 Linearlayout2#onInterceptTouchEvent 攔截了 ACTION_MOVE事件且不消費任何事件。

場景五可知:ACTION_DOWN傳遞到 View3被其消費,後續序列事件本應該傳遞到 View3。當 ACTION_MOVE 被 Linearlayout2攔截之後,無論是否消費,View3再也收不到 ACTION_MOVE 及其後續的事件序列,但是會在事件被一次攔截時收到 ACTION_CANCEL,是否消費 ACTION_CANCEL 的結果會被當做此次傳遞的結果返回。此後,此次ACTION_MOVE後續的事件序列往 Linearlayout2 下發。

2張流程圖看懂沒?

 

 

出處: yummyLau原文鏈接(https://yummylau.com/2018/03/05/源碼解析_2018-03-05_Touch事件源碼解析/)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章