本文適用于對Android事件分發(fā)機制有一定基礎(chǔ)的開發(fā)者閱讀,主要是通過對View類中的事件分發(fā)玫锋、事件消費方法的源代碼進行解析以達到完全理解其原理的目的
- (一)Android事件分發(fā)機制 - View篇
- (二)Android事件分發(fā)機制 - ViewGroup篇
- (三)Android事件分發(fā)機制 - Activity篇
- (四)Android事件分發(fā)機制 - 總結(jié)篇
我們知道View中包含dispatchTouchEvent
和onTouchEvent
方法挠乳,接下來我們通過源代碼(基于Android6.0)看看這些方法內(nèi)部到底做了哪些事情权薯。
1、View#dispatchTouchEvent源碼解析
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
// 當(dāng)前View是否可見(未被其他窗口遮蓋住且未隱藏)
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
從上面看出:
- 只有以下四個條件都為真睡扬,dispatchTouchEvent()才返回true盟蚣;否則執(zhí)行onTouchEvent(event)方法
第一個條件:li! = null
第二個條件:li.mOnTouchListener != null;
第三個條件:(mViewFlags & ENABLED_MASK) == ENABLED卖怜;
第四個條件:li.mOnTouchListener.onTouch(this, event)屎开;
- 下面我們來看下這四個判斷條件:
第一個條件:li! = null
- li是ListenerInfo類對象,而ListenerInfo類作用是保存點擊马靠、長按點擊奄抽、上下文點擊等監(jiān)聽listener用以在需要的時候進行回調(diào);
- 只要我們注冊了監(jiān)聽事件如下文的mOnTouchListener甩鳄,那么li就一定不為null逞度。
第二個條件:li.mOnTouchListener != null
// mOnTouchListener是在View類的setOnTouchListener方法里賦值的
public void setOnTouchListener(OnTouchListener l) {
// 只要我們給控件注冊了Touch事件,mOnTouchListener就一定被賦值(不為空)
getListenerInfo().mOnTouchListener = l;
}
第三個條件:(mViewFlags & ENABLED_MASK) == ENABLED
- 該條件是判斷當(dāng)前點擊的控件是否enable娩贷;
- 由于很多View默認(rèn)是enable的第晰,因此該條件恒定為true,當(dāng)然我們也可以調(diào)用View#setEnabled()方法來改變此值彬祖。
第四個條件:mOnTouchListener.onTouch(this, event)
回調(diào)注冊的Touch事件的onTouch方法:
- 如果返回true茁瘦,就會讓上述四個條件全部成立,從而返回true储笑;
- 如果返回false甜熔,就會去執(zhí)行onTouchEvent(event)方法。
onTouch和onTouchEvent的區(qū)別
-
onTouch
方法優(yōu)先于onTouchEvent
方法執(zhí)行突倍; - 如果
onTouch
方法返回true
腔稀,onTouchEvent
將不會再執(zhí)行盆昙。
2、View#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();
// 如果當(dāng)前View是DISABLED狀態(tài)且是可點擊/可長按則會消費掉事件焊虏,不讓它繼續(xù)傳遞
if ((viewFlags & ENABLED_MASK) == DISABLED) {
...
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
// 如果設(shè)置了mTouchDelegate淡喜,則會將事件交給代理者處理,直接return true诵闭,如果大家希望自己的View增加它的touch范圍炼团,可以嘗試使用TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) { // / 如果有TouchDelegate的話,優(yōu)先交給它處理
return true; // 處理成功返回true疏尿,否則接著往下走
}
}
// 如果view可點擊/可長按瘟芝,則最終一定return true
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP: // 抬起操作
...
break;
case MotionEvent.ACTION_DOWN: // 按下操作
...
break;
case MotionEvent.ACTION_CANCEL: // 取消操作
...
break;
}
return true;
}
return false;
}
MotionEvent.ACTION_DOWN
switch (action) {
...
case MotionEvent.ACTION_DOWN:
// 設(shè)置mHasPerformedLongPress = false 表示長按事件還未觸發(fā);
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// 判斷當(dāng)前View是否在滑動控件里面
boolean isInScrollingContainer = isInScrollingContainer();
// 如果當(dāng)前View在一個可滑動的父View中褥琐,我們觸摸它時需要延遲一小段時間用于判斷是否為屏幕滾動事件
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED; // 給mPrivateFlags設(shè)置一個PREPRESSED的標(biāo)識
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
// 發(fā)送一個延遲為100ms的點擊(非滑動)延遲消息锌俱,到達延時時間后會執(zhí)行CheckForTap()里面的run方法
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
...
}
CheckForTap
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED; // 取消mPrivateFlags的PREPRESSED
setPressed(true, x, y); // 設(shè)置PRESSED標(biāo)識為true,刷新背景
checkForLongClick(ViewConfiguration.getTapTimeout());
}
}
checkForLongClick
private void checkForLongClick(int delayOffset) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { // 支持長按
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
// 發(fā)送一個延遲消息敌呈,到達延遲時間后會執(zhí)行CheckForLongPress()里面的run方法
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
CheckForLongPress
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
@Override
public void run() {
// 用戶從DOWN觸發(fā)開始算起贸宏,向消息隊列插入的長按響應(yīng)消息如果在延遲時間內(nèi)沒有被取消則觸發(fā)長按
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) { // 根據(jù)長按的返回結(jié)果來設(shè)置mHasPerformedLongPress值
mHasPerformedLongPress = true;
}
}
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}
performLongClick
public boolean performLongClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) { // 長按返回結(jié)果為false - 不攔截事件
// ContextMenu是Android的context menu上下文菜單,比如EditeText就可以通過長按來彈出擁有“cut”,"copy","paste"等項的ContextMenu驱富。
// 是否彈出context menu上下文菜單 - 是則攔截事件
handled = showContextMenu();
}
if (handled) { // 如果彈出了context menu上下文菜單
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
簡單總結(jié)一下锚赤,ACTION_DOWN中都做什么:
- 將mHasPerformedLongPress置為false,mPrivateFlags置為PREPRESSED褐鸥,而后發(fā)出一個延時為100ms的點擊任務(wù)mPendingCheckForTap;
- 如果在100ms內(nèi)沒有觸發(fā)ACTION_UP赐稽,則執(zhí)行mPendingCheckForTap任務(wù):清除PREPRESSED標(biāo)志叫榕,并將mPrivateFlags設(shè)置為PRESSED,同時發(fā)出一個延時為 500ms - 100ms 的任務(wù)mPendingCheckForLongPress用于檢測是否為長按姊舵;
- 如果在500ms內(nèi)(從ACTION_DOWN觸發(fā)開始)沒有觸發(fā)ACTION_UP晰绎,則認(rèn)為是長按,此時執(zhí)行mPendingCheckForLongPress任務(wù):如果mOnLongClickListener不為null括丁,則執(zhí)行回調(diào)荞下,同時當(dāng)它的onLongClick方法返回true,才會把mHasPerformedLongPress設(shè)置為true史飞。
MotionEvent.ACTION_MOVE
switch (action) {
...
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// 判斷當(dāng)然觸摸點有沒有移出我們的View
if (!pointInView(x, y, mTouchSlop)) {
// 移除點按定時任務(wù) 標(biāo)志重置為0
removeTapCallback();
// 判斷是否包含PRESSED標(biāo)識尖昏,因為可能已經(jīng)超過100ms,此時mPrivateFlags標(biāo)志為PRESSED
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// 移除長按定時任務(wù)
removeLongPressCallback();
setPressed(false);
}
}
break;
...
}
-
如果觸摸點在View外构资,則執(zhí)行removeTapCallback
private void removeTapCallback() { if (mPendingCheckForTap != null) { mPrivateFlags &= ~PFLAG_PREPRESSED; // 重置標(biāo)志 // 移除消息隊列中在ACTION_DOWN時插入的mPendingCheckForTap消息 removeCallbacks(mPendingCheckForTap); } }
-
如果標(biāo)志為PRESSED抽诉,則執(zhí)行removeLongPressCallback
private void removeLongPressCallback() { if (mPendingCheckForLongPress != null) { // 移除消息隊列中在ACTION_DOWN時插入的mPendingCheckForLongPress消息 removeCallbacks(mPendingCheckForLongPress); } }
簡單總結(jié)一下,ACTION_MOVE中都做了什么:
檢測觸摸是否劃出了控件吐绵,如果劃出了:
- 100ms內(nèi)迹淌,直接移除mPendingCheckForTap河绽;
- 100ms后,則將標(biāo)志中的PRESSED去除唉窃,同時移除長按的檢查mPendingCheckForLongPress耙饰。
MotionEvent.ACTION_UP
switch (action) {
...
case MotionEvent.ACTION_UP:
// 判斷mPrivateFlags是否包含PREPRESSED
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
// 如果mPrivateFlags包含PRESSED或者PREPRESSED
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
// 沒有執(zhí)行長按操作
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除長按消息
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// 執(zhí)行點擊
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;
...
}
performClick
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK); // 聲音
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
最后總會進入執(zhí)行到UnsetPressedState
private final class UnsetPressedState implements Runnable {
@Override
public void run() {
setPressed(false); // 取消mPrivateFlags中的PRESSED標(biāo)志
}
}
setPressed方法
public void setPressed(boolean pressed) {
final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
if (pressed) {
mPrivateFlags |= PFLAG_PRESSED;
} else {
mPrivateFlags &= ~PFLAG_PRESSED;
}
if (needsRefresh) {
refreshDrawableState();
}
// 把setPress轉(zhuǎn)發(fā)下去給所有的子View
dispatchSetPressed(pressed);
}
簡單總結(jié)一下,ACTION_UP中都做了什么:
- 如果是在500ms內(nèi)觸發(fā)ACTION_UP纹份,即長按還未發(fā)生榔幸,則首先移除長按檢測,執(zhí)行onClick回調(diào)矮嫉;
- 如果是500ms以后削咆,那么有兩種情況:
1、設(shè)置了mOnLongClickListener蠢笋,且mOnLongClickListener.onLongClick返回 mHasPerformedLongPress = true拨齐,則點擊事件onClick事件無法觸發(fā);
2昨寞、未設(shè)置mOnLongClickListener或者mOnLongClickListener.onLongClick返回 mHasPerformedLongPress = false瞻惋,則點擊事件onClick事件可以觸發(fā)。 - 最后執(zhí)行mUnsetPressedState.run()援岩,將setPressed傳遞下去歼狼,可以在View中復(fù)寫dispatchSetPressed方法接收,然后將PRESSED標(biāo)識去除享怀。
setOnLongClickListener和setOnClickListener是否只能執(zhí)行一個
不是的羽峰,只要setOnLongClickListener
中的onLongClick
返回false
,則兩個都會執(zhí)行添瓷;返回true
則會屏蔽setOnClickListener
梅屉。
我們總結(jié)一下:
- 整個View的事件分發(fā)的流程是
dispatchEvent -> mOnTouchListener.onTouch -> onTouchEvent -> mOnClickListener.onClick
,也就是說鳞贷,我們平時調(diào)用的setOnClickListener事件坯汤,優(yōu)先級是最低的; - 如果在回調(diào)
onTouch()
里返回true
搀愧,則不執(zhí)行onTouchEvent()
方法惰聂,更不會執(zhí)行mOnClickListener.onClick()
方法,也表示View
消費了Touch
事件咱筛,返回false
則繼續(xù)執(zhí)行onTouchEvent()
方法搓幌; - 一個
clickable
或者longClickable
的View
會永遠(yuǎn)消費Touch
事件,不管他是enabled
還是disabled
的眷蚓; -
View
的長按事件是在onTouchEvent
的ACTION_DOWN
事件中執(zhí)行鼻种,要想執(zhí)行長按事件該View
必須是longClickable
的; -
View
的點擊事件是在onTouchEvent
的ACTION_UP
中執(zhí)行沙热,想要執(zhí)行點擊事件的前提是消費了ACTION_DOWN
和ACTION_MOVE
叉钥,并且沒有設(shè)置OnLongClickListener
罢缸,如設(shè)置了OnLongClickListener
,則必須使onLongClick()
方法返回false
投队。
結(jié)合下面這篇文章看更好理解哦:
Android觸摸屏事件派發(fā)機制詳解與源碼分析一(View篇)