Android事件分發(fā)機制(上) ViewGroup的事件分發(fā)
綜述
Android中的事件分發(fā)機制也就是View與ViewGroup的對事件的分發(fā)與處理。在ViewGroup的內(nèi)部包含了許多View,而ViewGroup繼承自View,所以ViewGroup本身也是一個View。對于事件可以通過ViewGroup下發(fā)到它的子View并交由子View進(jìn)行處理,而ViewGroup本身也能夠?qū)κ录龀鎏幚?。下面就來詳?xì)分析一下ViewGroup對時間的分發(fā)處理。
MotionEvent
當(dāng)手指接觸到屏幕以后,所產(chǎn)生的一系列的事件中,都是由以下三種事件類型組成。
1. ACTION_DOWN: 手指按下屏幕
2. ACTION_MOVE: 手指在屏幕上移動
3. ACTION_UP: 手指從屏幕上抬起
例如一個簡單的屏幕觸摸動作觸發(fā)了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->…->ACTION_MOVE->ACTION_UP
對于Android中的這個事件分發(fā)機制,其中的這個事件指的就是MotionEvent。而View的對事件的分發(fā)也是對MotionEvent的分發(fā)操作。可以通過getRawX和getRawY來獲取事件相對于屏幕左上角的橫縱坐標(biāo)。通過getX()和getY()來獲取事件相對于當(dāng)前View左上角的橫縱坐標(biāo)。
三個重要方法
public boolean dispatchTouchEvent(MotionEvent ev)
這是一個對事件分發(fā)的方法。如果一個事件傳遞給了當(dāng)前的View,那么當(dāng)前View一定會調(diào)用該方法。對于dispatchTouchEvent的返回類型是boolean類型的,返回結(jié)果表示是否消耗了這個事件,如果返回的是true,就表明了這個View已經(jīng)被消耗,不會再繼續(xù)向下傳遞?! ?
public boolean onInterceptTouchEvent(MotionEvent ev)
該方法存在于ViewGroup類中,對于View類并無此方法。表示是否攔截某個事件,ViewGroup如果成功攔截某個事件,那么這個事件就不在向下進(jìn)行傳遞。對于同一個事件序列當(dāng)中,當(dāng)前View若是成功攔截該事件,那么對于后面的一系列事件不會再次調(diào)用該方法。返回的結(jié)果表示是否攔截當(dāng)前事件,默認(rèn)返回false。由于一個View它已經(jīng)處于最底層,它不會存在子控件,所以無該方法。
public boolean onTouchEvent(MotionEvent event)
這個方法被dispatchTouchEvent調(diào)用,用來處理事件,對于返回的結(jié)果用來表示是否消耗掉當(dāng)前事件。如果不消耗當(dāng)前事件的話,那么對于在同一個事件序列當(dāng)中,當(dāng)前View就不會再次接收到事件。
View事件分發(fā)流程圖
對于事件的分發(fā),在這里先通過一個流程圖來看一下整個分發(fā)過程。
ViewGroup事件分發(fā)源碼分析
根據(jù)上面的流程圖現(xiàn)在就詳細(xì)的來分析一下ViewGroup事件分發(fā)的整個過程。
手指在觸摸屏上滑動所產(chǎn)生的一系列事件,當(dāng)Activity接收到這些事件通過調(diào)用Activity的dispatchTouchEvent方法來進(jìn)行對事件的分發(fā)操作。下面就來看一下Activity的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
通過getWindow().superDispatchTouchEvent(ev)這個方法可以看出來,這個時候Activity又會將事件交由Window處理。Window它是一個抽象類,它的具體實現(xiàn)只有一個PhoneWindow,也就是說這個時候,Activity將事件交由PhoneWindow中的superDispatchTouchEvent方法?,F(xiàn)在跟蹤進(jìn)去看一下這個superDispatchTouchEvent代碼。
public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
這里面的mDecor它是一個DecorView,DecorView它是一個Activity的頂級View。它是PhoneWindow的一個內(nèi)部類,繼承自FrameLayout。于是在這個時候事件又交由DecorView的superDispatchTouchEvent方法來處理。下面就來看一下這個superDispatchTouchEvent方法。
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
在這個時候就能夠很清晰的看到DecorView它調(diào)用了父類的dispatchTouchEvent方法。在上面說到DecorView它繼承了FrameLayout,而這個FrameLayout又繼承自ViewGroup。所以在這個時候事件就開始交給了ViewGroup進(jìn)行處理了。下面就開始詳細(xì)看下這個ViewGroup的dispatchTouchEvent方法。由于dispatchTouchEvent代碼比較長,在這里就摘取部分代碼進(jìn)行說明。
// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); }
從上面代碼可以看出,在dispatchTouchEvent中,會對接收的事件進(jìn)行判斷,當(dāng)接收到的是ACTION_DOWN事件時,便會清空事件分發(fā)的目標(biāo)和狀態(tài)。然后執(zhí)行resetTouchState方法重置了觸摸狀態(tài)。下面就來看一下這兩個方法。
1. cancelAndClearTouchTargets(ev)
private TouchTarget mFirstTouchTarget; ...... 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(); } } }
在這里先介紹一下mFirstTouchTarget,它是TouchTarget對象,TouchTarget是ViewGroup的一個內(nèi)部類,TouchTarget采用鏈表數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲View。而在這個方法中主要的作用就是清空mFirstTouchTarget鏈表并將mFirstTouchTarget設(shè)為空。
2. resetTouchState()
private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
在這里介紹一下FLAG_DISALLOW_INTERCEPT標(biāo)記,這是禁止ViewGroup攔截事件的標(biāo)記,可以通過requestDisallowInterceptTouchEvent方法來設(shè)置這個標(biāo)記,當(dāng)設(shè)置了這個標(biāo)記以后,ViewGroup便無法攔截除了ACTION_DOWN以外的其它事件。因為在上面代碼中可以看出,當(dāng)事件為ACTION_DOWN時,會重置FLAG_DISALLOW_INTERCEPT標(biāo)記。
那么下面就再次回到dispatchTouchEvent方法中繼續(xù)看它的源代碼。
// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
這段代碼主要就是ViewGroup對事件是否需要攔截進(jìn)行的判斷。下面先對mFirstTouchTarget是否為null這兩種情況進(jìn)行說明。當(dāng)事件沒有被攔截時,ViewGroup的子元素成功處理事件后,mFirstTouchTarget會被賦值并且指向其子元素。也就是說這個時候mFirstTouchTarget!=null??墒且坏┦录粩r截,mFirstTouchTarget不會被賦值,mFirstTouchTarget也就為null。
在上面代碼中可以看到根據(jù)actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null這兩個情況進(jìn)行判斷事件是否需要攔截。對于actionMasked==MotionEvent.ACTION_DOWN這個條件很好理解,對于mFirstTouchTarget!=null的兩種情況上面已經(jīng)說明。那么對于一個事件序列,當(dāng)事件為MotionEvent.ACTION_DOWN時,會重置FLAG_DISALLOW_INTERCEPT,也就是說!disallowIntercept一定為true,必然會執(zhí)行onInterceptTouchEvent方法,對于onInterceptTouchEvent方法默認(rèn)返回為false,所以需要ViewGroup攔截事件時,必須重寫onInterceptTouchEvent方法,并返回true。這里有一點需要注意,對于一個事件序列,一旦序列中的某一個事件被成功攔截,執(zhí)行了onInterceptTouchEvent方法,也就是說onInterceptTouchEvent返回值為true,那么該事件之后一系列事件對于條件actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null必然為false,那么這個時候該事件序列剩下的一系列事件將會被攔截,并且不會執(zhí)行onInterceptTouchEvent方法。于是在這里得出一個結(jié)論:對于一個事件序列,當(dāng)其中某一個事件成功攔截時,那么對于剩下的一系列事件也會被攔截,并且不會再次執(zhí)行onInterceptTouchEvent方法
下面再來看一下對于ViewGroup并沒有攔截事件是如何進(jìn)行處理的。
final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); 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; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); 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(); }
對于這段代碼雖然說比較長,但是在這里面的邏輯去不是很復(fù)雜。首先獲取當(dāng)前ViewGroup中的子View和ViewGroup的數(shù)量。然后對該ViewGroup中的元素進(jìn)行逐步遍歷。在獲取到ViewGroup中的子元素后,判斷該元素是否能夠接收觸摸事件。子元素若是能夠接收觸摸事件,并且該觸摸坐標(biāo)在子元素的可視范圍內(nèi)的話,便繼續(xù)向下執(zhí)行。否則就continue。對于衡量子元素能否接收到觸摸事件的標(biāo)準(zhǔn)有兩個:子元素是否在播放動畫和點擊事件的坐標(biāo)是否在子元素的區(qū)域內(nèi)。
一旦子View接收到了觸摸事件,然后便開始調(diào)用dispatchTransformedTouchEvent方法對事件進(jìn)行分發(fā)處理。對于dispatchTransformedTouchEvent方法代碼比較多,現(xiàn)在只關(guān)注下面這五行代碼。從下面5行代碼中可以看出,這時候會調(diào)用子View的dispatchTouchEvent,也就是在這個時候ViewGroup已經(jīng)完成了事件分發(fā)的整個過程。
if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); }
當(dāng)子元素的dispatchTouchEvent返回為true的時候,也就是子View對事件處理成功。這時候便會通過addTouchTarget方法對mFirstTouchTarget進(jìn)行賦值。
如果dispatchTouchEvent返回了false,或者說當(dāng)前的ViewGroup沒有子元素的話,那么這個時候便會調(diào)用如下代碼。
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
在這里調(diào)用dispatchTransformedTouchEvent方法,并將child參數(shù)設(shè)為null。也就是執(zhí)行了super.dispatchTouchEvent(event)方法。由于ViewGroup繼承自View,所以這個時候又將事件交由父類的dispatchTouchEvent進(jìn)行處理。對于父類View是如何通過dispatchTouchEvent對事件進(jìn)行處理的,在下篇文章中會進(jìn)行詳細(xì)說明。
到這里對于ViewGroup的事件分發(fā)已經(jīng)講完了,在這一路下來,不難發(fā)現(xiàn)對于dispatchTouchEvent有一個boolean類型返回值。對于這個返回值,當(dāng)返回true的時候表示當(dāng)前事件處理成功,若是返回false,一般來說是因為在事件處理onTouchEvent返回了false,這時候變會交由它的父控件進(jìn)行處理,以此類推,若是一直處理失敗,則最終會交由Activity的onTouchEvent方法進(jìn)行處理。
總結(jié)
在這里從宏觀上再看一下這個ViewGroup對事件的分發(fā),當(dāng)ViewGroup接收一個事件序列以后,首先會判斷是否攔截該事件,若是攔截該事件,則通過調(diào)用父類View的dispatchTouchEvent來處理這個事件。若是不去攔截這一事件,便將該事件下發(fā)到子View當(dāng)中。若果說ViewGroup沒有子View,或者說子View對事件處理失敗,則將該事件有交由該ViewGroup處理,若是該ViewGroup對事件依然處理失敗,最終則會將事件交由Activity進(jìn)行處理。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android?拍照功能實現(xiàn)(手機關(guān)閉依然拍照)詳解及實例代碼
這篇文章主要介紹了?Android?拍照功能實現(xiàn)(手機關(guān)閉依然拍照)詳解及實例代碼的相關(guān)資料,這對Android相機在不開手機的情況下還能繼續(xù)拍照,附有實例Demo,需要的朋友可以參考下2016-12-12Android中TabLayout結(jié)合ViewPager實現(xiàn)頁面切換效果
這篇文章主要介紹了Android中TabLayout結(jié)合ViewPager實現(xiàn)頁面切換效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10Android?RecyclerView使用ListAdapter高效刷新數(shù)據(jù)的操作方法
這篇文章主要介紹了Android?RecyclerView使用ListAdapter高效刷新數(shù)據(jù),本次也是介紹了用另外一種方法來實現(xiàn)RecyclerView高效刷新數(shù)據(jù)的功能,需要的朋友可以參考下2022-10-10詳解Android_性能優(yōu)化之ViewPager加載成百上千高清大圖oom解決方案
這篇文章主要介紹了詳解Android_性能優(yōu)化之ViewPager加載成百上千高清大圖oom解決方案,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2016-12-12Android實戰(zhàn)教程第三篇之簡單實現(xiàn)撥打電話功能
這篇文章主要為大家詳細(xì)介紹了Android實戰(zhàn)教程第三篇之簡單實現(xiàn)撥打電話功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11Android EditText長按菜單中分享功能的隱藏方法
Android EditText控件是經(jīng)常使用的控件,下面這篇文章主要給大家介紹了關(guān)于Android中EditText長按菜單中分享功能的隱藏方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02