自由筆記-AndroidView模塊之View觸摸事件分發機制分析

相關方法介紹:

dispatchTouchEvent方法:事件的分發。返回值一般由裏面的onTouchEvent方法或者下架View的dispatchTouchEvent方法決定。

onInterceptTouchEvent:事件的攔截,如果Down事件一旦攔截了,那麼之後的move和up事件也會一起攔截。事件不會在向下傳遞。

onTouchEvent:事件的處理。如果在子View中設定了mOnTouchListener,那麼會優先調用mOnTouchListener的方法。如果返回true,

那麼不會在調用onTouchEvent。如果返回false,那麼會繼續走onTouchEvent。如果一個View的down事件沒有

消耗,直接返回的false,那麼其他事件都不會在傳遞到這個View上。事件開始向上傳遞。

 

事件傳遞源頭: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)傳遞下去,如果下面的UI控件沒有消耗該時間

由activity的onToucHEvent去消耗。getWindow().superDispatchTouchEvent(ev)的具體實現如下:

 

@Override

public boolean superDispatchTouchEvent(MotionEvent event) {

return mDecor.superDispatchTouchEvent(event);

}

 

由代碼可知,事件由整個窗口布局的根View DecorView負責進行分發。

public boolean superDispatchTouchEvent(MotionEvent event) {

return super.dispatchTouchEvent(event);

}

 

在 DecorView中,直接調用了super.dispatchTouchEvent(event),而DecorView是一個FrameLayout,即該方法

走入了ViewGroup裏面的dispatchTouchEvent。接下來具體分析該方法

 

第一段: // 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();

}

如果事件是DOWN事件,那麼初始化一些狀態。

 

第二段:

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;

}

 

這裏主要分析攔截動作。從代碼可以看出如果是DOWN事件則一定會進入到onInterceptTouchEvent方法判斷是否攔截

如果down事件被父控件攔截了,那麼mFirstTouchTarget必定會是null,所以onInterceptTouchEvent不會在執行,

默認後續的事件都返回intercepted = true;表示攔截。而如果不是DOWN事件,那麼DOWN事件必須由子控件響應過(mFirstTouchTarget!=null表示之前的DOWN事件被子空間消耗了

否則mFirstTouchTarget會依然是null)纔會繼續判斷是否攔截。如果子控件沒有響應過即子控件沒有消耗過DOWN事件,那麼

該地方不會在進行判斷,直接默認攔截,事件不會在傳遞到下層子控件

 

由此可以得出三個結論:

A、如果父控件攔截了down事件,那麼down事件不會在傳遞給子控件。所有其他事件都不會在傳遞下去並交由父控件處理

B、如果父控件沒有攔截down事件,並且子控件消耗了down事件,那麼後續的move up等事件父控件依然會判斷是否攔截,如果攔截那麼後續的事件不會在往下傳遞,如果攔截move事件,那麼第一個move事件會變成一個cancel事件傳遞到子控件,詳情參見第四段代碼分析

C、如果父控件沒有攔截down事件,並且子控件也沒有消耗down事件,那麼後續的事件不會在傳遞到子控件當中,將有父控件處理。因爲mFirstTouchTarget爲null

 

disallowIntercept該標誌位表示是否攔截,由子控件去調用父控件的requestDisallowInterceptTouchEvent方法設定,如果設定爲true,ViewGroup將無法攔截除了ACTION_DOWN以外的其它事件

1、子view不希望父view攔截事件可以調用mParent.requestDisallowInterceptTouchEvent(true)

2、viewgroup的mGroupFlags在下一個cycle來臨的時候,FLAG_DISALLOW_INTERCEPT標誌位會被清零

3、requestDisallowInterceptTouchEvent這個函數一般不是自己調用的,而是給子View調用的

4、requestDisallowInterceptTouchEvent是解決滑動衝突的大殺器,目前大部分原生控件都是使用

 

 

第三段:

if (!canceled && !intercepted) {

if (newTouchTarget == null && childrenCount != 0) {

final float x = ev.getX(actionIndex);

final float y = ev.getY(actionIndex);

// 找出可以接受事件的子控件

final View[] children = mChildren;

for (int i = childrenCount - 1; i >= 0; i--) {

if (!canViewReceivePointerEvents(child)

|| !isTransformedTouchPointInView(x, y, child, null)) {

ev.setTargetAccessibilityFocus(false);

continue;

}

newTouchTarget = getTouchTarget(child);

if (newTouchTarget != null) {

newTouchTarget.pointerIdBits |= idBitsToAssign;

break;

}

resetCancelNextUpFlag(child);

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

mLastTouchDownTime = ev.getDownTime();

if (preorderedList != null) {

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;

}

}

}

}

 

第三段代碼是核心代碼,主要做了以下幾個事情,前提條件是父控件沒有進行攔截和取消事件:

1、從當前觸摸區域找出可以相應事件的子控件;

2、通過getTouchTarget方法找到觸摸的目標控件,如果之前DOWN事件觸摸過,那麼直接跳出循環,由後面的代碼進行事件傳遞。

3、如果getTouchTarget方法返回一個null,則之前沒有觸摸過,那麼則通過dispatchTransformedTouchEvent方法傳遞事件給對應區域的子控件。

 

dispatchTransformedTouchEvent方法是事件傳遞的關鍵方法,在該方法中,如果傳入的child爲null則表示沒有找到對應的子控件消耗。那麼會由父控件去消耗

但是在第三段代碼中,只有找到了子控件纔會調用dispatchTransformedTouchEvent方法,然後會繼續調用子控件的child.dispatchTouchEvent(event);進入下一個輪訓

 

如果子控件child.dispatchTouchEvent(event)方法返回false,表示子控件沒有消耗該事件,dispatchTransformedTouchEvent也會爲false,那麼newTouchTarget和mFirstTouchTarget都會爲null

這也就是爲什麼第二段代碼會由結論C了。

 

 

第四段:

if (mFirstTouchTarget == null) {

// No touch targets so treat this as an ordinary view.

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

} else {

// 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) {

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;

}

}

 

這裏代碼也很好理解,首先,如果在第三段代碼沒有找到能消耗事件的子控件,或者父控件進行了攔截沒有進入到第三段代碼中

那麼這裏會直接調用handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);

child傳入會null,那麼裏面會直接調用super.dispatchTouchEvent(event);會走到View類中的dispatchTouchEvent方法進行事件傳遞

表示當前觸摸事件由當前父控件進行消耗。

 

而如果在第三段代碼中找到了消耗down事件,那麼在down事件走到這裏之後,會直接返回handled=true。而後續的move和up事件

因爲找到了mFirstTouchTarget,那麼不會再走進入第三段代碼,會調用

if (dispatchTransformedTouchEvent(ev, cancelChild,arget.child, target.pointerIdBits)) {

handled = true;

}

這裏注意,如果進行了MOVE這個觸摸事件攔截,那麼會觸發一個cancel的事件動作傳遞給之前消耗DOWN事件的子控件,並且在後面將mFirstTouchTarget置空

這樣後續的move事件就不會在傳遞給之前的子控件了。所以一旦父控件執行了onInterceptTouchEvent並且返回true,那麼後續的事件就不會再往下分發。

此時由於子控件已經消耗了down事件,如果後續的move和up事件在這裏沒有消耗,dispatchTransformedTouchEvent返回false,那麼handled也爲false,那麼後續的

事件不會在傳遞給父控件去消耗,而是傳遞給activity中的onTouchEvent方法去處理。

 

 

view對touch事件的處理

 

1、先看view如何分發事件:dispatchTouchEvent

if (onFilterTouchEventForSecurity(event)) {

if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {

result = true;

}

//noinspection SimplifiableIfStatement

ListenerInfo li = mListenerInfo;

if (li != null && li.mOnTouchListener != null

&& (mViewFlags & ENABLED_MASK) == ENABLED

&& li.mOnTouchListener.onTouch(this, event)) {

result = true;

}

 

if (!result && onTouchEvent(event)) {

result = true;

}

}

如果view設置的enable爲true並且也設定了touchEvent的監聽器,那麼就會先響應touchEvent的監聽器。如果enable爲false或者是沒有設定監聽器,那麼會響應

view的onTouchEvent的方法

 

1、view的onTouchEvent的方法,如果view設置的enable爲false,那麼會進入下面代碼

if ((viewFlags & ENABLED_MASK) == DISABLED) {

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);

}

這個時候,如果view的clickable和longClickable是true的話,依舊會返回true,也就是說view即使設置的enable爲false,還是能夠響應touchEvent事件的

 

如果view設置的enable爲true的話,那麼進入action的判斷狀態

switch (action) {

case MotionEvent.ACTION_UP:

if (!post(mPerformClick)) {

performClick();

}

break;

case MotionEvent.ACTION_DOWN:

checkForLongClick(0, x, y);

break;

case MotionEvent.ACTION_MOVE:

break;

}

在ACTION_UP的地方,會響應click事件;在ACTION_DOWM的地方,會根據時間響應longClick事件。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章