事件傳入#
事件是從 Activity 透過一層層傳遞至 View 中
事件分發概念#
主要是由 3 個方法來完成
分發重點方法 | 功能 | 返回意義 |
---|---|---|
調度觸摸事件:布林值 | 由上層 View 被觸發,傳遞至目標 View | 返回結果會由 、子 View 的 影響 (true: 被處理)onTouchEvent``dispatchTouchEvent |
onInterceptTouchEvent : boolean | 在當前 View 中,用來判斷是否攔截某個事件 | 返回結果表示該事件是否被攔截 (true: 被攔截) |
觸摸事件:布林值 | 當前 View 已經攔截,開始處理是建 | 返回結果代表該事件是否被消耗 (true: 被處理) |
public boolean dispatchTouchEvent(MotionEvent e) {
bool isEventConsume = false;
if(onInterceptTouchEvent(e)) {
isEventConsume = onTouchEvent(e);
} else {
isEventConsume = child.dispatchTouchEvent(e);
}
return isEventConsume;
}
可以看出分發順序 dispatchTouchEvent -> onInterceptTouchEvent,在依照是否消耗來決定之後的走向,dispatchTouchEvent 的遞歸調用,直到找到消耗事件的 View,View 是一個 樹狀結構,若該 View 已經攔截點擊事件,則會觸發 onTouch,onTouch 若沒消耗該事件,才會傳遞給 onTouchEvent 方法,onTouchEvent 內才會有 onClick 事件。
Activity 接收事件#
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 這裡的 getWindow 就是 PhoneWindow 類
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//PhoneWindow.java
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java
//DecorView 繼承於 FrameLayout,而 FrameLayout 並沒有 Override
//dispatchTouchEvent 方法,所以必須往它的父類尋找
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
// ViewGroup.java
// ViewGroup 繼承於 View
// ViewGroup 有重寫dispatchTouchEvent,所以不用繼續往 View 去
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
}
事件處理#
透過上面分析,我們就知道點擊事件是如何傳遞至 DecorView#ViewGroup
中,這裡我們會再分 ViewGroup 點擊事件來分析
先了解幾個 MotionEvent:
MotionEvent 事件 | 動作 | 其他 |
---|---|---|
ACTION_DOWN | 手指下壓 | 又分為攔截、不攔截 |
ACTION_UP | 手指抬起 | 事件結束 |
ACTION_MOVE | 在螢幕滑動 | 會被多次觸發 |
ACTION_CANCEL | 事件取消 | 事件被上層攔截時觸發 |
ViewGroup 處理 - ChildView 攔截 ACTION_DOWN
#
Down 事件只會觸發一次(單點觸控,多點觸控就不只一次),攔截就是該 ViewGroup 自己處理事件,不會對 ChildView 分發
每個點擊事件都是以 Action Down 開始,細節請注意以下的註解,而它主要做的事情有 (這裡會先列出主要的處理項目)
清除先前的事件:注意 resetTouchState 方法,它會在 ViewGroup#dispatchTouchEvent 事件是 ACTION_DOWN 時執行
Note
清除先前的事件:注意 resetTouchState 方法,它會在 ViewGroup#dispatchTouchEvent 事件是 ACTION_DOWN 時執行
// ViewGroup.java
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 取得目前行為
final int action = ev.getAction();
// 與 Mask 進行 and 操作,取得真正的 Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 在 Action Down 時才執行 reset
// @ 追蹤 cancelAndClearTouchTargets 方法
cancelAndClearTouchTargets(ev);
// @ 追蹤 resetTouchState 方法
resetTouchState();
}
}
...
return handled;
}
private void resetTouchState() {
// @ 查看 clearTouchTargets 方法
clearTouchTargets();
resetCancelNextUpFlag(this);
// 清除 FLAG_DISALLOW_INTERCEPT、允許 ViewGroup 攔截
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
// 清除第一個點擊 View 串列的所有事件
private void clearTouchTargets() {
// TouchTarget 是單向鏈表
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
// 回收 TouchTarget 方便重用
target.recycle();
target = next;
} while (target != null);
// 將成員 mFirstTouchTarget 設為 null
mFirstTouchTarget = null;
}
}
是否會呼叫 ViewGroup#onInterceptTouchEvent 方法:會有兩個條件,再加上一個 FLAG 判斷:
- 目前是 ACTION_DOWN 事件
- 已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 mFirstTouchTarget 賦值)
- 目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 時,如果沒有禁止攔截的話,就會執行 onInterceptTouchEvent 方法)
ACTION_DOWN
// ViewGroup.java
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 取得目前行為
final int action = ev.getAction();
// 與 Mask 進行 and 操作,取得真正的 Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Action down 時,初始化 FLAG_DISALLOW_INTERCEPT
final boolean intercepted;
// 這裡有兩個判斷 決定是否呼叫 ViewGroup 自己的 onInterceptTouchEvent 方法
// 1. 目前是 ACTION_DOWN
// 2. 已經有子 View 處理這個點擊事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 判斷是否禁止攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 呼叫自身的 onInterceptTouchEvent 方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
...
return handled;
}
若 ViewGroup 自身沒有攔截,就會 迴圈 ChildView:並逐一執行 ViewGroup#dispatchTransformedTouchEvent
分發給每個 ChildView 處理,若 ChildView 消耗事件則返回 true,則跳出循環,否則往下個 ChildView 詢問。
// ViewGroup
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
// 與 Mask 進行 and 操作,取得真正的 Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Action down 時 初始化 FLAG_DISALLOW_INTERCEPT
final boolean intercepted;
// 判斷自身 ViewGroup 是否可以攔截事件
// 目前假設 ViewGroup 不攔截
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
// 重新排列在該 ViewGroup 中的 ChildView 們的順序
final ArrayList<View> preorderedList =
buildTouchDispatchChildList();
...
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// 是否動畫中 canReceivePointerEvents
// 點擊是否在 child 元素內 isTransformedTouchPointInView
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 獲取點擊中的 View
// 新的 View 事件才會返回 非 null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 給它新的指標,除了它正在處理的指標。
// Child View 已經接收到點擊
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// 重點在 dispatchTransformedTouchEvent 方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ChildView 處理事件成功
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// addTouchTarget 方法
// 內會賦予 mFirstTouchTarget 值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// 跳出循環
break;
}
}
}
}
}
}
...
return handled;
}
ViewGroup#dispatchTransformedTouchEvent
方法:ViewGroup 會透過該方法把事件傳遞給 ChildView,若是傳入的 child 是 null 就呼叫 ViewGroup 父類的 dispatchTouchEvent,不是 null 則呼叫指定 View 的 dispatchTouchEvent (當前情況就是有傳入 View 對象,所以會轉跳到指定 View#dispatchTouchEvent
方法)
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event,
boolean cancel,
View child,
int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
// 如果 Child view 為 null 則回傳給 ViewGroup 的 Parent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
ViewGroup#addTouchTarget
:賦予 成員mFirstTouchTarget
ViewGroup#addTouchTarget
:配備 mFirstTouchTarget
成員
// ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
// 取復用的 TouchTarget 物件
final TouchTarget target = TouchTarget
.obtain(child, pointerIdBits);
// 串接上個事件的 View
target.next = mFirstTouchTarget;
// 第一個 處理點擊事件 的 View
mFirstTouchTarget = target;
return target;
}
在 ChildView 接收並攔截事件後 (透過 方法),會賦予該 ViewGroup#mFirstTouchTarget
值、並跳出循環dispatchTouchEvent
ChildView 處理完事件後,返回到 ViewGroup 繼續處理
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
if (mFirstTouchTarget == null) {
...
} else {
...
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
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;
}
}
}
...
return handled;
}
ViewGroup 處理 - ChildView 不攔截 ACTION_DOWN
#
ViewGroup ChildView 攔截事件:處理方式相同 (上一小節),這裡主要看看不同的部分,該事件由 ViewGroup 自己處理 (dispatchTransformedTouchEvent 方法)
// ViewGroup.java
// 從 ViewGroup 開始
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 在 Action Down 時才執行 reset
cancelAndClearTouchTargets(ev);
resetTouchState();
}
}
...
return handled;
}
清除先前的事件:注意 resetTouchState 方法,它會在ViewGroup#dispatchTouchEvent
,事件是 ACTION_DOWN
時執行,是否會呼叫 ViewGroup#onInterceptTouchEvent
方法:會有兩個條件,再加上一個 FLAG 判斷,目前是 事件ACTION_DOWN
,已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 賦值)mFirstTouchTarget
,目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 時,如果沒有禁止攔截的話,就會執行,onInterceptTouchEvent 方法)ACTION_DOWN
,若 ViewGroup 自身沒有攔截,就會迴圈 ChildView:並逐一執行 ViewGroup#dispatchTransformedTouchEvent
分發給每個 ChildView 處理
// ViewGroup
// 從 ViewGroup 開始
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
if (!canceled && !intercepted) {
// 目前沒有 ChildView 要處理這個事件
}
}
...
}
ChildView 接收但 全部都不攔截事件,ViewGroup#mFirstTouchTarget 為 null
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
// 目前情況,沒有 ChildView 處理事件
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
...
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
// 回到 ViewGroup 的 Parent dispatchTouchEvent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
}
...
return handled;
}
View 收到事件 - 處理 ACTION_UP
#
View 首先分發給 onTouch 若是沒有處理則分發到 onTouchEvent,順序如下
- 最先執行 onTouch 接口,若是已經處理,就 onTouchEvent 就不會接收到事件
onTouch
- 若 onTouch 沒有處理這個事件,就會輪到該 View 的 onTouchEvent 處理事件
onTouchEvent
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
// 事件分發到 onTouch
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
// 判斷該 View 是 enable
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 分析 onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
在 View#onTouchEvent
方法中,可以看到 onClick、onLongClick 接口的呼叫
// View.java
// PerformClick 代表該 View 的點擊事件
private PerformClick mPerformClick;
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 只要設定 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE 其中一個,
// 該 View 可點擊
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 點擊事件的代理 TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
// 長按任務 mHasPerformedLongPress
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除長按 Callback
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 分析 performClickInternal
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
}
}
}
這邊可以看到 View 的點擊事件是透過 Handler 傳送 Click 點擊任務 (Runnable),這樣就不會造成點擊事件堵塞
// View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 將 Click 任務放入 Handler
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
// 分析 performClick
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 執行 OnClick
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
ViewGroup - ACTION_DOWN#
- 判斷事件是否被 ViewGroup 攔截:
ViewGroup 攔截事件 -> 直接走3
ViewGroup 不攔截事件 -> 先走 再走2
3
- ViewGroup 不攔截,並有其中一個 ChildView 攔截事件並處理
ViewGroup#newTouchTarget
被賦予值
分發到自身的 View ( 呼叫自己的父類)super.dispatchTouchEvent
// ViewGroup.java
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
- 事件分發、處理:有兩種情況
所有 ChildView 不攔截這個事件,所以 mFirstTouchTarget 為 null,相當於最後一個 View
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
有 ChildView 攔截,所以 mFirstTouchTarget 不為 null,並且 while 只循環一次,因為 ChildView 已處理 (分發時處理)
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
ACTION_MOVE - 查看收到的 MOVE#
Move 事件仍會先走 DecorView 這個 ViewGroup,在執行 事件時先需要知道幾件事情ACTION_MOVE
,ViewGroup 不會清理點擊的 Flag,TouchTarget 類型的 mFirstTouchTarget
元素 不為空 (因為已經有 ChildView 元素處理),ACTION_MOVE 事件的判斷流程圖如下,它主要做的事情是 ViewGroup 是否攔截事件 (以下預設不攔截事件),不攔截、Move 不分發事件分發 or 處理,同樣先來觀察 ViewGroup#dispatchTouchEvent 方法,並從這裡開始分析,不會進入 reset 環節
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
...
}
}
判斷當前 ViewGroup 是否有禁止攔截 ,如果沒有的話就調用自身的 方法 檢查是否攔截FLAG_DISALLOW_INTERCEPT
onInterceptTouchEvent
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 判斷 ViewGroup 时候禁止攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 預設 ViewGroup 不攔截
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
不是 事件,所以不會進入最初的 事件分發環節 ACTION_DOWN
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 不是 Down 事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
}
}
...
}
ACTION_MOVE
的重點在事件分發
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// alreadyDispatchedToNewTouchTarget 是 false
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 分析 dispatchTransformedTouchEvent 分發給 ChildView
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
}
接下來透過 dispatchTransformedTouchEvent 方法:分發 事件給 ChildView 的 dispatchTouchEventACTION_MOVE
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)
...
if (child == null) {
...
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
}
ACTION_MOVE - ViewGroup 攔截 MOVE#
先了解目前情況:已經有 ChildView 處理 ACTION_DOWN 事件,而我們自訂一個 ViewGroup 並複製 ViewGroup#onInterceptTouchEvent
方法,在 ACTION_ VIEW
時返回 true 攔截
// 自定義 ViewGroup.java
public class MyViewGroup extends View {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_MOVE) {
// ViewGroup 自己消耗事件
return true;
}
return super.onInterceptTouchEvent(ev);
}
}
ACTION_MOVE 事件是多次發生,第一个 ACTION_MOVE (從 ParentView 進來)的目的是:
- 取消 ChildView 事件(事件改為 ACTION_CANCEL)
- 將
ViewGroup#mFirstTouchTarget
置為空,這時 ParentView 是不處理事件的
結果:這時 ChildView 會收到 ACTION_CANCEL 事件(事件被取消,之後都會由 ViewGroup 處理事件)
/**
* ViewGroup.java
*
* 由於覆寫 onInterceptTouchEvent 方法會讓 Move 事件為 ture,所以 cancelChild 也為 ture,
* 傳入 dispatchTransformedTouchEvent 方法的 cancel 參數為 ture,
*
* 這時事件就會被改為 ACTION_CANCEL,下面的 ChildView 就會接收到 ACTION_CANCEL 事件
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// ACTION_MOVE 第一次進入
// intercepted 是 true,所以 cancelChild = true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// mFirstTouchTarget = next,next為 null
// 所以 mFirstTouchTarget = null
if (cancelChild) {
if (predecessor == null) {
// next 为空,所以 mFirstTouchTarget 为空
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// cancel 為 true
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// 改變事件為 ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
...
} else {
// 往下分發 ChildView 的事件就為 cancel
// 事件被上層攔截時觸發
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
return handled;
}
第二個 ACTION_MOVE(從 ParentView 進來),由於 mFirstTouchTarget 為空,所以 ParentView 不會分發
結果:ParentView 自己處理事件
//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 在第一个 ACTION_MOVE 时 mFirstTouchTarget 被置为空
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
...
} else {
intercepted = true;
}
// intercepted = true
if (!canceled && !intercepted) {
...
}
if (mFirstTouchTarget == null) {
// ChildView 为空
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
事件衝突#
為何會發生衝突 ? 因為事件只會有一個,若是響應的組件不是自己要的 View 原件,這時就可以稱之為事件衝突
分為兩種方法處理
- 內部攔截法:需要
ChildView#dispatchTouchEvent
處理 & 需要配合改動父容器 (ParentView) 的 onInterceptTouchEvent 攔截 - 外部攔截法(較常使用):只需要 ParentView 處理
內部攔截 - ChildView 處理#
在 ChildView 中使用 requestDisallowInterceptTouchEvent
方法,控制 FLAG_DISALLOW_INTERCEPT
元素,讓 ParentView 不會往下分發事件
內部攔截作法
ChildView 在適當時機透過 方法,要求 ParentView 不要攔截事件requestDisallowInterceptTouchEvent
public class MyView extends View {
int dispatchX, dispatchY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 要求 ParentView 不攔截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - dispatchX;
int deltaY = y - dispatchY;
// 事件交由 ViewGroup 處理
if(Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
dispatchX = x;
dispatchY = y;
return super.dispatchTouchEvent(event);
}
}
重寫 ParentView 的 方法,設定在 ACTION_DOWN 時不攔截事件,這樣事件才能傳入 ChildView onInterceptTouchEvent
public class MyViewGroup extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN) {
// ViewGroup 不攔截
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
requestDisallowInterceptTouchEvent
細節說明: 為啥 requestDisallowInterceptTouchEvent 無法控制 ACTION_DOWN 事件
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
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);
// 查看 resetTouchState
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // mFirstTouchTarget != null
// 控制 FLAG_DISALLOW_INTERCEPT
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果為 ture 則 ParentView 就不往下分發
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//
}
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
從上面可看出 ViewGroup 時 ACTION_DOWN 使用 resetTouchState () 方法,這裡會清理 這個 Flag,導致事件 ACTION_DOWN 一定會被分發FLAG_DISALLOW_INTERCEPT
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 清理後 FLAG_DISALLOW_INTERCEPT 为空
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 判斷 ViewGroup#onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
...
}
Note
ViewGroup 中覆寫 onInterceptTouchEvent
,讓 ViewGroup 在 ACTION_DOWN 事件時返回 false(不撷取),這樣 ACTION_DOWN 就會分發,這樣才能往下分發到 ChildView,而其他的動作則會攔截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == ACTION_DOWN) {
return false;
}
return true;
}
外部攔截 - ParentView 處理#
由 ParentView 處理事件攔截比較簡單,只需要覆蓋 ParentView#onInterceptTouchEvent
決定哪個時間(條件)攔截事件
public class MyExternalViewGroup extends ViewGroup {
int interceptX, interceptY;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
boolean defaultIntercept = super.onInterceptTouchEvent(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - interceptX;
int deltaY = y - interceptY;
// 水平滑動,攔截
if(Math.abs(deltaX) > Math.abs(deltaY)) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
default:
intercept = defaultIntercept;
break;
}
interceptX = x;
interceptY = y;
return intercept;
}
}