這次說下Android中的事件分發機制
從開始點擊屏幕開始,就會產生從Activity開始到decorview一直到最裏層的view一連串事件傳遞。每一層view或者viewgroup都會首先調用它的dispatchTouchEvent方法,然後判斷是否就在當前一層消費掉事件
view的事件分發
首先上一段僞代碼,是在書上看到的,也是我覺得總結的最好的
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isConsume = false;
if (isViewGroup) {
if (onInterceptTouchEvent(event)) {
isConsume = onTouchEvent(event);
} else {
isConsume = child.dispatchTouchEvent(event);
}
} else {
//isView
isConsume = onTouchEvent(event);
}
return isConsume;
}
如果當前是viewgroup層級,就會判斷 onInterceptTouchEvent 是否爲true,如果爲true,則代表事件要消費在這一層級,不再往下傳遞。接着便執行當前 viewgroup 的onTouchEvent方法。如果onInterceptTouchEvent爲false,則代表事件繼續傳遞到下一層級的 dispatchTouchEvent方法,接着一樣的代碼邏輯,一直到最裏面一層的view。
ok,還沒完哦,到最裏面一層就會直接執行onTouchEvent方法,這時候,view有沒有權利拒絕消費事件呢? 按道理view作爲最底層的,應該是沒有發言權纔對。但是呢,秉着公平公正原則,view也是可以拒絕的,可以在onTouchEvent方法返回false,表示他不想消費這個事件。那麼這個事件又會怎麼處理呢?見下面一段僞代碼:
public void handleTouchEvent(MotionEvent event) {
if (!onTouchEvent(event)) {
getParent.onTouchEvent(event);
}
}
如果view的onTouchEvent方法返回false,那麼它的父容器的onTouchEvent又會被調用,如果父容器的onTouchEvent又返回false,則又交給上一級。一直到最上層,也就是Activity的onTouchEvent被調用。
至此,消費流程完畢
但是,關於onTouch,onTouchEvent和onClick又是怎麼樣的調用關係呢?
那就再來一段僞代碼:
public void consumeEvent(MotionEvent event) {
if (setOnTouchListener) {
onTouch();
if (!onTouch()) {
onTouchEvent(event);
}
} else {
onTouchEvent(event);
}
if (setOnClickListener) {
onClick();
}
}
當某一層view的onInterceptTouchEvent被調用,則代表當前層級要消費事件。如果它的onTouchListener被設置了的話,則onTouch會被調用,如果onTouch的返回值返回true,則onTouchEvent不會被調用。如果返回false或者沒有設置onTouchListener,則會繼續調用onTouchEvent。而onClick方法則是設置了onClickListener則會被正常調用。
這裏用一張流程圖總結下:
源碼分析
一個觸摸事件,首先是傳到Activity層級,然後傳到根view,通過一層層的viewgroup最終到底最裏面一層的view,我們來一層層解析
Activity(dispatchTouchEvent)
直接上代碼
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public void onUserInteraction() {
}
這裏可以看到,onUserInteraction方法是空的,主要是調用了getWindow().superDispatchTouchEvent(ev)方法,返回true,就代表事件消費了。返回false,就代表下層沒人處理,那就直接到了activity的onTouchEvent方法,這點根之前的消費傳遞也是吻合的。
繼續看看superDispatchTouchEvent方法,然後就走到了PhoneWindow的superDispatchTouchEvent方法,以及DecorView的superDispatchTouchEvent,看看代碼:
//PhoneWindow.java
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
這裏可以看到,依次經過了PhoneWindow到達了DecorView,DecorView是activity的根view,也是setcontentView所設置的view的父view,它是繼承自FrameLayout。所以這裏super.dispatchTouchEvent(event)方法,其實就是走到了viewgroup的dispatchTouchEvent
方法。
ViewGroup(dispatchTouchEvent)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
// Check for interception,表示是否攔截的字段
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT標誌是通過requestDisallowInterceptTouchEvent設置
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;
}
//mFirstTouchTarget賦值
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
} else {
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
continue;
}
}
}
}
這裏截取了部分關鍵的代碼,首先是兩個條件
-
actionMasked == MotionEvent.ACTION_DOWN
-
mFirstTouchTarget != null
如果滿足了其中一個條件纔會繼續走下去,執行onInterceptTouchEvent方法等,否則就直接intercepted = true,表示攔截。
第一個條件很明顯,就是表示當前事件位按下事件(ACTION_DOWN)
第二個條件是個字段,根據下面的代碼可以得知,當後面有view消費掉事件的時候,這個mFirstTouchTarget
字段就會賦值,否則就爲空。
所以什麼意思呢,當ACTION_DOWN事件時候,一定會執行到後面代碼。當其他事件來的時候,要看當前viewgroup是否消費了事件,如果當前viewgroup已經消費了事件,沒傳到子view,那麼mFirstTouchTarget
字段就爲空,所以就不會執行到後面的代碼,就直接消費掉所有事件了。
這就符合了之前的所說的一種機制:
某個view一旦開始攔截,那麼後續事件就全部就給它處理了,也不會執行onInterceptTouchEvent方法了
但是,兩個條件滿足了一個,就能執行到onInterceptTouchEvent了嗎?不一定,這裏看到還有一個判斷條件:disallowIntercept
。這個字段是由requestDisallowInterceptTouchEvent方法
設置的,後面我們會講到,主要用於滑動衝突,意思就是子view告訴你不想讓你攔截,那麼你就不攔截了,直接返回false。
ok,繼續看源碼,之前的內容我們瞭解到,如果viewgroup不攔截事件,應該會傳遞給子view,那在哪裏傳的呢?繼續看看dispatchTouchEvent
的代碼:
if (!canceled && !intercepted) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
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();
}
}
}
這裏可以看到,進行了一個子view的便利,其中,如果滿足兩個條件中的一個,就跳出。否則就執行dispatchTransformedTouchEvent
方法。先看看這兩個條件:
- !child.canReceivePointerEvents()
- !isTransformedTouchPointInView(x, y, child, null)
看名字是看不出啥了,直接看代碼吧:
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
哦,原來是這個意思。canReceivePointerEvents方法就代表view是不是可以接受點擊事件,比如是不是在播放動畫。而isTransformedTouchPointInView方法代表點擊事件的座標是不是在這個view的區域上面。
ok,如果條件都滿足,就執行到dispatchTransformedTouchEvent
方法了:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
這個方法大家應該都猜到了,其實就是執行了child.dispatchTouchEvent(event)。也就是下一層view的dispatchTouchEvent
方法唄,開始事件的層級傳遞。
View(dispatchTouchEvent)
到view 層級的時候,自然就執行的view的dispatchTouchEvent
,上代碼
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
這裏可以看到,首先會判斷li.mOnTouchListener != null
,如果不爲空,就會執行onTouch方法。
根據onTouch方法返回的結果,如果爲false,result就爲false,那麼onTouchEvent纔會執行。這個邏輯也是符合我們之前說的傳遞方式。
最後我們再看看view的onTouchEvent
都做了什麼事:
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
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 (!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) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
}
return true;
}
從代碼可以得知,如果設置了CLICKABLE或者LONG_CLICKABLE,那麼這個view就會消費事件,並且執行performClickInternal方法,然後執行到performClick方法。這個performClick方法大家應該都很熟悉,就是觸發點擊的方法,其實內部就是執行了onClick方法。
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
至此,源代碼也看的差不多了,內部其實有很多細節,這裏也就不一一說明了,大家有空可以去研究下。
事件分發的應用(requestDisallowInterceptTouchEvent)
那既然學會了事件分發機制,我們實際工作中會怎麼應用呢?其實最常見的就是解決滑動衝突的問題。一般有兩種解決辦法:
- 一種是外部攔截:從父view端處理,根據情況決定事件是否分發到子view
- 一種是內部攔截:從子view端處理,根據情況決定是否阻止父view進行攔截,其中的關鍵就是
requestDisallowInterceptTouchEvent
方法。
第一種方法,其實就是在onInterceptTouchEvnet方法裏面進行判斷返回true還是返回false。
第二種方法,就是用到了requestDisallowInterceptTouchEvent
方法,這個方法的意思就是讓父view不要去攔截事件了,在dispatchTouchEvent方法裏面就有這個標誌位:FLAG_DISALLOW_INTERCEPT,如果disallowIntercept字段爲true,就不會去執行onInterceptTouchEvent方法,而是返回false,不攔截事件。
上代碼:
//外部攔截法:父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
//父view攔截條件
boolean parentCanIntercept;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
外部攔截很簡單,就是判斷條件,然後決定是否進行攔截。
//父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
//子view.java
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//父view攔截條件
boolean parentCanIntercept;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
感覺內部攔截有點複雜呀,還要重寫父view的方法,這裏分析下,爲什麼要去這麼寫:
- 父view ACTION_DOWN的時候,不能攔截,因爲如果攔截,那麼後續事件也就跟子view無關了
- 父view 其他事件的時候,要返回true,表示攔截。因爲onInterceptTouchEvent方法的調用是被FLAG_DISALLOW_INTERCEPT標誌位所控制,所以子view需要父view攔截的時候,纔會走到這個onInterceptTouchEvent方法中來,那麼這時候要保證方法中一定是要攔截的。
至此,事件的分發機制也就說的差不多了。有說的不對的地方望指正,謝謝。🙏
你的一個👍,就是我分享的動力❤️。