詳細(xì)分析Android中onTouch事件傳遞機(jī)制
onTach介紹
ontach是Android系統(tǒng)中整個(gè)事件機(jī)制的基礎(chǔ)。Android中的其他事件,如onClick、onLongClick等都是以onTach為基礎(chǔ)的。
onTach包括從手指按下到離開(kāi)手機(jī)屏幕的整個(gè)過(guò)程,在微觀形式上,具體表現(xiàn)為action_down、action_move和action_up等過(guò)程。
onTach兩種主要定義形式如下:
1.在自定義控件中,常見(jiàn)的有重寫(xiě)onTouchEvent(MotionEvent ev)方法。如在開(kāi)發(fā)中經(jīng)??梢钥吹街貙?xiě)的onTouchEvent方法,
并且其中有針對(duì)不同的微觀表現(xiàn)(action_down、action_move和action_up等)做出的相應(yīng)判斷,執(zhí)行邏輯并可能返回不同的布爾值。
2.在代碼中,直接對(duì)現(xiàn)有控件設(shè)置setOnTouchListener監(jiān)聽(tīng)器。并重寫(xiě)監(jiān)聽(tīng)器的onTouch方法。onTouch回調(diào)函數(shù)中有view和MotionEvent
onTouch事件傳遞機(jī)制
大家都知道一般我們使用的UI控件都是繼承自共同的父類(lèi)——View。所以View這個(gè)類(lèi)應(yīng)該掌管著onTouch事件的相關(guān)處理。那就讓我們?nèi)タ纯矗涸赩iew中尋找Touch相關(guān)的方法,其中一個(gè)很容易地引起了我們的注意: dispatchTouchEvent(MotionEvent event) 。
根據(jù)方法名的意思應(yīng)該是負(fù)責(zé)分發(fā)觸摸事件的,下面給出了源碼:
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
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;
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;
}
源碼有點(diǎn)長(zhǎng),但我們不必每一行都看。首先注意到dispatchTouchEvent的返回值是boolean類(lèi)型的,注釋上的解釋:@return True if the event was handled by the view, false otherwise.也就是說(shuō)如果該觸摸事件被這個(gè)View消費(fèi)了就返回true,否則返回false。在方法中首先判斷了該event是否是否得到了焦點(diǎn),如果沒(méi)有得到焦點(diǎn)直接返回false。然后讓我們把目光轉(zhuǎn)向if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event))這個(gè)片段,看到這里有一個(gè)名為li的局部變量,屬于 ListenerInfo 類(lèi),經(jīng) mListenerInfo 賦值得到。ListenerInfo只是一個(gè)包裝類(lèi),里面封裝了大量的監(jiān)聽(tīng)器。
再在 View 類(lèi)中去尋找 mListenerInfo ,可以看到下面的代碼:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
因此我們可以知道m(xù)ListenerInfo是不為空的,所以li也不是空,第一個(gè)判斷為true,然后看到li.mOnTouchListener,前面說(shuō)過(guò)ListenerInfo是一個(gè)監(jiān)聽(tīng)器的封裝類(lèi),所以我們同樣去追蹤mOnTouchListener:
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
正是通過(guò)上面的方法來(lái)設(shè)置 mOnTouchListener 的,我想上面的方法大家肯定都很熟悉吧,正是我們平時(shí)經(jīng)常用的 xxx.setOnTouchListener ,好了我們從中得知如果設(shè)置了OnTouchListener則第二個(gè)判斷也為true,第三個(gè)判斷為如果該View是否為enable,默認(rèn)都是enable的,所以同樣為true。還剩最后一個(gè):li.mOnTouchListener.onTouch(this, event) ,顯然是回調(diào)了第二個(gè)判斷中監(jiān)聽(tīng)器的onTouch()方法,如果onTouch()方法返回true,則上面四個(gè)判斷全部為true,dispatchTouchEvent()方法會(huì)返回true,并且不會(huì)執(zhí)行if (!result && onTouchEvent(event))這個(gè)判斷;而在這個(gè)判斷中我們又看到了一個(gè)熟悉的方法:onTouchEvent() 。所以想要執(zhí)行onTouchEvent,則在上面的四個(gè)判斷中必須至少有一個(gè)false。
那就假定我們?cè)?code>onTouch()方法中返回的是false,這樣就順利地執(zhí)行了onTouchEvent,那就看看onTouchEvent的源碼吧:
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
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;
}
}
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)) {
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;
}
這段源碼比 dispatchTouchEvent 的還要長(zhǎng),不過(guò)同樣我們挑重點(diǎn)的看:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)看到這句話就大概知道了主要是判斷該view是否是可點(diǎn)擊的,如果可以點(diǎn)擊則接著執(zhí)行,否則直接返回false??梢钥吹絠f里面用switch來(lái)判斷是哪種觸摸事件,但在最后都是返回true的。
還有一點(diǎn)要注意:在 ACTION_UP 中會(huì)執(zhí)行 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;
}
可以看到上面的li.mOnClickListener.onClick(this); ,沒(méi)錯(cuò),我們好像又有了新的發(fā)現(xiàn)。根據(jù)上面的經(jīng)驗(yàn),這句代碼會(huì)去回調(diào)我們?cè)O(shè)置好的點(diǎn)擊事件監(jiān)聽(tīng)器。也就是我們平常用的xxx.setOnClickListener(listener);
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
我們可以看到上面方法設(shè)置正是mListenerInfo的點(diǎn)擊監(jiān)聽(tīng)器,驗(yàn)證了上面的猜想。到了這里onTouch事件的傳遞機(jī)制基本已經(jīng)分析完成了,也算是告一段落了。
好了,這下我們可以解決開(kāi)頭的問(wèn)題了,順便我們?cè)賮?lái)小結(jié)一下:在dispatchTouchEvent中,如果設(shè)置了OnTouchListener并且View是enable的,那么首先被執(zhí)行的是OnTouchListener中的onTouch(View v, MotionEvent event) 。若onTouch返回true,則dispatchTouchEvent不再往下執(zhí)行并且返回true;不然會(huì)執(zhí)行onTouchEvent,在onTouchEvent中若View是可點(diǎn)擊的,則返回true,不然為false。還有在onTouchEvent中若View是可點(diǎn)擊以及當(dāng)前觸摸事件為ACTION_UP,會(huì)執(zhí)行performClick() ,回調(diào)OnClickListener的onClick方法。
下面是我畫(huà)的一張草圖:

還有一點(diǎn)值得注意的地方是:假如當(dāng)前事件是ACTION_DOWN,只有dispatchTouchEvent返回true了之后該View才會(huì)接收到接下來(lái)的ACTION_MOVE,ACTION_UP事件,也就是說(shuō)只有事件被消費(fèi)了才能接收接下來(lái)的事件。
總結(jié)
以上就是關(guān)于Android中onTouch事件傳遞機(jī)制的詳細(xì)分析,希望對(duì)各位Android開(kāi)發(fā)者們的學(xué)習(xí)或者工作能有一定的幫助,如果有疑問(wèn)大家可以留言交流。
相關(guān)文章
Android?studio?利用共享存儲(chǔ)進(jìn)行用戶的注冊(cè)和登錄驗(yàn)證功能
這篇文章主要介紹了Android?studio?利用共享存儲(chǔ)進(jìn)行用戶的注冊(cè)和登錄驗(yàn)證功能,包括注冊(cè)頁(yè)面布局及登錄頁(yè)面功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-12-12
Android?`%d`?與?`1$%d`?格式化的區(qū)別解析
本文詳細(xì)解析了Android開(kāi)發(fā)中`%d`和`1$%d`格式化占位符的區(qū)別,并通過(guò)Kotlin代碼示例幫助理解,`%d`按順序填充參數(shù),而`1$%d`按指定索引填充參數(shù),后者在多語(yǔ)言場(chǎng)景下更靈活,感興趣的朋友一起看看吧2025-03-03
Android實(shí)戰(zhàn)教程第一篇之最簡(jiǎn)單的計(jì)算器
這篇文章主要為大家詳細(xì)介紹了Android實(shí)戰(zhàn)教程第一篇,如何實(shí)現(xiàn)最簡(jiǎn)單的計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
android中Invalidate和postInvalidate的更新view區(qū)別
Android中實(shí)現(xiàn)view的更新有兩組方法,一組是invalidate,另一組是postInvalidate,其中前者是在UI線程自身中使用,而后者在非UI線程中使用,感興趣的朋友可以了解下哦2013-01-01
Android onKeyDown監(jiān)聽(tīng)返回鍵無(wú)效的解決辦法
這篇文章主要介紹了 Android onKeyDown監(jiān)聽(tīng)返回鍵無(wú)效的解決辦法的相關(guān)資料,需要的朋友可以參考下2017-06-06
Android開(kāi)發(fā)簡(jiǎn)易音樂(lè)播放器
這篇文章主要為大家詳細(xì)介紹了Android開(kāi)發(fā)簡(jiǎn)易音樂(lè)播放器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
Android照片墻應(yīng)用實(shí)現(xiàn) 再多的圖片也不怕崩潰
這篇文章主要為大家詳細(xì)介紹了Android照片墻應(yīng)用實(shí)現(xiàn),再多的圖片也不怕崩潰,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10
關(guān)于Android高德地圖的簡(jiǎn)單開(kāi)發(fā)實(shí)例代碼(DEMO)
高德地圖在日常生活中經(jīng)常會(huì)用到,那么基于代碼如何實(shí)現(xiàn)高德地圖呢?下面小編給大家分享一個(gè)demo幫助大家學(xué)習(xí)android高德地圖的簡(jiǎn)單開(kāi)發(fā),需要的朋友參考下2016-11-11
Android計(jì)時(shí)器chronometer使用實(shí)例講解
這篇文章主要為大家詳細(xì)介紹了Android計(jì)時(shí)器chronometer使用實(shí)例,介紹了Android計(jì)時(shí)器chronometer基本使用方法,感興趣的小伙伴們可以參考一下2016-04-04

