Android事件分發(fā)機(jī)制深入刨析原理及源碼
前言
在 Android 中,事件分發(fā)機(jī)制是一塊很重要的知識(shí)點(diǎn), 掌握這個(gè)機(jī)制能幫你在平時(shí)的開(kāi)發(fā)中解決掉很多的 View 事件沖突問(wèn)題,這個(gè)問(wèn)題也是面試中問(wèn)的比較多的一個(gè)問(wèn)題了,本篇就來(lái)總結(jié)下這個(gè)知識(shí)點(diǎn)。
事件分發(fā)原因
Android 中頁(yè)面上的 View 是以樹(shù)型結(jié)構(gòu)顯示的,View 會(huì)重疊在一起,當(dāng)我們點(diǎn)擊的地方有多個(gè) View 可以響應(yīng)的時(shí)候,這個(gè)點(diǎn)擊事件應(yīng)該給誰(shuí),為了解決這個(gè)問(wèn)題就需要一個(gè)事件分發(fā)機(jī)制
事件分發(fā)對(duì)象
Touch 事件,即將每一個(gè) Touch 事件(MotionEvent)傳遞給 View,至于最終這個(gè)事件有沒(méi)有處理看接收事件者的邏輯而定
當(dāng)用戶觸摸屏幕的時(shí)候,就會(huì)產(chǎn)生 Touch 事件(Touch 事件被封裝成 MotionEvent 對(duì)象),其主要分為如下幾種
- MotionEvent.ACTION_DOWN:使用手指點(diǎn)擊屏幕這一瞬間,產(chǎn)生該事件,是所有事件的開(kāi)始
- MotionEvent.ACTION_MOVE:使用手指在屏幕滑動(dòng)的時(shí)候產(chǎn)生該事件
- MotionEvent.ACTION_CANCLE:非人為原因結(jié)束當(dāng)前事件
- MotionEvent.ACTION_UP:手指離開(kāi)屏幕一瞬間產(chǎn)生該事件
一次完整的 Touch 事件,是從用戶手指觸摸屏幕(伴隨著一次 ACTIONDOWN 事件)到用戶手指離開(kāi)屏幕(伴隨著一次 ACTIONUP 事件)這一過(guò)程,整個(gè)過(guò)程如下
ACTIONDOWN(一次) --> ACTIONMOVE(N 次) --> ACTION_UP(一次)
事件分發(fā)方法
- dispatchTouchEvent(MotionEvent ev) :從方法名也能看出它的作用是對(duì)事件進(jìn)行分發(fā);當(dāng)一個(gè)事件由底層驅(qū)動(dòng)檢測(cè)到了之后,會(huì)進(jìn)行上報(bào),最終會(huì)交由 Activity 的該方法處理,來(lái)決定是自己消費(fèi)還是繼續(xù)傳遞下去
- onInterceptTouchEvent(MotionEvent ev) :當(dāng)一個(gè)事件分發(fā)到 ViewGroup 后,它可以決定是否對(duì)該事件進(jìn)行攔截,該方法只有 ViewGroup 擁有
- onTouchEvent(MotionEvent event) :這是事件分發(fā)流程的最后一個(gè)方法了,即是否消費(fèi)該次事件
事件分發(fā)參與者
- Activity:包含 ViewGroup 和 View
- ViewGroup:包含 ViewGroup 和 View
- View:并不包含其它 View,只有自己
事件分發(fā)流向一般是 Activity --> ViewGroup --> … --> View
注意:
- 子 View 可以通過(guò) requestDisallowInterceptTouchEvent 方法干預(yù)父 View 的事件分發(fā)過(guò)程(ACTION_DOWN 事件除外),而這就是我們處理滑動(dòng)沖突常用的關(guān)鍵方法
- 如果 View 設(shè)置了 onTouchListener,在重寫的 onTouch 方法中返回 true,那么它的 onTouchEvent 方法不會(huì)被調(diào)用,因?yàn)樵?View 的 dispatchTouchEvent 中 onTouch 優(yōu)先于 onTouchEvent 執(zhí)行;onClick 方法也不會(huì)被調(diào)用,因?yàn)?onClick 是在 onTouchEvent 中回調(diào)的
事件分發(fā)流程
- 當(dāng)手指觸摸屏幕后,底層 Input 驅(qū)動(dòng)從/dev/input/路徑下讀寫以 event[NUMBER]為名的硬件輸入設(shè)備節(jié)點(diǎn)獲取事件(可以通過(guò) adb shell getevent 查看你的設(shè)備下的節(jié)點(diǎn),Android 也是從這些節(jié)點(diǎn)獲取這些原始數(shù)據(jù)再封裝后提供給開(kāi)發(fā)者使用;如果做游戲開(kāi)發(fā)可能就直接獲取這些原始數(shù)據(jù)自己處理了),經(jīng)過(guò)一系列調(diào)用后傳遞到了 DecorView 的 dispatchTouchEvent 方法
- 在 DecorView 中,會(huì)通過(guò) Window 的內(nèi)部接口 Callback,將事件繼續(xù)傳遞,因?yàn)?Activity 實(shí)現(xiàn)了該接口,故事件分發(fā)到 Activity;Activity 獲取到事件后,在 dispatchTouchEvent 方法中先將事件分發(fā)到該 Activity 所在的 window,實(shí)際類型是 PhoneWindow,這個(gè) window 又將事件交給它的頂級(jí) view 即 DecorView 處理
- DecorView 是 FrameLayout 的子類,即 ViewGroup 的子類,自己沒(méi)有處理,只是繼續(xù)將事件交由 ViewGroup 處理;就這樣一個(gè)事件就從 Activity 轉(zhuǎn)到了 ViewGroup
- ViewGroup 在 dispatchTouchEvent 方法進(jìn)行分發(fā),如果自己的 onInterceptTouchEvent 方法攔截此次事件,就把事件交給自身的 onTouchEvent 方法處理;反之遍歷自己的子 View,繼續(xù)將事件分發(fā)下去,只要有一個(gè)子 View 消費(fèi)了這個(gè)事件,那就停止遍歷
- 事件會(huì)傳遞到子 View 的 dispatchTouchEvent 方法,如果給子 View 注冊(cè)了 OnTouchListener,且返回 true,那事件分發(fā)就到此結(jié)束;反之就會(huì)繼續(xù)將事件傳遞到子 View 的 onTouchEvent 方法
- 子 View 會(huì)在 ACTION_UP 事件中回調(diào) View 的 onClick 監(jiān)聽(tīng),如果子 View 沒(méi)有消費(fèi)此次事件,就會(huì)按照分發(fā)流程反過(guò)來(lái)傳遞回去到 Activity;如果到了 Activity 還沒(méi)人消費(fèi)(包括 Activity 自己),那就會(huì)銷毀這個(gè)事件
事件分發(fā)源碼
以下源碼基于 API24
對(duì)應(yīng)上面的流程,當(dāng)有 Touch 事件后,步驟如下
DecorView.dispatchTouchEvent
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); }
此處的 cb 指的是 window 內(nèi)部的 Callback 接口,Activity 實(shí)現(xiàn)了這個(gè)接口,接下來(lái)進(jìn)入 Activity
Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
這個(gè)方法就是 Activity 用來(lái)處理觸摸屏事件,我們可以重寫這個(gè)方法,并返回 true/false,這樣在事件分發(fā)到 window 前就能進(jìn)行攔截,Activity 內(nèi)的 ViewGroup 或者 View 將收不到事件
一個(gè)觸摸屏事件都是以 ACTION_DOWN 開(kāi)始,那就肯定會(huì)進(jìn)入 onUserInteraction()方法
public void onUserInteraction() { }
這是一個(gè)空方法,它的調(diào)用時(shí)機(jī)如下: 當(dāng)一個(gè)按鍵事件,觸摸屏事件或者 trackball 事件分發(fā)到 Activity 的時(shí)候,它就會(huì)被調(diào)用;如果你希望在 Activity 正在運(yùn)行的時(shí)候了解用戶和設(shè)備用某種方式交互,可以重寫這個(gè)方法;不過(guò)需要注意的是這個(gè)方法只響應(yīng) touch-down 這種觸摸手勢(shì),不會(huì)響應(yīng)接下來(lái)的 touch-move 和 touch-up
與這個(gè)方法相對(duì)應(yīng)的一個(gè)方法就是onUserLeaveHint,它同樣也是一個(gè)空方法,它的調(diào)用時(shí)機(jī)如下:
當(dāng)在用戶操作的情況下 Activity 進(jìn)入后臺(tái),這個(gè)方法會(huì)作為 Activity 生命周期的一部分被調(diào)用;比如,用戶按下 home 鍵,當(dāng)前 Activity 就會(huì)進(jìn)入后臺(tái),它就會(huì)被調(diào)用,并且是在 onPause 之前調(diào)用;但是比如有電話打進(jìn)來(lái)了導(dǎo)致 Activity 被動(dòng)進(jìn)入后臺(tái),這個(gè)方法就不會(huì)被調(diào)用
接下來(lái)進(jìn)入第二個(gè) if 語(yǔ)句
getWindow().superDispatchTouchEvent
通過(guò) getWindow()獲取到的是一個(gè) Window 對(duì)象,但是它是在 Activity 的 attach 方法中進(jìn)行實(shí)例化,實(shí)際類型是 PhoneWindow,也是在這里實(shí)現(xiàn)了 Callback 接口
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window) { ...... mWindow = new PhoneWindow(this, window); mWindow.setCallback(this); ...... }
這里就轉(zhuǎn)到 PhoneWindow,如下
PhoneWindow.superDispatchTouchEvent
//這是窗口的頂層視圖 private DecorView mDecor @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
DecorView .superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
DecorView 是 FrameLayout 的子類,F(xiàn)rameLayout 又是 ViewGroup 的子類,這里就會(huì)走到 ViewGroup
ViewGroup.dispatchTouchEvent
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //用于調(diào)試目的的一致性驗(yàn)證程序 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // If the event targets the accessibility focused view and this is it, start // normal event dispatch. Maybe a descendant is what will handle the click. if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } //這個(gè)變量用于標(biāo)記事件是否被消費(fèi) boolean handled = false; //根據(jù)應(yīng)用安全策略過(guò)濾觸摸事件 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 處理 initial down 發(fā)生后的初始化操作 if (actionMasked == MotionEvent.ACTION_DOWN) { // 新的 ACTION_DOWN 事件來(lái)了,需要取消并清除之前的 touch Targets //清空掉 mFirstTouchTarget cancelAndClearTouchTargets(ev); //重置觸摸狀態(tài) resetTouchState(); } //標(biāo)記是否攔截事件 final boolean intercepted; // 當(dāng) ACTION_DOWN 來(lái)了或者已經(jīng)發(fā)生過(guò) ACTION_DOWN,并且將 mFirstTouchTarget 賦值 就檢測(cè) ViewGroup 是否需要攔截事件. //只有發(fā)生過(guò) ACTION_DOWN 事件,mFirstTouchTarget != null if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //子 View 可以通過(guò)調(diào)用父 View 的 requestDisallowInterceptTouchEvent 方法設(shè)置 mGroupFlags 值 //以此告訴父 View 是否攔截事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //如果子 view 沒(méi)有告訴父 View 別攔截事件,那父 View 就判斷自己是否需要攔截事件 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // 重新恢復(fù) action 以防被改變了 } else { //這里表明子 View 告訴父 View 不要攔截事件 intercepted = false; } } else { //當(dāng) mFirstTouchTarget=null(沒(méi)有子 View 被分配處理),且不是 initial down 事件時(shí)(事件已經(jīng)初始化過(guò)了),ViewGroup 繼續(xù)攔截觸摸 //繼續(xù)設(shè)置為 true intercepted = true; } // 如果當(dāng)前事件是 ACTION_CANCEL,或者 view.mPrivateFlags 被設(shè)置了 PFLAG_CANCEL_NEXT_UP_EVENT //那么當(dāng)前事件就取消了 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; //split 表示當(dāng)前的 ViewGroup 是不是支持分割 MotionEvent 到不同的 View 當(dāng)中 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; //新的 TouchTarget TouchTarget newTouchTarget = null; //是否把事件分發(fā)給了新的 TouchTarget boolean alreadyDispatchedToNewTouchTarget = false; //不取消事件,同時(shí)不攔截事件才進(jìn)入該區(qū)域 if (!canceled && !intercepted) { //把事件分發(fā)給所有的子視圖,尋找可以獲取焦點(diǎn)的視圖 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; //如果是這三種事件就得遍歷子 View if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // 對(duì)于這個(gè) PointerId 清空更早的 touch targets removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; //如果當(dāng)前 ViewGroup 有子 View 且 newTouchTarget=null if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 在視圖里從前到后掃描一遍獲取可以接收事件的子 View final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; //遍歷所有子 View,找到一個(gè)來(lái)接收事件 for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); //如果當(dāng)前子 View 沒(méi)有獲取焦點(diǎn),則跳過(guò)這個(gè)子 View if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } //如果當(dāng)前子 View 不可見(jiàn)且沒(méi)有播放動(dòng)畫 或者 不在觸摸點(diǎn)范圍內(nèi),跳過(guò)這個(gè)子 View if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //如果在觸摸目標(biāo)列表找到了與該子 View 對(duì)應(yīng)的 TouchTarget,說(shuō)明這個(gè) view 正在接收事件,不需要再遍歷,直接退出 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //子 view 處于觸摸位置,就將事件分發(fā)給子 View,如果該子 View 返回 true,說(shuō)明消費(fèi)了這個(gè)事件,就跳出遍歷 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 獲取 TouchDown 的時(shí)間點(diǎn) mLastTouchDownTime = ev.getDownTime(); // 獲取 TouchDown 的 Index if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } //獲取 TouchDown 的 x,y 坐標(biāo) mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //添加到觸摸目標(biāo)列表 同時(shí)給 mFirstTouchTarget 賦值 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { // 到這里說(shuō)明沒(méi)有子 View 接收事件,那就把最近一次的觸摸目標(biāo)賦值給 newTouchTarget newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // mFirstTouchTarget 賦值是在通過(guò) addTouchTarget 方法獲取的; // 只有處理 ACTION_DOWN 事件,才會(huì)進(jìn)入 addTouchTarget 方法。 // 這也正是當(dāng) View 沒(méi)有消費(fèi) ACTION_DOWN 事件,則不會(huì)接收其他 MOVE,UP 等事件的原因 if (mFirstTouchTarget == null) { // 那就只能 ViewGroup 自己處理事件了 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 到這里就說(shuō)明有子 View 接收了 ACTION_DOWN 事件,那后續(xù)的 move up 等事件就繼續(xù)分發(fā)給這個(gè)觸摸目標(biāo) TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { //如果 view.mPrivateFlags 被設(shè)置了 PFLAG_CANCEL_NEXT_UP_EVENT 或者事件被 ViewGroup 攔截了 //那子 View 需要取消事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //繼續(xù)分發(fā)事件給子 View if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } //當(dāng)發(fā)生抬起或取消事件,更新觸摸目標(biāo)列表 if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { //如果是多點(diǎn)觸摸下的手指抬起事件,就要根據(jù) idBit 從 TouchTarget 中移除掉對(duì)應(yīng)的 Pointer(觸摸點(diǎn)) final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
這個(gè)方法內(nèi)容有點(diǎn)多,需要拆分開(kāi)分析
第一步:事件初始化
第一個(gè)進(jìn)來(lái)的是 ACTION_DOWN 事件,那需要做一些初始化:
第一件事就是清空所有的 TouchTarget,并將 mFirstTouchTarget 值為 null;mFirstTouchTarget 的類型也是 TouchTarget,是 ViewGroup 的一個(gè)內(nèi)部類,描述一個(gè)觸摸的視圖和它捕獲的指針的 id;mFirstTouchTarget 可以理解為如果事件由子 View 去處理時(shí) mFirstTouchTarget 會(huì)被賦值并指向子 View
第二件事是重置狀態(tài)值,通過(guò) FLAGDISALLOWINTERCEPT 重置 mGroupFlags 值
ViewGroup.cancelAndClearTouchTargets
/** * 取消和清空所有的 touch targets. */ private void cancelAndClearTouchTargets(MotionEvent event) { if (mFirstTouchTarget != null) { boolean syntheticEvent = false; if (event == null) { final long now = SystemClock.uptimeMillis(); event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); event.setSource(InputDevice.SOURCE_TOUCHSCREEN); syntheticEvent = true; } for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { resetCancelNextUpFlag(target.child); dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits); } clearTouchTargets(); if (syntheticEvent) { event.recycle(); } } } /** * 清空所有的 touch targets. */ private void clearTouchTargets() { TouchTarget target = mFirstTouchTarget; if (target != null) { do { TouchTarget next = target.next; target.recycle(); target = next; } while (target != null); mFirstTouchTarget = null; } } /** * 重置所有觸摸狀態(tài)以準(zhǔn)備新周期. */ private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
第二步:攔截判斷
接下來(lái)就需要判斷是否需要攔截事件:
首先看條件是
//標(biāo)記是否攔截事件 final boolean intercepted; // 當(dāng) ACTION_DOWN 來(lái)了或者已經(jīng)發(fā)生過(guò) ACTION_DOWN,并且將 mFirstTouchTarget 賦值 就檢測(cè) ViewGroup 是否需要攔截事件. if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //子 View 可以通過(guò)調(diào)用父 View 的 requestDisallowInterceptTouchEvent 方法設(shè)置 mGroupFlags 值 //以此告訴父 View 是否攔截事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //如果子 view 沒(méi)有告訴父 View 別攔截事件,那父 View 就判斷自己是否需要攔截事件 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // 重新恢復(fù) action 以防被改變了 } else { //這里表明子 View 告訴父 View 不要攔截事件 intercepted = false; } } else { //當(dāng) mFirstTouchTarget=null(沒(méi)有子 View 被分配處理),且不是 initial down 事件時(shí)(事件已經(jīng)初始化過(guò)了),ViewGroup 繼續(xù)攔截觸摸 //繼續(xù)設(shè)置為 true intercepted = true; }
- 當(dāng)事件是 ACTIONDOWN 或者 mFirstTouchTarget != null 才會(huì)去判斷要不要攔截,由第一步可知,當(dāng)事件是 ACTIONDOWN 的時(shí)候,mFirstTouchTarget 肯定為 null,所以這里只有兩種情況會(huì)進(jìn)入:ACTIONDOWN 事件來(lái)了需要判斷攔截;ACTIONDOWN 事件中如果有子 View 接收了事件(這樣 mFirstTouchTarget 就賦值了),那接下來(lái)的事件也需要判斷是否攔截事件
- 上面條件的反向邏輯就是事件是 ACTIONDOWN 事件以后的事件(比如 move 或者 up)且 mFirstTouchTarget 為 null,說(shuō)明在 ACTIONDOWN 事件中就判斷了需要攔截事件或者沒(méi)有子 View 處理事件,那接下來(lái)的事件就沒(méi)必要分發(fā)了,繼續(xù)攔截
第一個(gè) if 語(yǔ)句里面是攔截判斷邏輯是
- 先通過(guò)與運(yùn)算獲得 mGroupFlags 的值,子 view 可以通過(guò)調(diào)用父 view 的requestDisallowInterceptTouchEvent 方法設(shè)置 mGroupFlags 的值,告訴父 view 不要攔截事件
- 如果 disallowIntercept 為 true,說(shuō)明子 view 要求父 view 不要攔截,就將 intercepted 設(shè)置 false
- 如果 disallowIntercept 為 false,表明子 view 沒(méi)有提出不要攔截請(qǐng)求,那就調(diào)用 onInterceptTouchEvent 看看自己是不是需要攔截事件
ViewGroup.requestDisallowInterceptTouchEvent
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // 如果已經(jīng)設(shè)置過(guò)了,就返回 return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // 依次告訴父 view if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
ViewGroup.onInterceptTouchEvent
/** * ViewGroup 可在這個(gè)方法里攔截所有觸摸事件,默認(rèn)是不攔截事件,開(kāi)發(fā)者可以重寫這個(gè)方法決定是否要攔截 * 如下四個(gè)條件都成立,返回 true,攔截事件 * 第一個(gè):觸摸事件是否來(lái)自鼠標(biāo)指針設(shè)備 * 第二個(gè):觸摸事件是否是 ACTION_DOWN * 第三個(gè):檢查是否按下了鼠標(biāo)或手寫筆按鈕(或按鈕組合),也就是說(shuō)用戶必須實(shí)際按下 * 第四個(gè):觸摸點(diǎn)是否在滾動(dòng)條上 */ public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }
第三步:ACTION_DOWN 事件分發(fā)
接下來(lái)就需要遍歷子 View,然后將 ACTION_DOWN 事件分發(fā)給能接收事件的子 View
- 如果當(dāng)前子 View 沒(méi)有獲取焦點(diǎn),則跳過(guò)這個(gè)子 View
- 如果當(dāng)前子 View 不可見(jiàn)且沒(méi)有播放動(dòng)畫 或者 不在觸摸點(diǎn)范圍內(nèi),跳過(guò)這個(gè)子 View
- 如果在觸摸目標(biāo)列表找到了與該子 View 對(duì)應(yīng)的 TouchTarget,說(shuō)明這個(gè) view 正在接收事件,不需要再遍歷,直接退出
- 如果子 view 處于觸摸位置,就調(diào)用 dispatchTransformedTouchEvent 方法將事件分發(fā)給子 View,如果該方法返回 true,說(shuō)明子 View 消費(fèi)了這個(gè)事件,那就不需要再尋找子 view 接收事件了,跳出遍歷
ViewGroup.dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // 發(fā)生取消操作時(shí),不再執(zhí)行后續(xù)的任何操作 final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; //由于某些原因,發(fā)生不一致的操作,那么將拋棄該事件 if (newPointerIdBits == 0) { return false; } //分發(fā)的主要區(qū)域 final MotionEvent transformedEvent; //判斷預(yù)期的 pointer id 與事件的 pointer id 是否相等 if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { //不存在子視圖時(shí),ViewGroup 調(diào)用 View.dispatchTouchEvent 分發(fā)事件,再調(diào)用 ViewGroup.onTouchEvent 來(lái)處理事件 handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); //將觸摸事件分發(fā)給子 ViewGroup 或 View; handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); //調(diào)整該事件的位置 } return handled; } transformedEvent = MotionEvent.obtain(event); //拷貝該事件,來(lái)創(chuàng)建一個(gè)新的 MotionEvent } else { //分離事件,獲取包含 newPointerIdBits 的 MotionEvent transformedEvent = event.split(newPointerIdBits); } if (child == null) { //不存在子視圖時(shí),ViewGroup 調(diào)用 View.dispatchTouchEvent 分發(fā)事件,再調(diào)用 ViewGroup.onTouchEvent 來(lái)處理事件 handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { //將該視圖的矩陣進(jìn)行轉(zhuǎn)換 transformedEvent.transform(child.getInverseMatrix()); } //將觸摸事件分發(fā)給子 ViewGroup 或 View; handled = child.dispatchTouchEvent(transformedEvent); } //回收 transformedEvent transformedEvent.recycle(); return handled; }
該方法是 ViewGroup 真正處理事件的地方,分發(fā)子 View 來(lái)消費(fèi)事件,過(guò)濾掉不相干的 pointer ids。當(dāng)子視圖為 null 時(shí),MotionEvent 將會(huì)發(fā)送給該 ViewGroup;不為 null,最終調(diào)用 View.dispatchTouchEvent 方法來(lái)分發(fā)事件。
這個(gè)方法調(diào)用完畢,回到 ViewGroup.dispatchTouchEvent 會(huì)調(diào)用 addTouchTarget 方法
ViewGroup.addTouchTarget
private TouchTarget addTouchTarget(View child, int pointerIdBits) { TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
可以看到在這里給 mFirstTouchTarget 賦值了
當(dāng)子控件消費(fèi)了事件,mFirstTouchTarget 不為空;當(dāng)子控件沒(méi)有消費(fèi)事件或者被攔截,mFirstTouchTarget 為空
第四步:ACTIONMOVE ACTIONUP 事件分發(fā)
在第三步過(guò)后,ViewGroup 可能會(huì)找到有子 View 消費(fèi)事件
- 如果事件被攔截,mFirstTouchTarget==null,那接下來(lái)的事件最終調(diào)用 View.dispatchTouchEvent 方法來(lái)分發(fā)事件
- 如果 ViewGroup 沒(méi)有子 View,mFirstTouchTarget==null,那接下來(lái)同上
- 如果有子 View,但是子 View 沒(méi)消費(fèi)事件,mFirstTouchTarget==null,那接下來(lái)同上
- 如果有子 View,且子 View 消費(fèi)了 ACTION_DOWN 事件,但是在 dispatchTouchEvent 返回了 false(即 dispatchTransformedTouchEvent 返回 false,那 addTouchTarget 就不會(huì)被調(diào)用),mFirstTouchTarget==null,那接下來(lái)的處理也同上
- 接下來(lái)就是 mFirstTouchTarget 不為 null 了,那就需要將后續(xù)事件分發(fā)給消費(fèi) ACTION_DOWN 事件的 View 了
通過(guò)對(duì) ViewGroup.dispatchTouchEvent 方法的分析,我們知道不管有沒(méi)有子 View 消費(fèi)事件,最終事件都會(huì)進(jìn)入 View.dispatchTouchEvent 方法,那我們繼續(xù)一探究竟
View.dispatchTouchEvent
/** * 將觸摸事件向下傳遞到目標(biāo)視圖,或者這個(gè) View 是目標(biāo)視圖。 * * @return 返回 true 表示消費(fèi)了事件,反之返回 false */ public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { //在 Down 事件之前,如果存在滾動(dòng)操作則停止。不存在則不進(jìn)行操作 stopNestedScroll(); } //過(guò)濾觸摸事件以應(yīng)用安全策略 if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } ListenerInfo li = mListenerInfo; // 如果給 View 設(shè)置了 OnTouchListener //且該 view 沒(méi)有禁用的 //且 OnTouchListener.onTouch 返回 true //那說(shuō)明該 View 消費(fèi)了該事件,返回 true if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //如果 OnTouchListener.onTouch 沒(méi)有消費(fèi)事件且 View 的 onTouchEvent 方法返回 true,那返回 true if (!result && onTouchEvent(event)) { result = true; } } // 如果這是手勢(shì)的結(jié)束,則在嵌套滾動(dòng)后清理; //如果我們嘗試了 ACTION_DOWN 但是我們不想要其余的手勢(shì),也要取消它。 if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
這里有兩點(diǎn)比較重要
- 如果開(kāi)發(fā)者設(shè)置 OnTouchListener 監(jiān)聽(tīng),且在 onTouch 方法返回 true,說(shuō)明 view 消費(fèi)了事件
- 如果沒(méi)有設(shè)置監(jiān)聽(tīng),那就調(diào)用 View 的 onTouchEvent 方法去處理事件
可以看出 OnTouchListener.onTouch 是優(yōu)先于 onTouchEvent 執(zhí)行的,只要前者返回 true,那后者就不會(huì)執(zhí)行了,事件到此為止結(jié)束
接下來(lái)看看 onTouchEvent 的邏輯
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(); //如果這個(gè) view 是禁用的,可以通過(guò) setEnabled()設(shè)置是否禁用 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // 即使設(shè)置了禁用,但是只要這個(gè) view 滿足 CLICKABLE ,LONG_CLICKABLE ,CONTEXT_CLICKABLE 其中一種 //任然算消費(fèi)該事件,只是沒(méi)有響應(yīng)而已 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } //當(dāng) View 狀態(tài)為 ENABLED //且這個(gè) view 滿足 CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE 其中一種,就消費(fèi)這個(gè)事件 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) { // 獲取焦點(diǎn)處于可觸摸模式 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) { //這是 Tap 操作,移除長(zhǎng)按回調(diào)方法 removeLongPressCallback(); // 如果處于按下?tīng)顟B(tài)盡執(zhí)行點(diǎn)擊操作 if (!focusTaken) { // 使用 Runnable 并發(fā)布而不是直接調(diào)用 performClick //這樣可以在單擊操作開(kāi)始之前更新視圖的其他可視狀態(tài) if (mPerformClick == null) { mPerformClick = new PerformClick(); } //調(diào)用 View.OnClickListener 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; } // 確定是否處于可滾動(dòng)的視圖內(nèi) boolean isInScrollingContainer = isInScrollingContainer(); if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); //當(dāng)處于可滾動(dòng)視圖內(nèi),則延遲 TAP_TIMEOUT,再反饋按壓狀態(tài),用來(lái)判斷用戶是否想要滾動(dòng)。默認(rèn)延時(shí)為 100ms postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { //當(dāng)不再滾動(dòng)視圖內(nèi),則立刻反饋按壓狀態(tài) setPressed(true, x, y); //檢測(cè)是否是長(zhǎng)按,如果長(zhǎng)按,回調(diào) OnLongClickListener.onLongClick checkForLongClick(0, x, y); } 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; }
這里有幾點(diǎn)需要注意
- 只要是這個(gè) view 滿足 CLICKABLE ,LONGCLICKABLE ,CONTEXTCLICKABLE 其中一種,不管通過(guò) setEnabled()設(shè)置禁用還是可用,都會(huì)返回 true,認(rèn)為消費(fèi)事件
- View 的 longClickable 默認(rèn)為 false,clickable 需要區(qū)分情況,如 Button 的 clickable 默認(rèn)為 true,而 TextView 的 clickable 默認(rèn)為 false;但是 View 的 setOnClickListener 會(huì)默認(rèn)將 View 的 clickable 設(shè)置成 true,View 的 setOnLongClickListener 同樣會(huì)將 View 的 longClickable 設(shè)置成 true
- 在 ACTION_DOWN 操作中,如果是長(zhǎng)按,回調(diào) OnLongClickListener.onLongClick
- 在 ACTION_UP 操作中,回調(diào) OnClickListener.onClick
Activity.OnTouchEvent
所有流程走完,假如沒(méi)有一個(gè) View 消費(fèi)事件,那最終會(huì)回到 Activity.OnTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //循環(huán)判斷是否有 ViewGroup 或者 View 消費(fèi)事件,如果沒(méi)有,事件回到 activity if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
事件分發(fā)流程圖
- 觸摸事件由 Activity.dispatchTouchEvent 先處理;再一層層往下分發(fā),當(dāng)中間的 ViewGroup 都不消費(fèi)或者攔截時(shí),進(jìn)入最底層的 View,開(kāi)始由最底層的 OnTouchEvent 來(lái)處理,如果一直不消費(fèi),則最后返回到 Activity.OnTouchEvent
- 只有 ViewGroup 有 onInterceptTouchEvent 攔截方法;在分發(fā)過(guò)程中,中間任何一層 ViewGroup 都可以直接攔截,則不再往下分發(fā),而是交由發(fā)生攔截操作的 ViewGroup 的 OnTouchEvent 來(lái)處理
- 子 View 可調(diào)用父 ViewGroup 的 requestDisallowInterceptTouchEvent 方法,來(lái)設(shè)置 disallowIntercept=true,從而阻止父 ViewGroup 的 onInterceptTouchEvent 攔截操作
- OnTouchEvent 由下往上冒泡時(shí),當(dāng)中間任何一層的 OnTouchEvent 消費(fèi)該事件,則不再往上傳遞,表示事件已消費(fèi)
- 如果 dispatchTouchEvent 在進(jìn)行事件分發(fā)的時(shí)候,View 沒(méi)有消費(fèi) ACTIONDOWN 事件,即返回 true,則之后的 ACTIONMOVE 等事件都將無(wú)法接收
- 不管 View 是 DISABLED(禁用)的還是 ENABLED(可用)的,只要是 CLICKABLE (可點(diǎn)擊),LONG_CLICKABLE(可長(zhǎng)按) ,都會(huì)消費(fèi)事件
- View 的 setOnClickListener 會(huì)默認(rèn)將 View 的 clickable 設(shè)置成 true,View 的 setOnLongClickListener 同樣會(huì)將 View 的 longClickable 設(shè)置成 true;所有 View 的 setClickable 和 setLongClickable 最好在兩個(gè)監(jiān)聽(tīng)方法后調(diào)用
- onTouch 優(yōu)先于 onTouchEvent 執(zhí)行,onClick 和 onLongClick 在 onTouchEvent 中被調(diào)用,且 onLongClick 優(yōu)先于 onClick 被執(zhí)行;如果 onTouch 返回 true,就不會(huì)執(zhí)行 onTouchEvent;onTouch 只有 View 設(shè)置了 OnTouchListener,且是 enable 的才執(zhí)行該方法
到此這篇關(guān)于Android事件分發(fā)機(jī)制深入刨析原理及源碼的文章就介紹到這了,更多相關(guān)Android事件分發(fā)機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Android Studio Gradle實(shí)現(xiàn)友盟多渠道打包
這篇文章主要介紹了使用Android Studio Gradle實(shí)現(xiàn)友盟多渠道打包,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05android實(shí)現(xiàn)將位置信息寫入JPEG圖片文件
下面小編就為大家?guī)?lái)一篇android實(shí)現(xiàn)將位置信息寫入JPEG圖片文件。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03簡(jiǎn)單實(shí)現(xiàn)Android放大鏡效果
這篇文章主要教大家簡(jiǎn)單實(shí)現(xiàn)Android放大鏡效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android開(kāi)發(fā)實(shí)現(xiàn)Switch控件修改樣式功能示例【附源碼下載】
這篇文章主要介紹了Android開(kāi)發(fā)實(shí)現(xiàn)Switch控件修改樣式功能,涉及Android Switch開(kāi)關(guān)控件樣式設(shè)置與事件響應(yīng)相關(guān)操作技巧,需要的朋友可以參考下2019-04-04android廣角相機(jī)畸變校正算法和實(shí)現(xiàn)示例
今天小編就為大家分享一篇android廣角相機(jī)畸變校正算法和實(shí)現(xiàn)示例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08Android中生成、使用Json數(shù)據(jù)實(shí)例
這篇文章主要介紹了Android中生成、使用Json數(shù)據(jù)實(shí)例,本文直接給出了實(shí)現(xiàn)代碼,相對(duì)容易理解,需要的朋友可以參考下2014-10-10Android開(kāi)發(fā)之WebView輸入框提示解決辦法
在做webview應(yīng)用時(shí),當(dāng)輸入的文字過(guò)多時(shí),輸入的提示箭頭會(huì)移動(dòng)到輸入框外,怎么解決這個(gè)問(wèn)題呢?下面小編給大家介紹Android開(kāi)發(fā)之WebView輸入框提示解決辦法,一起看看吧2016-06-06