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

onTouchEvent會遲到,有時也會缺席
- 如果設(shè)置了OnTouchListener,那么在執(zhí)行過程中會先執(zhí)行OnTouchListener的onTouch方法,接著根據(jù)返回值來確定是否需要執(zhí)行onTouchEvent方法。
- onTouchEvent是否需要調(diào)用是和result的值有關(guān):如果result為true,則不調(diào)用;反之,則調(diào)用。
所以說:onTouchEvent會遲到,有時也會缺席。不過缺席的時候并不是我們關(guān)心的,我們需要看下正常流程中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仍然可以接收觸摸事件,僅僅是不響應(yīng)他們
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) {
// 抬起時進行的操作,這里面有點擊事件的調(diào)用
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.
// 如果我們沒有獲得焦點,那么我們要根據(jù)觸摸模式來判斷是否可以獲得焦點
// 這里問題比較多,后面會說
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.
// 按下按鈕之前,我們已經(jīng)釋放按鈕。 現(xiàn)在顯示按下的狀態(tài)(調(diào)度點擊之前),以確保用戶看到它。
setPressed(true, x, y);
}
// 如果沒有執(zhí)行長按事件并且不忽略下次抬起事件
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已經(jīng)獲得焦點或者觸摸模式為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執(zhí)行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在正在滾動的容器中,那么延遲發(fā)送這條消息
// 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
// 不在正在滾動的容器中,直接設(shè)置按下,并檢查長按事件
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;
}
上面的代碼去除注釋,去除移動和取消動作,真正的代碼并不多:
- 判斷View是否不可用:如果不可用,那么onTouchEvent返回值是否可點擊(clickable )
- 如果View可以點擊或者有TOOLTIP標志位的話,則進行對事件的不同動作的處理。
ACTION_DOWN:主要包括了setPressed和checkForLongClick兩個操作:
-
setPressed用于設(shè)置按下狀態(tài),此時PFLAG_PRESSED標志位被設(shè)置。 -
checkForLongClick用于檢查LongClick是否可以觸發(fā),以及發(fā)送延遲消息來響應(yīng)長按事件。
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();
// 延遲執(zhí)行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,那么設(shè)置mHasPerformedLongPress為true
// 表示已經(jīng)執(zhí)行了長按事件,并且返回值為true
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
public void setAnchor(float x, float y) {
mX = x;
mY = y;
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}
// 執(zhí)行長按事件
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
// 調(diào)用performLongClick()方法
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
public boolean performLongClick() {
// 繼續(xù)調(diào)用
return performLongClickInternal(mLongClickX, mLongClickY);
}
// 真正執(zhí)行長按方法
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
// ListenerInfo中mOnLongClickListener屬性是否不為空,不為空則執(zhí)行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;
}
執(zhí)行長按過程如下:
- 判斷是否可以長按,可以的話進行下面操作。這里如果先設(shè)置不可長按又設(shè)置
OnLongClickListener的話,此時長按事件仍有效。但是,如果順序顛倒下的話,就長按事件就無效了。 - 如果可以長按,那么通過
Handler將CheckForLongPress延遲發(fā)送,時間是時間默認值DEFAULT_LONG_PRESS_TIMEOUT = 500ms。 -
CheckForLongPress在其run方法中會根據(jù)performLongClick方法的返回值來設(shè)置mHasPerformedLongPress變量的值,這個變量的值在后面會用到,這里先不說。 - 接著會一路調(diào)用最終從
ListenerInfo中獲得OnLongClickListener,如果不為null,則執(zhí)行其onLongClick方法。
ACTION_UP:
- 不可點擊,則需要把callBack移除
- 可以點擊的話,通過是否可以點擊(
clickable)、長按事件的返回值(mHasPerformedLongPress)、是否忽略下次抬起(mIgnoreNextUpEvent)以及焦點是否拿到(focusTaken)這四個值來判斷可否執(zhí)行click事件。一般來說,大部分博客都會直接分析performClick過程,很少會提到為什么這個條件會成立。我這邊深究一下,看下到底為什么能夠執(zhí)行performClick操作:
2.1.prepressed的值(mPrivateFlags & PFLAG_PREPRESSED) != 0,這個可以在ACTION_DOWN中可以看到賦值,但賦值的情況是在正在滾動的容器中。
2.2(mPrivateFlags & PFLAG_PRESSED)此處PFLAG_PRESSED賦值同樣也是在ACTION_DOWN中賦值,與prepressed相反,此時View不在正在滾動的容器中。
2.3focusTaken的值,這個值涉及的東西有點多。首先判斷條件isFocusable() && isFocusableInTouchMode() && !isFocused():isFocusable()一般為true;isFocusableInTouchMode()如果不設(shè)置setFocusableInTouchMode(true)的話,默認為false;isFocused()這個值需要注意下,此值意思是是否擁有焦點,但是我們可以看到判斷條件為!isFocused(),所以如果前面條件都為true的情況下,若此時isFocused()返回true,那么將不會再次請求焦點,因為此時已經(jīng)擁有焦點,否則,則會調(diào)用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標志位
// 這里出現(xiàn)一個常用的變量FOCUS_BLOCK_DESCENDANTS,這里是關(guān)于焦點設(shè)置,后面有代碼分析
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) {
// 如果沒有獲取到焦點,則設(shè)置獲取焦點
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,那么根據(jù)后面的判斷條件是不會執(zhí)行performClick操作的。這里可以假設(shè)有如下代碼:
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");
}
});
......
}
那么按照上面的分析此時第一次點擊的時候應(yīng)該會去請求焦點的,此時點擊事件不會生效。但,真的會這樣嗎?不會的,最初我以為也是這樣,但是經(jīng)過測試發(fā)現(xiàn):View在設(shè)置成可見(VISIBLE)是,會調(diào)用mParent.focusableViewAvailable(this);方法。在之前從源碼分析Activity啟動時的生命周期有源碼decor.setVisibility(View.INVISIBLE),之后會調(diào)用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;
// 如果新的狀態(tài)是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);
}
}
}
......
}
這里偷懶了,把無關(guān)代碼省去了,我們可以看到如果設(shè)置的狀態(tài)和以前不一致的話需要重新根據(jù)狀態(tài)執(zhí)行不同過程。我們這里設(shè)置的是可見,所以會執(zhí)行mParent.focusableViewAvailable(this);方法:
ViewRootImpl.java:
@Override
public void focusableViewAvailable(View v) {
checkThread();
if (mView != null) {
// mView是我們的DecorView,我們并沒有設(shè)置其獲取焦點
if (!mView.hasFocus()) {
if (sAlwaysAssignFocus) {
// 調(diào)用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會調(diào)用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:
// 直接去調(diào)用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獲取焦點時需要根據(jù)descendantFocusability的值來判斷,這里descendantFocusability可能出現(xiàn)三個值:
- FOCUS_BLOCK_DESCENDANTS:父View直接請求獲取焦點。
- FOCUS_BEFORE_DESCENDANTS:父View會優(yōu)先其子View,請求獲取焦點,如果沒有獲取到焦點,則會讓其子View請求獲取焦點。
- FOCUS_AFTER_DESCENDANTS:與FOCUS_BEFORE_DESCENDANTS相反,子View會先請求獲取焦點,如果獲取到焦點,那么父View不會請求獲取焦點。
默認情況ViewGroup在初始化的時候設(shè)置為FOCUS_BEFORE_DESCENDANTS,但是DecorView設(shè)置為FOCUS_AFTER_DESCENDANTS

好了,到這里我們知道為什么剛才的代碼可以執(zhí)行點擊事件了。不過,如果改成下面的代碼執(zhí)行結(jié)果需要自己試試了:

此時,點擊事件執(zhí)行會出現(xiàn):View1獲取焦點后,點擊正常,點擊View2,無反應(yīng),再次點擊事件正常。
我們接著分析繼續(xù)來:
這次我們知道條件成立后會通過
Handler發(fā)送PerformClick對象,如果發(fā)送成功,則執(zhí)行PerformClick.run方法,否則執(zhí)行performClick()方法(PerformClick.run也調(diào)用了performClick方法),最終執(zhí)行OnClickListener的onClick方法。
總結(jié)
寫了這么多,還是來個總結(jié)吧:
- 在按下的時候,如果長按事件執(zhí)行了,并且返回值為false,那么此時點擊事件不會執(zhí)行;反之則會執(zhí)行點擊事件。
-
關(guān)于ViewGroup和子View獲取焦點的先后順序,根據(jù)
descendantFocusability的值來判斷:
FOCUS_BLOCK_DESCENDANTS:父View直接請求獲取焦點。
FOCUS_BEFORE_DESCENDANTS:父View會優(yōu)先其子View,請求獲取焦點,如果沒有獲取到焦點,則會讓其子View請求獲取焦點。
FOCUS_AFTER_DESCENDANTS:與FOCUS_BEFORE_DESCENDANTS相反,子View會先請求獲取焦點,如果獲取到焦點,那么父View不會請求獲取焦點。 - 如果同一個頁面中,有多個View都可以獲得焦點,那么只有當前獲取焦點的點擊事件可以正常執(zhí)行,其他View需要先點擊一次獲取焦點,之后可以正常執(zhí)行點擊事件。
OK,這篇分析到此結(jié)束了。如果有問題,請指出(估計也就是自己來發(fā)現(xiàn)問題吧)。
這里多說幾句:之前,我覺得許多博客分析的真的是頭頭是道,當輪到我去分析某些東西的時候再去看他們的博客卻發(fā)現(xiàn):好多東西都說的不明確,太模糊。所以,自己分析的時候盡量把所有過程全部分析完成,爭取步驟完整,方便以后自己回顧。
That's all!
