詳細(xì)分析Android中onTouch事件傳遞機(jī)制
onTach介紹
ontach是Android系統(tǒng)中整個(gè)事件機(jī)制的基礎(chǔ)。Android中的其他事件,如onClick、onLongClick等都是以onTach為基礎(chǔ)的。
onTach包括從手指按下到離開手機(jī)屏幕的整個(gè)過程,在微觀形式上,具體表現(xiàn)為action_down、action_move和action_up等過程。
onTach兩種主要定義形式如下:
1.在自定義控件中,常見的有重寫onTouchEvent(MotionEvent ev)
方法。如在開發(fā)中經(jīng)常可以看到重寫的onTouchEvent方法,
并且其中有針對(duì)不同的微觀表現(xiàn)(action_down、action_move和action_up等)做出的相應(yīng)判斷,執(zhí)行邏輯并可能返回不同的布爾值。
2.在代碼中,直接對(duì)現(xiàn)有控件設(shè)置setOnTouchListener監(jiān)聽器。并重寫監(jiān)聽器的onTouch方法。onTouch回調(diào)函數(shù)中有view和MotionEvent
onTouch事件傳遞機(jī)制
大家都知道一般我們使用的UI控件都是繼承自共同的父類——View。所以View這個(gè)類應(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類型的,注釋上的解釋:@return True if the event was handled by the view, false otherwise.
也就是說如果該觸摸事件被這個(gè)View消費(fèi)了就返回true,否則返回false。在方法中首先判斷了該event是否是否得到了焦點(diǎn),如果沒有得到焦點(diǎn)直接返回false。然后讓我們把目光轉(zhuǎn)向if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event))
這個(gè)片段,看到這里有一個(gè)名為li的局部變量,屬于 ListenerInfo 類,經(jīng) mListenerInfo 賦值得到。ListenerInfo只是一個(gè)包裝類,里面封裝了大量的監(jiān)聽器。
再在 View 類中去尋找 mListenerInfo ,可以看到下面的代碼:
ListenerInfo getListenerInfo() { if (mListenerInfo != null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; }
因此我們可以知道m(xù)ListenerInfo是不為空的,所以li也不是空,第一個(gè)判斷為true,然后看到li.mOnTouchListener,前面說過ListenerInfo是一個(gè)監(jiān)聽器的封裝類,所以我們同樣去追蹤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; }
正是通過上面的方法來設(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)聽器的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),不過同樣我們挑重點(diǎn)的看:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
看到這句話就大概知道了主要是判斷該view是否是可點(diǎn)擊的,如果可以點(diǎn)擊則接著執(zhí)行,否則直接返回false。可以看到if里面用switch來判斷是哪種觸摸事件,但在最后都是返回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);
,沒錯(cuò),我們好像又有了新的發(fā)現(xiàn)。根據(jù)上面的經(jīng)驗(yàn),這句代碼會(huì)去回調(diào)我們?cè)O(shè)置好的點(diǎn)擊事件監(jiān)聽器。也就是我們平常用的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)聽器,驗(yàn)證了上面的猜想。到了這里onTouch事件的傳遞機(jī)制基本已經(jīng)分析完成了,也算是告一段落了。
好了,這下我們可以解決開頭的問題了,順便我們?cè)賮硇〗Y(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方法。
下面是我畫的一張草圖:
還有一點(diǎn)值得注意的地方是:假如當(dāng)前事件是ACTION_DOWN,只有dispatchTouchEvent返回true了之后該View才會(huì)接收到接下來的ACTION_MOVE,ACTION_UP事件,也就是說只有事件被消費(fèi)了才能接收接下來的事件。
總結(jié)
以上就是關(guān)于Android中onTouch事件傳遞機(jī)制的詳細(xì)分析,希望對(duì)各位Android開發(fā)者們的學(xué)習(xí)或者工作能有一定的幫助,如果有疑問大家可以留言交流。
相關(guān)文章
Android使用線程獲取網(wǎng)絡(luò)圖片的方法
這篇文章主要為大家詳細(xì)介紹了Android使用線程獲取網(wǎng)絡(luò)圖片的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06Android下拉刷新ListView——RTPullListView(demo)
下拉刷新已經(jīng)形成一種默認(rèn)的用戶習(xí)慣,今天主要介紹下在Android上實(shí)現(xiàn)下拉刷新的Demo,感興趣的朋友可以參考下哈,希望可以幫助到你2013-04-04loadavg數(shù)據(jù)異常引發(fā)問題起源分析
這篇文章主要為大家介紹了loadavg數(shù)據(jù)異常引發(fā)問題起源分析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Anroid四大組件service之本地服務(wù)的示例代碼
本篇文章主要介紹了Anroid四大組件service之本地服務(wù)的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10android應(yīng)用開發(fā)之spinner控件的簡(jiǎn)單使用
Android的控件有很多種,其中就有一個(gè)Spinner的控件,這個(gè)控件其實(shí)就是一個(gè)下拉顯示列表。本文通過腳本之家平臺(tái)給大家介紹android應(yīng)用開發(fā)之spinner控件的簡(jiǎn)單使用,感興趣的朋友可以參考下2015-11-11Android系列---JSON數(shù)據(jù)解析的實(shí)例
JSON(JavaScript Object Notation)和XML,并稱為客戶端和服務(wù)端交互解決方案的倚天劍和屠龍刀,這篇文章主要介紹了Android系列---JSON數(shù)據(jù)解析的實(shí)例,有興趣的可以了解一下。2016-11-11