整理自:《Android 藝術探索》
關於事件傳遞機制部分:點擊事件分發機制 關鍵源碼筆記
1、衝突的幾種場景
- 外部滑動與內部滑動方向不一致
- 外部滑動與內部滑動方向一致
- 上述兩種情況的嵌套
2、解決衝突的前提
制定好規則,即什麼情況由外部的父容器攔截處理,什麼時候分發給內部的子控件處理。
3、解決方法
(1)外部攔截法
即事件先經過父容器的攔截處理,如果父容器需要此事件就攔截,否則就分發給子控件。
該方法的實現需要重寫父容器的 onInterceptTouchEvent()
方法,僞代碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
// 根據實際情況來判斷是否攔截
if (父容器需要攔截) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
//
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
需要注意的幾個點:
-
1、通常情況下,對於 DOWN 事件必須返回 false,因爲如果父容器攔截了 DOWN 事件,則後續的 MOVE、UP 事件就不會分發給子控件處理,而直接交由父容器處理。
-
2、對於 ACTION_UP 事件,在父容器中(
onInterceptTouchEvent()
方法中)必須返回 false,因爲 ACTION_UP 事件本身沒有太多意義。注意,這裏返回 false 是爲了在父容器不攔截 MOVE 事件的時候能夠使得 UP 事件正常傳遞到子控件,因爲如果攔截父控件攔截了 MOVE 事件,則會把 mFirstTouchTarget 清空,此時對於 UP 事件本身就無法傳遞到子控件中了。
但是在父控件沒有攔截 MOVE 事件的時候,如果父容器在 ACTION_UP 事件時返回了 true,則子控件就無法接收到該事件了,此時子控件的 onClick 事件就無法觸發。不過對於父容器來說,即使 ACTION_UP 事件在
onInterceptTouchEvent()
方法中返回了 false,但是此時已經傳遞到父容器了dispatchTouchEvent()
中(因爲是在dispatchTouchEvent()
中通過onInterceptTouchEvent()
來判斷是否攔截事件)。
(2)內部攔截法
父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要該事件就直接消耗掉,否則就交由父容器進行處理。
注意,“父容器不攔截任何事件” 指的是在邏輯上不攔截任何事件,而由子元素的自行判斷。
但是在具體代碼的實現上,則稍有不同:
首先,需要重寫子元素的 dispatchTouchEvent()
方法:
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// true 表示不允許父控件攔截
// 當傳遞到這裏來的時候,表示父控件的 mGroupFlags 已經被重置過了
// 因此這裏設置爲不允許攔截時不會受重置的影響
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此類點擊事件) {
// 不過此次的 MOVE 事件會交由當前控件的 super.onTouchEvent(event),
// 而不會回傳給父容器的 onTouchEvent() 了
// 之後的第一次事件也不會傳遞給父容器的 onTouchEvent(),
// 因此第一次的要轉換成 ACTION_CANCEL 事件傳遞給 mFirstTouchTarget 中的子 View,
// 並清空 mFirstTouchTarget,第二次及以後的纔會正常的傳遞給父容器的 onTouchEvent()。
// 當然上述是在當前代碼邏輯下的場景,否則視具體代碼而定。
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
}
mLastX = x;
mLastY = y;
return super.onTouchEvent(event);
}
在子元素的 onTouchEvent()
中,需要對 ACTION_DOWN
事件調用父容器的 requestDisallowInterceptTouchEvent(true)
,將父容器的 mGroupFlags 設置爲不允許攔截狀態,同時該方法裏面也會依次將父容器的父容器的 mGroupFlags 也設置爲不允許攔截狀態。
然後還需要重寫父容器的 onInterceptTouchEvent()
方法,在實際的代碼上對非 ACTION_DOWN 進行攔截。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
需要進行上述兩步的原因如下:
當父容器的 mGroupFlags 設置爲不允許攔截狀態時,此時對於 ACTION_DOWN 事件是無法干預的,因爲每次 ACTION_DOWN 事件傳遞到父容器時,都會先重置其 mGroupFlags 爲允許攔截狀態(如後面源碼所示)。
而當 mGroupFlags 爲允許攔截狀態時,ACTION_DOWN 事件又會先傳遞到父容器的 onInterceptTouchEvent()
去進行判斷是否攔截,如果攔截了,則會導致子控件無法接收點擊事件。
因此在父容器中必須不攔截 ACTION_DOWN 事件,而對於後續事件,如果子元素設置了:
getParent().requestDisallowInterceptTouchEvent(false);
則表示子元素希望父容器攔截,因此父容器的 onInterceptTouchEvent()
又要對非 ACTION_DOWN 事件默認進行攔截,否則父容器無法正常的對後續事件進行攔截。
補充源碼
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// 重置 mGroupFlags 爲可攔截狀態
resetTouchState();
}
// 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;
}
...
}
...
}