Android 事件分發(fā)和滑動沖突都是開發(fā)中經(jīng)常遇到的難點問題,遇到問題時可能會通過 Google 或者 StackOverflow 按照別人的經(jīng)驗解決了問題,但每次遇到這種問題都去 Google 也是非常不合適的事情。本篇文章將從 Android 事件分發(fā)的源碼入手,首先分析源碼,當(dāng)我們了解了源碼,不但能從源碼中總結(jié)到常見問題的解決方式,并且遇到更加深入問題時也能冷靜的從源碼入手來解決問題,做到知其然更知其所以然。滑動沖突問題究其根本其實就是事件分發(fā)問題,了解了事件分發(fā),我們也就能從一定的高度來解決滑動沖突問題,并總結(jié)出解決滑動沖突問題的模式。
一、Android UI 界面架構(gòu)
要了解 Android 事件分發(fā),我們要先來了解一下 Android 的 UI 界面架構(gòu),因為事件的分發(fā)流程是以 Android 界面架構(gòu)為基礎(chǔ)的,以一張圖來介紹如圖所示,每一個 Activity 都包含一個 Window,Android 中 Window 的實現(xiàn)類是 PhoneWindow;PhoneWindow 中包含一個 DecorView,也就是一個界面布局的根 View,一般是一個 FrameLayout;DecorView 中有一個 ContentView 實際上是一個 ViewGroup,看名字很熟悉,其實這個 ViewGroup 就是 Activity 中我們要顯示布局的 View 對象的父容器,一般是一個 FrameLayout,在 Activity 的 onCreate() 中通過 setContentView 方法將要顯示的布局的 View 對象放入該 ContentView;ContentView 中的 ViewGroup 就是我們界面要顯示的布局 ViewGroup;View 則是界面中每一個需要顯示的 View 控件,這就是 Android UI 界面框架的簡單模型。
二、Android 事件分發(fā)
接著說事件分發(fā),一個觸摸事件的產(chǎn)生是由屏幕、Native 層、Framework 層產(chǎn)生的,產(chǎn)生之后會通過 Framework 層傳遞到 Activity 中,剛才提到了事件分發(fā)過程是以 Android 界面架構(gòu)為基礎(chǔ)的,怎么理解呢,就是說事件的分發(fā)流程是以 Activity 開始,經(jīng)過 PhoneWindow、DecorView、ViewGroup、View,整個過程正好與界面架構(gòu)的層級匹配。
由于 DecorView 也是 GroupView,為了簡單,在分析整個事件分發(fā)流程時我們可以把 DecorView、ContentView、GroupView 合一,直接分析 Activity、PhoneWindow、ViewGroup、View 四個層級
注意:每個事件都會經(jīng)歷以下所說的所有流程,并且一個事件執(zhí)行結(jié)束后才開始執(zhí)行下一事件,例如一次點擊:ACTION_DOWN ACTION_MOVE ACTION_UP,這里就會產(chǎn)生三個事件,這三個事件屬于同一個事件系列,三個事件都會經(jīng)歷以下所說的所有流程
事件在 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法開始,來看一下該方法
// Acitivty
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); // 是個空方法
}
if (getWindow().superDispatchTouchEvent(ev)) { // 將事件傳遞到 PhoneWindow
return true;
}
return onTouchEvent(ev); // 如果 Window 沒有處理則 Activity 自己處理
}
// Activity 的處理邏輯是默認(rèn)關(guān)閉事件,如果需要 Activity 處理,則需要開發(fā)者重寫該方法
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
// PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event); // mDecor 即為 DecorView
}
先稍微梳理一下,Activity 的 dispatchTouchEvent 方法中會調(diào)用 PhoneWindow 中的 superDispatchTouchEvent 方法,如果該方法返回 true 那么事件處理結(jié)束,如果該方法返回 false ,那么調(diào)用 Activity 的 onTouchEvent 方法 Activity 自己來處理事件。
PhoneWindow 中的 superDispatchTouchEvent 方法中直接調(diào)用 mDecor.superDispatchTouchEvent(event) ,這里的 mDecor 就是界面框架中的 DecorView,DecorView 的 superDispatchTouchEvent 方法直接調(diào)用 ViewGroup 的 dispatchTouchEvent 方法,這里就講事件傳遞到了 ViewGroup 中,下面的內(nèi)容就是本篇文章的重中之重,現(xiàn)在開始吧。
1. ViewGroup 中的事件分發(fā)和處理
這里先來稍微介紹一下事件如何在 ViewGroup 中分發(fā),然后在根據(jù)源碼來理解即可,點擊事件到達(dá)頂級 View(一般是 ViewGroup) 后,會調(diào)用 ViewGroup 的 dispatchTouchEvent 來進行事件分發(fā),邏輯如下:
簡單來說,ViewGroup 的 dispatchTouchEvent 方法首先根據(jù) onInterceptTouchEvent 方法以及一些其他條件來判斷是否攔截該事件,如果 ViewGroup 攔截該事件,那么調(diào)用 ViewGroup 的 onTouchEvent 來處理事件,并且不會將事件傳遞到子 View,如果不攔截則將事件傳遞到子 View 來處理。不管是 ViewGroup 自己處理該事件還是傳遞到子 View 處理,dispatchTouchEvent 都會接收事件的處理結(jié)果,并將事件的處理結(jié)果返回到上一層
ViewGroup.dispatchTouchEvent() 完成事件分發(fā),并接收事件處理結(jié)果,最后將事件的處理結(jié)果返回到上一層
ViewGroup.onInterceptTouchEvent() 方法判斷當(dāng)前 ViewGroup 是否攔截此事件
View.onTouchEvent() 事件處理,并將事件的處理結(jié)果返回
好啦,下面就從源碼開始來一點點分析,由于 ViewGroup 的 dispatchTouchEvent 方法比較長,所以下面會一段一段來分析
// ViewGroup.dispatchTouchEvent
{
// Handle an initial down. ACTION_DOWN 事件時將 FLAG_DISALLOW_INTERCEPT 標(biāo)記重置為關(guān)閉
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();
}
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
public boolean onInterceptHoverEvent(MotionEvent event) {
return false;
}
上面這段代碼的作用就是確定 ViewGroup 是否需要攔截事件
先來解釋一下里面的對象,mFirstTouchTarget 默認(rèn)為 null,在有子 View 處理了 ACTION_DOWN 事件時會賦值,一個事件系列中第一個事件到來時 mFirstTouchTarget 肯定為 null
FLAG_DISALLOW_INTERCEPT,一個標(biāo)記,通過子 View 中調(diào)用 getParent().requestDisallowInterceptTouchEvent() 方法修改此標(biāo)記,如果開啟此標(biāo)記,表示 ViewGroup 不會攔截事件,該標(biāo)記在 ACTION_DOWN 事件到來時會重置此標(biāo)記為關(guān)閉狀態(tài)。
由代碼來分析,一個事件系列中,第一次來的總是 ACTION_DOWN,此時 mFirstTouchTarget 也為 null,且 FLAG_DISALLOW_INTERCEPT 為關(guān)閉,此時會調(diào)用 onInterceptTouchEvent(ev) 方法來判斷是否攔截, ViewGroup 默認(rèn)不攔截任何事件。
如果事件不是 ACTION_DOWN ,且 mFirstTouchTarget 為 null,說明在 ACTION_DOWN 事件時沒有子 View 處理了事件或者是 ViewGroup 攔截了 ACTION_DOWN 事件,那么同一系列事件都不會再傳遞到子 View,那么 ViewGroup 直接攔截事件;此時會調(diào)用 dispatchTransformedTouchEvent 方法,該方法中會調(diào)用 super.dispatchTouchEvent(event) ,該方法為父類的 dispatchTouchEvent ,其中會調(diào)用處理事件的方法,下面會分析源碼。最后將處理結(jié)果返回上一層。
如果 mFirstTouchTarget 則說明有處理了 ACTION_DOWN 事件的 View ,則會繼續(xù)通過 onInterceptTouchEvent 來判斷是否需要攔截事件,在判斷是還會受 FLAG_DISALLOW_INTERCEPT 的影響,如果 FLAG_DISALLOW_INTERCEPT 開啟,那么只要 mFirstTouchTarget 有值,ViewGroup 都不會攔截事件,如果 mFirstTouchTarget 關(guān)閉則根據(jù) onInterceptTouchEvent 方法的返回值來決定是否攔截。
上面確定了是否需要攔截事件,接著看 ViewGroup 的 dispatchTouchEvent 方法的源碼中在攔截和非攔截情況下事件是怎么處理的
// ViewGroup.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
// 判斷是否攔截
...
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
if (!canceled && !intercepted) { // 如果不攔截
if (actionMasked == MotionEvent.ACTION_DOWN...) { // ACTION_DOWN 時該判斷為真,會執(zhí)行其中的方法
final int actionIndex = ev.getActionIndex(); // always 0 for down
...
for (int i = childrenCount - 1; i >= 0; i--) { // 遍歷所有子 View
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)) { // 調(diào)用子 View 來處理事件
// 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); // 子 View 處理了事件,則將 mFirstTouchTarget 賦值并終止遍歷子 View
alreadyDispatchedToNewTouchTarget = true; // 將事件處理結(jié)果置為 true 表示已經(jīng)有子 View 處理了事件
break; // 跳出循環(huán)
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
...
}
if (mFirstTouchTarget == null) { // 如果 mFirstTouchTarget 為 null,說明是 ACTION_DOWN 事件且沒有子 View 處理事件,直接調(diào)用 ViewGroup 的 dispatchTransformedTouchEvent ,并且其中調(diào)用父類的 dispatchTouchEvent 方法處理事件并將該方法返回值賦值到處理結(jié)果
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}else {
// 執(zhí)行到這里說明 ViewGroup 攔截了事件或者是 mFirstTouchTarget 不為 null
// 如 ViewGroup 攔截事件就調(diào)用父類的 dispatchTouchEvent 方法處理事件并將該方法返回值賦值到處理結(jié)果
// 如果 ViewGroup 不攔截事件,mFirstTouchTarget 有值,通過 dispatchTransformedTouchEvent 方法調(diào)用調(diào)用 target.child 處理事件,并將該方法返回值賦值到處理結(jié)果
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // 如果已經(jīng)有子 View 處理了事件,則將 true 賦值處理結(jié)果
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
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;
}
}
}
// ViewGroup 有子 View 處理了 ACTION_DOWN 事件時為 mFirstTouchTarget 賦值
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target; // mFirstTouchTarget 賦值
return target;
}
// ViewGroup
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent); // 子 View 為空,調(diào)用父類 dispatchTouchEvent 方法處理事件
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
// 子 View 不為空,調(diào)用子 View 的 dispatchTouchEvent 方法
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
}
這段代碼也很簡單,主要就是根據(jù)上面 ViewGroup 是否攔截此事件以及此事件的事件類型來處理事件,下面就按照源碼的流程來分析。
如果 ViewGroup 不攔截事件且事件為 ACTION_DOWN 時遍歷子 View,尋找出符合條件的子 View 來處理事件,尋找的條件也很簡單,主要就是觸摸事件是否落到該 View 所在區(qū)域與該 View 是否在播動畫 ,如果找到符合條件的子 View,就會調(diào)用 dispatchTransformedTouchEvent ,在該方法中調(diào)用子 View 的 dispatchTouchEvent 方法,將事件傳遞到子 View 中,如果子 Viwe 處理了該事件則為 mFirstTouchTarget 賦將子 View 是否處理了事件的標(biāo)記 alreadyDispatchedToNewTouchTarget 置為 true,然后跳出循環(huán)。如果循環(huán)結(jié)束都沒有子 Viwe 處理事件則什么都不做。
接著判斷如果 mFirstTouchTarget 為 null,這里會調(diào)用 ViewGroup 的 dispatchTransformedTouchEvent 方法,其中會調(diào)用父類的 dispatchTouchEvent 方法處理事件并將處理結(jié)果返回。有兩種情況下 mFirstTouchTarget 為 null,第一種情況是如果事件是 ACTION_DOWN 并且遍歷所有子 View 后沒有子 View 處理事件從而導(dǎo)致 mFirstTouchTarget 為null;第二種情況是 ViewGroup 從 ACTION_DOWN 時就開始攔截事件所以沒有遍歷所有子 View 從而導(dǎo)致任何事件到來時執(zhí)行到這里 mFirstTouchTarget 都為 null。
第 2 點可以說明如果 ViewGroup 在 ACTION_DOWN 事件時攔截,那么 ACTION_DOWN 事件 ViewGroup 會處理,并且同一事件系列中其他事件時,不管 ViewGroup 是否攔截這里都會調(diào)用 ViewGroup 的方法來處理事件。還可以說明 ACTION_DOWN 時如果 ViewGroup 不攔截但是所有子 View 沒有處理事件這時 ViewGroup 會處理事件。這也是唯一一種將事件傳遞到子 View 后子 View 沒處理但是 ViewGroup 會處理的情況
接著判斷 mFirstTouchTarget 不為 null 時,會先判斷子 View 已經(jīng)處理了事件的標(biāo)識是否為 true。因為只有 ACTION_DOWN 事件時且有子 View 處理了事件時才會在前面為該標(biāo)記賦值為 true。如果為 true,說明這是 ACTION_DOWN 事件且 ViewGroup 不攔截并且遍歷子 View 處理事件時有子 View 處理了事件,則將事件處理結(jié)果賦值 true 。如果該標(biāo)記為 false,說明不是 ACTION_DOWN 事件。這時會根據(jù)前面部分的 ViewGroup 是否攔截此事件來判斷,如果 ViewGroup 攔截則調(diào)用 ViewGroup 的方法處理事件并將處理結(jié)果返回,如果 ViewGroup 不攔截此事件,則由 mFirstTouchTarget 標(biāo)記的 View 來處理事件,并將結(jié)果返回。
第 4 點可以看出,如果 ACTION_DOWN 事件被子 View 處理即 mFirstTouchTarget 不為 null 時,如果當(dāng)前事件不是 ACTION_DOWN 且 ViewGroup 不攔截此事件,則會將事件傳遞到子 View 處理,然后不管子 View 是否處理了該事件, ViewGroup 都不會再處理,只會將處理結(jié)果返回到上一層,第 5 點與第 3 點是對比分析的。因為第 3 點中提到的情況是唯一一種將事件傳遞到子 View 后子 View 沒有處理了事件但 ViewGroup 會處理的情況。
分析到這里,ViewGroup 的事件分發(fā)和攔截過程就基本結(jié)束了,所有問題都指向了一個方法 View.dispatchTouchEvent() ,不管是 ViewGroup 處理事件還是子 View 處理事件都會執(zhí)行該方法,我們接下來就分析這個方法干了什么。
2. Viwe 的 dispatchTouchEvent() 方法
首先來看源碼
// View
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
// ListenerInfo是View的靜態(tài)內(nèi)部類,用來定義一堆關(guān)于View的XXXListener等方法
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) { // 如果為 View 設(shè)置了 OnTouchListener 會首先調(diào)用 OnTouchListener.onTouch 方法
result = true;
}
if (!result && onTouchEvent(event)) { // 如果 OnTouchListener.onTouch 方法返回 false ,則執(zhí)行 onTouchEvent 方法
result = true;
}
}
...
return result;
}
// View
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) { // 如果 View 是 DISABLED 的,則直接返回該 View 是否可點擊 CLICKABLE
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);
}
// 如果設(shè)置了代理,類似 OnTouchListener ,則會調(diào)用代理的 onTouchEvent 方法,如果該方法返回 true ,則直接返回處理結(jié)果 true
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { // 只要 View 時候 CLICKABLE 或者 LONG_CLICKABLE 都會判斷為 true
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) { // PerformClick 為一個 Runnable
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) { // 調(diào)用 performClick 方法,通過 post 方法和 Runnable 保證 performClick 執(zhí)行在 UI 線程
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;
}
// View
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo; // onClickListenenr
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;
}
以上呢就是 View 的 dispatchTouchEvent 方法的源碼,該方法會先判斷是否設(shè)置了 OnTouchListener ,如果設(shè)置了則會調(diào)用 OnTouchListener 的 onTouch 方法,如果 onTouch 方法返回 true 則不會調(diào)用 onTouchEvent 方法,如果 onTouch 返回 false 或者沒有設(shè)置 OnTouchListener 則會調(diào)用 onTouchEvent。這里看出 OnTouchListener.onTouch 方法調(diào)用的時機在 View 的 dispatchTouchEvent 方法之前,且 OnTouchListener.onTouch 的優(yōu)先級是高于 onTouchEvent 方法的(也高于 onClickListener.onClick() 方法,接下來下面會分析)。
View 的 onTouchEvent() 方法中,根據(jù) View 的是否可用即 Enable 或者 Disable 來判斷,如果是 Disable ,那么之接返回該 View 是否可點擊 CLICKABLE 或者支持 LONG_CLICKABLE,說明 Disable 不影響 View 是否消耗事件。
接下來,如果 View 設(shè)置了代理,類似 OnTouchListener ,則會調(diào)用代理的 onTouchEvent 方法,如果該方法返回 true ,則直接返回處理結(jié)果 true.
如果 View 是 Enable 的,且為 CLICKABLE 或者 LONG_CLICKABLE 或者 CONTEXT_CLICKABLE 的,則會真正調(diào)用 View 處理事件的方法,我們關(guān)注 ACTION_UP 事件,該事件中會通過 post 和 Runnable 來調(diào)用 performClick 方法,保證該方法執(zhí)行在 UI 線程,performClick 中,如果 View 設(shè)置了 onClickListenenr 會調(diào)用 OnClickListenenr.onClick() 方法,該方法沒有返回值,最后 onTouchEvent 的執(zhí)行結(jié)果。從這里可以得出 OnClickListener.onClick() 方法的執(zhí)行時機在 onTouchEvent 方法中且該事件為 ACITON_UP,由此可以得出 ouTouchEvent 的優(yōu)先級高于 OnClickListenenr.onClick() 方法
OnTouchListener.onTouch() > onTouchEvent() > OnClickListener.onClick()
這里還有一點,就是有關(guān) View 是否是可點擊的,默認(rèn)情況下所有 View 的 LONG_CLICKABLE 都為 false,而 CLICKABLE 屬性則和具體 View 有關(guān),確切的說是如果 View 是可點擊的那么 CLICKABLE 默認(rèn)是 true, 例如:Button。如果 View 是不可點擊的,那么 CLICKABLE 默認(rèn)為 false,例如:TextView。并且 View 的 setOnClickListener 和 setOnLongClickListener 方法都會將對應(yīng)屬性設(shè)置為 true。
到這里事件的基本內(nèi)容就講完了,從整個流程可以看出,事件的分發(fā)過程是隧道式的也就是事件是從最外層的 Activity 一層一層傳遞到 View 中,而事件的處理則是冒泡式的,是從 View 一層一層傳遞到 Activity
下面將展示根據(jù)事件分發(fā)的源碼總結(jié)一些常見但是疑難的結(jié)論,并附加 AndroidStudio 的 Log,如果感興趣的可以自己敲一遍試試看。這些結(jié)論并不是全部的,所有人都可以在開發(fā)過程中根據(jù)源碼總結(jié)出自己的結(jié)論
3. 事件分發(fā)源碼歸納總結(jié)
3.1. 如果 ViewGroup 攔截某事件,則不管是什么事件,都會調(diào)用 ViewGroup 的處理事件的方法來處理事件
3.2. View 開始處理事件時,如果不消耗 ACTION_DOWN 即 dispatchTouchEvent() 返回 false ,事件會返回給父 View 處理,并且同一系列事件都不會交給它處理
ViewGroup 和 View 對 ACTION_DOWN 不處理,則 ACTION_MOVE 和 ACTION_UP 均不會傳遞給 ViewGroup 和 View
如果是 ViewGroup 消耗,則不會傳遞給 View
從源碼分析是因為,系列中其他事件到來時,ViewGroup 的 dispatchTouchEvent() 中 mFirstTouchTarget 為 null,默認(rèn)攔截自己處理
11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
11-29 09:15:03.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 onTouchEvent
11-29 09:15:03.720 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:15:03.720 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 onTouchEvent
3.3. ViewGroup 如果 攔截并消耗 了 ACTION_DOWN 事件,那么同一事件系列中其他事件會直接交給該 ViewGroup 處理,不會再調(diào)用該 ViewGroup 的 onInterceptTouchEvent() 方法
- 源碼分析:系列中其他事件到來時,ViewGroup 的 dispatchTouchEvent() 中 mFirstTouchTarget 為 null,默認(rèn)攔截自己處理
11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
11-29 09:29:23.270 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:29:23.330 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
3.4. 如果是子 View 消耗了事件,除非 View 調(diào)用了父 View 的 requestDisallowInterceptTouchEvent 方法設(shè)置不讓父 View 攔截,否則同一系列中其他事件來臨時在其父 ViewGroup 中還是會判斷是否攔截
源碼分析:即使 mFirstTouchTarget 不為 null,在事件到來時 ViewGroup 的 dispatchTouchEvent 方法中還是會調(diào)用 onInterceptTouchEvent 方法來判斷是否攔截
如果 ViewGroup 的 FLAG_DISALLOW_INTERCEPT 標(biāo)記開啟,則不會攔截事件
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
-------------------我是分割線----------------
11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
11-29 09:23:07.320 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:23:07.490 14790-14790/com.renxl.touchevent I/MainActivity: ViewGroup 的 onTouchEvent
3.5. 當(dāng) View 消耗了 ACTION_DOWN ,如果其父 Viwe 不攔截事件,那么同系列中的其他事件還會傳遞到 View ,這是即使 View 不消耗事件系列中其他事件,其父 View 的 onTouchEvent() 事件也不會被調(diào)用,事件會傳遞到 Activity 的 onTouchEvent() 方法處理
因為在 View 消耗了 ACTION_DOWN 時, mFirstTouchTarget 被賦值,ViewGroup 不攔截情況下,同一系列中其他事件到來時還是會傳遞到該 View
在 GroupView 不攔截事件時只有事件為 ACTION_DOWN 且所有子 View 都沒有處理了事件時才調(diào)用其父類也就是本身 View 的 dispatchTouchEvent() 方法來處理事件
Activity 中只要 DecorView 的 dispatchTouchEvent() 方法返回 false 就會調(diào)用自己的 onTouchEvent() 方法處理事件。
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
11-29 09:59:09.740 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: Activity 的 dispatchTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 dispatchTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: ViewGroup 的 onInterceptTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 dispatchTouchEvent
11-29 09:59:09.880 12049-12049/com.renxl.touchevent I/MainActivity: View 的 onTouchEvent
3.6. 除 ACTION_DOWN 以外,子 View 可以通過設(shè)置 FLAG_DISALLOW_INTERCEPT 標(biāo)記位來影響 GroupView 是否攔截事件
- 前提是 ACTION_DOWN 時 ViewGroup 沒有攔截,并且 View 消耗了 ACTION_DOWN 時的事件 mFirstTouchTarget 被賦值。該
不為 null 時,在 ViewGroup 的 dispatchTouchEvent() 中會根據(jù)該標(biāo)記位判斷是否需要調(diào)用 onInterceptTouchEvent() 方法,如果該標(biāo)記位為 true ,則不會調(diào)用 onInterceptTouchEvent() 方法,即不會攔截
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) { // 判斷標(biāo)記位
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
整個事件分發(fā)的流程還是比較清晰的,只有真正了解了源碼,再遇到事件分發(fā)問題時才能得心應(yīng)手,下面就開始本篇文章的第二個重點,滑動沖突問題的解決
二、View 的滑動沖突
在應(yīng)用中加了滑動效果后,簡單的滑動效果是不會有什么大問題的。不過要是添加復(fù)雜的滑動效果,或者滑動是嵌套的情況時,滑動沖突問題就出現(xiàn)了。這里會先說一下滑動沖突的類型,再根據(jù)事件分發(fā)的原理找到統(tǒng)一的滑動沖突問題的解決方式
1. 滑動沖突的種類
外部跟內(nèi)部滑動的方向不一致,例如 ViewPager 嵌套 ListView 的情況,ViwePager 是左右滑動,ListView 是上下滑動,這樣在有滑動事件時便會出現(xiàn)滑動沖突。當(dāng)然 ViewPager 默認(rèn)幫我們解決了滑動沖突
外部跟內(nèi)部滑動方向一致,例如 ScrollView 嵌套 ListView 的情況,ScrollView 可以上下滑動,ListView 也可以上下滑動,這時候如果有滑動事件系統(tǒng)將不知道用戶到底想滑動哪一層,會出現(xiàn)第二種滑動沖突
上面兩種情況的嵌套,例如 QQ 的側(cè)滑菜單,主頁,聯(lián)系人列表三個 View,側(cè)滑菜單跟首頁的 ViewPager 都可以左右滑動,聯(lián)系人列表 ListView 和 ViewPager 也會產(chǎn)生滑動沖突
下面來看滑動沖突的處理規(guī)則
2. 滑動沖突的處理規(guī)則
對于上面提到的第一種滑動沖突,它的處理規(guī)則比較簡單,當(dāng)用戶左右滑動時讓外部的 View 攔截事件,當(dāng)用戶上下滑動時讓內(nèi)部的 View 攔截事件。也就是根據(jù)滑動的特征來解決滑動沖突。至于如何判斷用戶是左右滑動還是上下滑動,我們可以根據(jù)用戶滑動過程中左右偏移量和上下偏移量的對比來確定,哪個方向的偏移量大判定為哪個方向的滑動。除了通過偏移量對比,還可以使用速度、滑動方向跟水平方向的夾角等來確定。確定了是哪個方向滑動就能決定讓相應(yīng) View 來響應(yīng)滑動事件
對于第二種和第三種沖突,我們不能通過速度、偏移量、夾角等來判斷,但是一般可以在業(yè)務(wù)上找到突破點,比如業(yè)務(wù)上規(guī)定當(dāng)處于某種狀態(tài)時內(nèi)部相應(yīng),當(dāng)處于另一種狀態(tài)時外部相應(yīng),這樣就根據(jù)業(yè)務(wù)確定了相應(yīng)的處理規(guī)則。有了相應(yīng)處理規(guī)則就可以決定讓相應(yīng)的 View 來響應(yīng)滑動事件
3. 滑動沖突的解決方式
上面提到了三種滑動沖突場景,并且根據(jù)每種場景都提出了相應(yīng)的處理原則,當(dāng)處理原則確定之后我們就可以找到一種不依賴具體滑動規(guī)則的通用解決辦法,并且在每種沖突場景時修改有關(guān)滑動規(guī)則的處理邏輯即可。
解決方式主要有外部攔截法,內(nèi)部攔截法 兩種,下面一一來介紹
外部攔截法
外部攔截法是指所有的事件都需要經(jīng)過外部 ViewGroup 的判斷,如果外部 ViewGroup 需要此事件就攔截,如果外部 ViewGroup 不需要此事件就不攔截,外部攔截法需要重寫外部 ViewGroup 的 onInterceptTouchEvent 方法,在內(nèi)部根據(jù)相應(yīng)規(guī)則確定是否攔截即可。
// MyViewGroup
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE:
return intercept();
break;
case MotionEvent.ACTION_UP:
return false;
}
return super.onInterceptTouchEvent(ev);
}
以上代碼即為外部攔截法的模板代碼,在 ACTION_DOWN 時外部 ViewGroup 的 onInterceptTouchEvent 方法必須返回 false,否則內(nèi)部 View 不能接收到事件,這里注意,內(nèi)部 View 處理 ACTION_DOWN 事件必須返回 true,否則將接收不到之后的事件,ACTION_DOWN 之后的事件 ViewGroup 都會進行是否攔截的判斷,intercept() 方法用來判斷是否需要攔截,如果根據(jù)處理規(guī)則判定為需要攔截就返回 true 然后外部 ViewGroup 來處理事件;如果處理規(guī)則判定為不需要攔截就返回 false,讓內(nèi)部 View 來處理事件
內(nèi)部攔截法
內(nèi)部攔截法是指所有的事件外部 ViewGroup 都不攔截,所有事件都傳遞給內(nèi)部 View,內(nèi)部 View 如果需要此事件就直接處理,否則就通過 requestDisallowInterceptTouchEvent 方法來讓外部 ViewGoup 攔截事件,內(nèi)部攔截法較外部攔截發(fā)稍微復(fù)雜一點
// MyViewGroup
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN)
return false;
else return true;
}
// MyView
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
return true;
case MotionEvent.ACTION_MOVE:
if (!intercept())
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
以上代碼即為內(nèi)部攔截法的模板代碼,其中外部 ViewGroup 的 onInterceptTouchEvent 方法中除了 ACTION_DOWN 需要返回 false,其他的事件都必須返回 true,這樣才能再內(nèi)部 View 中通過 requestDisallowInterceptTouchEvent 方法來控制外部 ViewGoup 攔截事件
內(nèi)部 View 中的 onInterceptTouchEvent 方法中 ACTION_DOWN 事件必須返回 true,且需要調(diào)用 requestDisallowInterceptTouchEvent 方法設(shè)置外部 ViewGoup 不攔截其他事件,當(dāng)內(nèi)部 View 不需要其他事件時再次調(diào)用 requestDisallowInterceptTouchEvent 方法設(shè)置外部 ViewGroup 攔截事件。
注意:一旦內(nèi)部 View 設(shè)置外部 ViewGroup 攔截事件,那么同一事件序列中之后的事件都不會再到達(dá)內(nèi)部 View
以上就是外部攔截法和內(nèi)部攔截法的大體結(jié)構(gòu),其中外部攔截法比較簡單,實現(xiàn)的功能也比較全,內(nèi)部攔截法有一定的缺點,所以在使用時最好選擇外部攔截法。
到這里 Android 事件分發(fā)和 View 的滑動沖突的內(nèi)容就結(jié)束啦,看起來很簡單的流程,居然寫了整整五個小時又改了三個小時。盡量表達(dá)的清晰,也盡量將整個事件分發(fā)過程描述清楚。希望可以幫到大家。如果有問題可以留言我們來一起討論。