Android觸控事件(續)——點選長按事件

ksuu發表於2017-12-22

昨天寫完了Android觸控事件(下)——事件的分發,寫完後以為這一部分終將告一段落了。今早無意間突然想起,好像關於點選事件、長按事件這一部分並沒有分析啊!!垂死病中驚坐起,粗略的看了下原始碼,好像沒啥東西啊。仔細看看吧,發現有些地方真的是叫人頭疼。沒辦法,仔細看吧看吧。正是: 碼中自有顏如玉,碼中自有黃金屋。

Android觸控事件(續)——點選長按事件

onTouchEvent會遲到,有時也會缺席

Android觸控事件(下)——事件的分發中寫過:

  1. 如果設定了OnTouchListener,那麼在執行過程中會先執行OnTouchListener的onTouch方法,接著根據返回值來確定是否需要執行onTouchEvent方法。
  2. onTouchEvent是否需要呼叫是和result的值有關:如果result為true,則不呼叫;反之,則呼叫。

所以說:onTouchEvent會遲到,有時也會缺席。不過缺席的時候並不是我們關心的,我們需要看下正常流程中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();

	// 判斷是否可以點選
	final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
			|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
			|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
	// View是否不可用:如果不可用,返回值是是否可點選
	// 註釋:不可用的但是可點選的View仍然可以接收觸控事件,僅僅是不響應他們
	if ((viewFlags & ENABLED_MASK) == DISABLED) {
		if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
			setPressed(false);
		}
		mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
		// A disabled view that is clickable still consumes the touch
		// events, it just doesn't respond to them.
		return clickable;
	}
	if (mTouchDelegate != null) {
		if (mTouchDelegate.onTouchEvent(event)) {
			return true;
		}
	}
	
	// 可點選或者有標誌位TOOLTIP
	if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
		switch (action) {
			// 抬起時進行的操作,這裡面有點選事件的呼叫
			case MotionEvent.ACTION_UP:
				mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
				if ((viewFlags & TOOLTIP) == TOOLTIP) {
					handleTooltipUp();
				}
				// 如果不可點選,則需要把callBack移除
				if (!clickable) {
					removeTapCallback();
					removeLongPressCallback();
					mInContextButtonPress = false;
					mHasPerformedLongPress = false;
					mIgnoreNextUpEvent = false;
					break;
				}
				// 這裡Prepressed也用於識別快速按下
				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
						// 如果當前View已經獲得焦點或者觸控模式為false
						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.
							// 通過performClick執行click事件
							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)) {
						// If the post failed, unpress right now
						mUnsetPressedState.run();
					}

					removeTapCallback();
				}
				mIgnoreNextUpEvent = false;
				break;

			case MotionEvent.ACTION_DOWN:
				if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
					mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
				}
				mHasPerformedLongPress = false;
				// 如果不可點選,那麼檢查長按事件
				if (!clickable) {
					checkForLongClick(0, x, y);
					break;
				}

				if (performButtonActionOnTouchDown(event)) {
					break;
				}

				// Walk up the hierarchy to determine if we're inside a scrolling container.
				boolean isInScrollingContainer = isInScrollingContainer();
				// 如果View在正在滾動的容器中,那麼延遲傳送這條訊息
				// 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, x, y);
				}
				break;
	
			// 取消,移除callBack
			case MotionEvent.ACTION_CANCEL:
				if (clickable) {
					setPressed(false);
				}
				removeTapCallback();
				removeLongPressCallback();
				mInContextButtonPress = false;
				mHasPerformedLongPress = false;
				mIgnoreNextUpEvent = false;
				mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
				break;
			// 移動
			case MotionEvent.ACTION_MOVE:
				if (clickable) {
					drawableHotspotChanged(x, y);
				}

				// Be lenient about moving outside of buttons
				if (!pointInView(x, y, mTouchSlop)) {
					// Outside button
					// Remove any future long press/tap checks
					removeTapCallback();
					removeLongPressCallback();
					if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
						setPressed(false);
					}
					mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
				}
				break;
		}

		return true;
	}

	return false;
}
複製程式碼

上面的程式碼去除註釋,去除移動和取消動作,真正的程式碼並不多:

  1. 判斷View是否不可用:如果不可用,那麼onTouchEvent返回值是否可點選(clickable )
  2. 如果View可以點選或者有TOOLTIP標誌位的話,則進行對事件的不同動作的處理。

ACTION_DOWN:主要包括了setPressedcheckForLongClick兩個操作:

  • setPressed用於設定按下狀態,此時PFLAG_PRESSED標誌位被設定。
  • checkForLongClick用於檢查LongClick是否可以觸發,以及傳送延遲訊息來響應長按事件。
private void checkForLongClick(int delayOffset, float x, float y) {
	// 如果可以長按
	if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
		mHasPerformedLongPress = false;

		if (mPendingCheckForLongPress == null) {
			mPendingCheckForLongPress = new CheckForLongPress();
		}
		mPendingCheckForLongPress.setAnchor(x, y);
		mPendingCheckForLongPress.rememberWindowAttachCount();
		// 延遲執行CheckForLongPress操作,時間預設值 DEFAULT_LONG_PRESS_TIMEOUT = 500ms
		postDelayed(mPendingCheckForLongPress,
				ViewConfiguration.getLongPressTimeout() - delayOffset);
	}
}

private final class CheckForLongPress implements Runnable {
	private int mOriginalWindowAttachCount;
	private float mX;
	private float mY;

	@Override
	public void run() {
		if (isPressed() && (mParent != null)
				&& mOriginalWindowAttachCount == mWindowAttachCount) {
			// 如果長按事件返回值true,那麼設定mHasPerformedLongPress為true
			// 表示已經執行了長按事件,並且返回值為true
			if (performLongClick(mX, mY)) {
				mHasPerformedLongPress = true;
			}
		}
	}

	public void setAnchor(float x, float y) {
		mX = x;
		mY = y;
	}

	public void rememberWindowAttachCount() {
		mOriginalWindowAttachCount = mWindowAttachCount;
	}
}

// 執行長按事件
public boolean performLongClick(float x, float y) {
	mLongClickX = x;
	mLongClickY = y;
	// 呼叫performLongClick()方法
	final boolean handled = performLongClick();
	mLongClickX = Float.NaN;
	mLongClickY = Float.NaN;
	return handled;
}

public boolean performLongClick() {
	// 繼續呼叫
	return performLongClickInternal(mLongClickX, mLongClickY);
}

// 真正執行長按方法	
private boolean performLongClickInternal(float x, float y) {
	sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

	boolean handled = false;
	// ListenerInfo中mOnLongClickListener屬性是否不為空,不為空則執行onLongClick操作,並將返回值賦給handled
	final ListenerInfo li = mListenerInfo;
	if (li != null && li.mOnLongClickListener != null) {
		handled = li.mOnLongClickListener.onLongClick(View.this);
	}
	if (!handled) {
		final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
		handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
	}
	if (handled) {
		performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
	}
	return handled;
}
複製程式碼

執行長按過程如下:

  • 判斷是否可以長按,可以的話進行下面操作。這裡如果先設定不可長按又設定OnLongClickListener的話,此時長按事件仍有效。但是,如果順序顛倒下的話,就長按事件就無效了。
  • 如果可以長按,那麼通過HandlerCheckForLongPress延遲傳送,時間是時間預設值 DEFAULT_LONG_PRESS_TIMEOUT = 500ms
  • CheckForLongPress在其run方法中會根據performLongClick方法的返回值來設定mHasPerformedLongPress變數的值,這個變數的值在後面會用到,這裡先不說。
  • 接著會一路呼叫最終從ListenerInfo中獲得OnLongClickListener,如果不為null,則執行其onLongClick方法。

ACTION_UP:

  1. 不可點選,則需要把callBack移除
  2. 可以點選的話,通過是否可以點選(clickable)、長按事件的返回值(mHasPerformedLongPress)、是否忽略下次抬起(mIgnoreNextUpEvent)以及焦點是否拿到(focusTaken )這四個值來判斷可否執行click事件。一般來說,大部分部落格都會直接分析performClick過程,很少會提到為什麼這個條件會成立。我這邊深究一下,看下到底為什麼能夠執行performClick操作: 2.1. prepressed的值(mPrivateFlags & PFLAG_PREPRESSED) != 0,這個可以在ACTION_DOWN中可以看到賦值,但賦值的情況是在正在滾動的容器中。 2.2 (mPrivateFlags & PFLAG_PRESSED)此處PFLAG_PRESSED賦值同樣也是在ACTION_DOWN中賦值,與prepressed相反,此時View不在正在滾動的容器中。 2.3 focusTaken的值,這個值涉及的東西有點多。首先判斷條件isFocusable() && isFocusableInTouchMode() && !isFocused()isFocusable()一般為trueisFocusableInTouchMode()如果不設定setFocusableInTouchMode(true)的話,預設為falseisFocused()這個值需要注意下,此值意思是是否擁有焦點,但是我們可以看到判斷條件為!isFocused(),所以如果前面條件都為true的情況下,若此時isFocused()返回true,那麼將不會再次請求焦點,因為此時已經擁有焦點,否則,則會呼叫requestFocus獲取焦點,並將返回值賦給focusTaken。 還是來看下這邊的程式碼吧,挺重要的:
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
	// need to be focusable
	if ((mViewFlags & FOCUSABLE) != FOCUSABLE
			|| (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
		return false;
	}

	// need to be focusable in touch mode if in touch mode
	if (isInTouchMode() &&
		(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
		   return false;
	}

	// need to not have any parents blocking us
	if (hasAncestorThatBlocksDescendantFocus()) {
		return false;
	}

	handleFocusGainInternal(direction, previouslyFocusedRect);
	return true;
}

private boolean hasAncestorThatBlocksDescendantFocus() {
	final boolean focusableInTouchMode = isFocusableInTouchMode();
	ViewParent ancestor = mParent;
	// 查詢View的父View,看其是否有FOCUS_BLOCK_DESCENDANTS標誌位
	// 這裡出現一個常用的變數FOCUS_BLOCK_DESCENDANTS,這裡是關於焦點設定,後面有程式碼分析
	while (ancestor instanceof ViewGroup) {
		final ViewGroup vgAncestor = (ViewGroup) ancestor;
		if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
				|| (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {
			return true;
		} else {
			ancestor = vgAncestor.getParent();
		}
	}
	return false;
}

void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
	// 如果沒有獲取到焦點,則設定獲取焦點
	if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
		mPrivateFlags |= PFLAG_FOCUSED;

		View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

		if (mParent != null) {
			mParent.requestChildFocus(this, this);
			updateFocusedInCluster(oldFocus, direction);
		}

		if (mAttachInfo != null) {
			mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
		}

		onFocusChanged(true, direction, previouslyFocusedRect);
		refreshDrawableState();
	}
}
複製程式碼

上面的程式碼很容易理解,為什麼需要單獨看一下呢?首先來說,如果去請求獲取焦點的話,真正獲取成功後此時返回值為true,那麼根據後面的判斷條件是不會執行performClick操作的。這裡可以假設有如下程式碼:

protected void onCreate(Bundle savedInstanceState) {
        ......
	View view2 = findViewById(R.id.v_2);
	view2.setFocusable(true);
	view2.setFocusableInTouchMode(true);
	view2.setOnClickListener(new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			Log.d("fxxk", "view2 onClick");
		}
	});
        ......
}
複製程式碼

那麼按照上面的分析此時第一次點選的時候應該會去請求焦點的,此時點選事件不會生效。但,真的會這樣嗎?不會的,最初我以為也是這樣,但是經過測試發現:View在設定成可見(VISIBLE)是,會呼叫mParent.focusableViewAvailable(this);方法。在之前從原始碼分析Activity啟動時的生命週期有原始碼decor.setVisibility(View.INVISIBLE),之後會呼叫Activity.makeVisible()mDecor.setVisibility(View.VISIBLE);,這時候我們看下setVisibility方法:

@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
	setFlags(visibility, VISIBILITY_MASK);
}

void setFlags(int flags, int mask) {
	final boolean accessibilityEnabled =
			AccessibilityManager.getInstance(mContext).isEnabled();
	final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();

	int old = mViewFlags;
	mViewFlags = (mViewFlags & ~mask) | (flags & mask);

	int changed = mViewFlags ^ old;
	if (changed == 0) {
		return;
	}
	......

	final int newVisibility = flags & VISIBILITY_MASK;
	// 如果新的狀態是VISIBLE
	if (newVisibility == VISIBLE) {
		// 如果有改變
		if ((changed & VISIBILITY_MASK) != 0) {
			/*
			 * If this view is becoming visible, invalidate it in case it changed while
			 * it was not visible. Marking it drawn ensures that the invalidation will
			 * go through.
			 */
			mPrivateFlags |= PFLAG_DRAWN;
			invalidate(true);

			needGlobalAttributesUpdate(true);

			// a view becoming visible is worth notifying the parent
			// about in case nothing has focus.  even if this specific view
			// isn't focusable, it may contain something that is, so let
			// the root view try to give this focus if nothing else does.
			// 這裡的DecorView的Parent是ViewRootImpl
			if ((mParent != null)) {
				// ViewRootImpl的方法
				mParent.focusableViewAvailable(this);
			}
		}
	}

	......
}
複製程式碼

這裡偷懶了,把無關程式碼省去了,我們可以看到如果設定的狀態和以前不一致的話需要重新根據狀態執行不同過程。我們這裡設定的是可見,所以會執行mParent.focusableViewAvailable(this);方法:

ViewRootImpl.java:
@Override
public void focusableViewAvailable(View v) {
	checkThread();
	if (mView != null) {
		// mView是我們的DecorView,我們並沒有設定其獲取焦點
		if (!mView.hasFocus()) {
			if (sAlwaysAssignFocus) {
				// 呼叫DecorView的requestFocus方法
				v.requestFocus();
			}
		} else {
			// the one case where will transfer focus away from the current one
			// is if the current view is a view group that prefers to give focus
			// to its children first AND the view is a descendant of it.
			View focused = mView.findFocus();
			if (focused instanceof ViewGroup) {
				ViewGroup group = (ViewGroup) focused;
				if (group.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS
						&& isViewDescendantOf(v, focused)) {
					v.requestFocus();
				}
			}
		}
	}
}
requestFocus會呼叫requestFocus(int direction, Rect previouslyFocusedRect)方法,在ViewGroup中重寫,這裡著重看下。
ViewGroup.java:
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
	int descendantFocusability = getDescendantFocusability();

	switch (descendantFocusability) {
		case FOCUS_BLOCK_DESCENDANTS:
			// 直接去呼叫View的requestFocus,不管子View
			return super.requestFocus(direction, previouslyFocusedRect);
		case FOCUS_BEFORE_DESCENDANTS: {
			// 先於子View請求獲取焦點,如果自身獲取焦點成功,子View不會請求獲取焦點
			final boolean took = super.requestFocus(direction, previouslyFocusedRect);
			return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
		}
		case FOCUS_AFTER_DESCENDANTS: {
			// 先讓子View請求獲取焦點,如果子View獲取焦點成功,那麼父View不會請求獲取焦點
			final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
			return took ? took : super.requestFocus(direction, previouslyFocusedRect);
		}
		default:
			throw new IllegalStateException("descendant focusability must be "
					+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
					+ "but is " + descendantFocusability);
	}
}
ViewGroup.java:
protected boolean onRequestFocusInDescendants(int direction,
		Rect previouslyFocusedRect) {
	int index;
	int increment;
	int end;
	int count = mChildrenCount;
	if ((direction & FOCUS_FORWARD) != 0) {
		index = 0;
		increment = 1;
		end = count;
	} else {
		index = count - 1;
		increment = -1;
		end = -1;
	}
	final View[] children = mChildren;
	for (int i = index; i != end; i += increment) {
		View child = children[i];
		// 只對可見的View請求獲取焦點,並且一旦有View獲取焦點則不會讓其他View請求獲取焦點
		if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
			if (child.requestFocus(direction, previouslyFocusedRect)) {
				return true;
			}
		}
	}
	return false;
}
複製程式碼

ViewGroup獲取焦點時需要根據descendantFocusability的值來判斷,這裡descendantFocusability可能出現三個值:

  1. FOCUS_BLOCK_DESCENDANTS:父View直接請求獲取焦點。
  2. FOCUS_BEFORE_DESCENDANTS:父View會優先其子View,請求獲取焦點,如果沒有獲取到焦點,則會讓其子View請求獲取焦點。
  3. FOCUS_AFTER_DESCENDANTS:與FOCUS_BEFORE_DESCENDANTS相反,子View會先請求獲取焦點,如果獲取到焦點,那麼父View不會請求獲取焦點。

預設情況ViewGroup在初始化的時候設定為FOCUS_BEFORE_DESCENDANTS,但是DecorView設定為FOCUS_AFTER_DESCENDANTS

Android觸控事件(續)——點選長按事件
好了,到這裡我們知道為什麼剛才的程式碼可以執行點選事件了。不過,如果改成下面的程式碼執行結果需要自己試試了:
Android觸控事件(續)——點選長按事件
此時,點選事件執行會出現:View1獲取焦點後,點選正常,點選View2,無反應,再次點選事件正常。 我們接著分析繼續來: 這次我們知道條件成立後會通過Handler傳送PerformClick物件,如果傳送成功,則執行PerformClick.run方法,否則執行performClick()方法(PerformClick.run也呼叫了performClick方法),最終執行OnClickListeneronClick方法。
performClick

總結

寫了這麼多,還是來個總結吧:

  1. 在按下的時候,如果長按事件執行了,並且返回值為false,那麼此時點選事件不會執行;反之則會執行點選事件。
  2. 關於ViewGroup和子View獲取焦點的先後順序,根據descendantFocusability的值來判斷: FOCUS_BLOCK_DESCENDANTS:父View直接請求獲取焦點。 FOCUS_BEFORE_DESCENDANTS:父View會優先其子View,請求獲取焦點,如果沒有獲取到焦點,則會讓其子View請求獲取焦點。 FOCUS_AFTER_DESCENDANTS:與FOCUS_BEFORE_DESCENDANTS相反,子View會先請求獲取焦點,如果獲取到焦點,那麼父View不會請求獲取焦點。
  3. 如果同一個頁面中,有多個View都可以獲得焦點,那麼只有當前獲取焦點的點選事件可以正常執行,其他View需要先點選一次獲取焦點,之後可以正常執行點選事件。

OK,這篇分析到此結束了。如果有問題,請指出(估計也就是自己來發現問題吧)。 這裡多說幾句:之前,我覺得許多部落格分析的真的是頭頭是道,當輪到我去分析某些東西的時候再去看他們的部落格卻發現:好多東西都說的不明確,太模糊。所以,自己分析的時候儘量把所有過程全部分析完成,爭取步驟完整,方便以後自己回顧。 That's all!

Android觸控事件(續)——點選長按事件

相關文章