
Android一文讓你輕松搞定Touch事件分發
原始碼分析
下面,咱們一起通過原始碼,全面決議事件分發機制,即按順序講解:
-
Activity事件分發機制
-
ViewGroup事件分發機制
-
View事件分發機制
Activity事件分發機制
Android事件分發機制首先會將點擊事件傳遞到Activity中,具體是執行dispatchTouchEvent()進行事件分發,
Activity.dispatchTouchEvent()原始碼
/**
* 創建人:帥次
* 創建時間:2021/7/5
* 功能:Activity.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//在這里僅用于ACTION_DOWN的判斷
onUserInteraction();
}
//回傳true
if (getWindow().superDispatchTouchEvent(ev)) {
//Activity.dispatchTouchEvent()就回傳true,則方法結束,
//該點擊事件停止往下傳遞&事件傳遞程序結束(讓爺爺吃了嘿嘿)
return true;
}
return onTouchEvent(ev);
}
Activity.onUserInteraction()原始碼
/**
* 創建人:帥次
* 創建時間:2021/7/6
* 功能:該方法是用戶互動,每當向Activity分派按鍵、觸摸或軌跡球事件時呼叫,
*/
public void onUserInteraction() {
}
Window.superDispatchTouchEvent()原始碼
/**
* 創建人:帥次
* 創建時間:2021/7/6
* 功能:Window.superDispatchTouchEvent屬于抽象方法,
* 用于自定義視窗,如Dialog,傳遞觸摸屏事件進一步向下視圖層次結構,
* 應用程式開發人員應該不需要實作或呼叫它,
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
因為Window是抽象類,咱就繼續挖,就找到了它的唯一實作類「PhoneWindow」,
PhoneWindow.superDispatchTouchEvent()原始碼
// This is the top-level view of the window, containing the window decor.
//這是視窗的頂層View的實體物件,
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//咱們繼續往下看
return mDecor.superDispatchTouchEvent(event);
}
DecorView.superDispatchTouchEvent()原始碼
public boolean superDispatchTouchEvent(MotionEvent event) {
//super呼叫父類dispatchTouchEvent方法,那它的父類是誰呢?
//DecorView extends FrameLayout、FrameLayout extends ViewGroup!
//從上面看出DecorView 是ViewGroup的間接子類,
//看到這里Activity.dispatchTouchEvent()也基本差不多了
//如果ViewGroup.dispatchTouchEvent return true,
//則Activity.dispatchTouchEvent()return true,
//如果ViewGroup.dispatchTouchEvent return false,
//則執行Activity.onTouchEvent(ev),
return super.dispatchTouchEvent(event);
}
Activity.onTouchEvent()原始碼
/**
* 創建人:帥次
* 創建時間:2021/7/6
* 功能:當Touch事件未被其下的任何View消費時呼叫,
*/
public boolean onTouchEvent(MotionEvent event) {
//Window.shouldCloseOnTouch來判斷是否消費,
//那咱就繼續看看Window.shouldCloseOnTouch是干嘛的
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
//已經消費了該事件,則回傳true
return true;
}
//還沒有消費回傳false,默認回傳false
return false;
}
Window.shouldCloseOnTouch()原始碼
//這里支持的最高版本maxTargetSdk = Build.VERSION_CODES.P(28),咱使用SDK30就沒辦法處理了,
//主要是對于處理邊界外點擊事件的判斷:是否是DOWN事件,event的坐標是否在邊界內等
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
//return true:說明事件在邊界外,即 消費事件
return true;
}
//回傳false:在邊界內,即未消費(默認)
return false;
}
Activity.onTouchEvent()到這里就基本結束了,后面原始碼完善在再補充,
Activity原始碼總結
當一個點擊事件發生時,從Activity的事件分發開始(Activity.dispatchTouchEvent()),流程如下:

ViewGroup事件分發機制
從上面Activity的事件分發機制可知,在Activity.dispatchTouchEvent()實作了將事件從Activity->ViewGroup的傳遞,ViewGroup的事件分發機制從dispatchTouchEvent()開始,
在Activity.dispatchTouchEvent()中遺留了ViewGroup.dispatchTouchEvent()什么時候回傳true/false在下面的原始碼分析中找出來,
ViewGroup.dispatchTouchEvent()原始碼
先從宏觀角度,縱覽整個 dispatch 的原始碼如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/**
* 一、檢查當前ViewGroup是否需要攔截事件
* 1、如果事件為DOWN事件,則呼叫onInterceptTouchEvent進行攔截判斷;
* 2、mFirstTouchTarget!=null,代表已經有子View捕獲了這個事件,
子 View 的 dispatchTouchEvent 回傳true就是代表捕獲touch 件,
*/
/**
* 二、將事件分發給子View
* 滿足條件canceled和intercepted都為false,既不取消也不攔截
* 1、actionMasked==MotionEvent.ACTION_DOWN
表明事件主動分發的前提是事件為 DOWN 事件
* 2、通過for回圈,遍歷當前ViewGroup下的所有子View;
* 3、處判斷事件坐標是否在子 View 坐標范圍內,并且子 View 并沒有處在影片狀態;
* 4、呼叫 dispatchTransformedTouchEvent 方法將事件分發給子 View,
如果子 View 捕獲事件成功,則將 mFirstTouchTarget 賦值給子 View,
*/
/**
* 三、根據mFirstTouchTarget再次分發事件
* 3.1、mFirstTouchTarget為null,說明在上述的事件分發中并沒有子 View 對事件進行了捕獲操作,
直接呼叫 dispatchTransformedTouchEvent 方法,并傳入child為 null
最侄訓呼叫 super.dispatchTouchEvent 方法,實際上最侄訓呼叫自身的 onTouchEvent 方法,進行處理touch事件,
結論:如果沒有子 View 捕獲處理 touch 事件,ViewGroup會通過自身的onTouchEvent方法進行處理,
* 3.2、mFirstTouchTarget 不為 null,說明在上述的事件分發中有子 View 對 touch 事件進行了捕獲,
則直接將當前以及后續的事件交給 mFirstTouchTarget 指向的 View 進行處理,
*/
}
下面咱逐層分析:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 是否按下操作 , 最終的對外回傳結果 , 該方法的最侄訓傳值
boolean handled = false;
/*onFilterTouchEventForSecurity以應用安全策略過濾觸摸事件,
/return true分派事件,return false洗掉事件,*/
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
final boolean intercepted;
/**
* 一、檢查當前ViewGroup是否需要攔截事件
* 1、如果事件為DOWN事件,則呼叫onInterceptTouchEvent進行攔截判斷;
* 2、mFirstTouchTarget!=null,代表已經有子View捕獲了這個事件,
子 View 的 dispatchTouchEvent 回傳true就是代表捕獲touch 件,
*/
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
//恢復操作以防它被更改
ev.setAction(action);
} 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;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
/**
* 二、將事件分發給子View
* 滿足條件canceled和intercepted都為false,既不取消也不攔截
*/
if (!canceled && !intercepted) {
/*1、actionMasked==MotionEvent.ACTION_DOWN表明
事件主動分發的前提是事件為 DOWN 事件*/
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//2、通過for回圈,遍歷當前ViewGroup下的所有子View;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//3、處判斷事件坐標是否在子 View 坐標范圍內,并且子 View 并沒有處在影片狀態;
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
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);
/*4、呼叫 dispatchTransformedTouchEvent 方法將事件分發給子 View,
如果子 View 捕獲事件成功,則將 mFirstTouchTarget 賦值給子 View,*/
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;
}
}
if (preorderedList != null) preorderedList.clear();
}
}
}
/**
* 三、根據mFirstTouchTarget再次分發事件
*/
//3.1mFirstTouchTarget為null,
if (mFirstTouchTarget == null) {
/*說明在上述的事件分發中并沒有子 View 對事件進行了捕獲操作,
直接呼叫 dispatchTransformedTouchEvent 方法,并傳入child為 null
最侄訓呼叫 super.dispatchTouchEvent 方法,
實際上最侄訓呼叫自身的 onTouchEvent 方法,進行處理touch事件,*/
/*結論:如果沒有子 View 捕獲處理 touch 事件,
ViewGroup會通過自身的onTouchEvent方法進行處理,*/
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;
/*mFirstTouchTarget 不為 null,說明在上述的事件分發中有子 View 對 touch 事件進行了捕獲,
則直接將當前以及后續的事件交給 mFirstTouchTarget 指向的 View 進行處理,
*/
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
}
return handled;
}
ViewGroup.onInterceptTouchEvent()原始碼
/**
* 創建人:帥次
* 創建時間:2021/7/6
* 前提ViewGroup.dispatchTouchEvent return super.dispatchTouchEvent(ev);
* 功能:是否攔截事件 return true攔截,return false(默認)不攔截
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
ViewGroup.onTouchEvent()原始碼
ViewGroup是沒有onTouchEvent()這個方法的,因為ViewGroup是View的子類,所以ViewGroup可以直接呼叫View的onTouchEvent()方法,這里就不做詳細介紹,下面在View事件分發機制中一起解答,
ViweGroup原始碼總結
Android事件分發傳遞到Acitivity后,總是先傳遞到ViewGroup、再傳遞到View,流程總結如下:(假設已經經過了Acitivity事件分發傳遞并傳遞到ViewGroup)

View的事件分發機制
從上面ViewGroup事件分發機制知道,View事件分發機制從dispatchTouchEvent()開始,
View.dispatchTouchEvent()原始碼
public boolean dispatchTouchEvent(MotionEvent event) {
/*一、(mViewFlags & ENABLED_MASK) == ENABLED
1、該條件是判斷當前點擊的控制元件是否enable
2、由于很多View默認enable,故該條件恒定為true(除非手動設定為false)
*/
/*二、mOnTouchListener != null
1、mOnTouchListener變數在View.setOnTouchListener()里賦值
2、即只要給控制元件注冊了Touch事件,mOnTouchListener就一定被賦值(即不為空)
*/
/*三、mOnTouchListener.onTouch(this, event)
1、即回呼控制元件注冊Touch事件時的onTouch();
2、需手動復寫設定,具體如下(以View為例)
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
MLog.logEvent("MyTouchTrueView.onTouch自行處理:",event);
return true;
// 1、若在onTouch()回傳true,就會讓上述三個條件全部成立,
從而使得View.dispatchTouchEvent()直接回傳true,事件分發結束
// 2、若在onTouch()回傳false,就會使得上述三個條件不全部成立,
從而使得View.dispatchTouchEvent()中跳出if,執行onTouchEvent(event)
// 3、onTouchEvent()原始碼分析
}
});
*/
if ( (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener != null &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
View.onTouchEvent()原始碼
public boolean onTouchEvent(MotionEvent event) {
... // 僅展示關鍵代碼
// 若該控制元件可點擊,則進入switch判斷中
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
// 根據當前事件型別進行判斷處理
switch (event.getAction()) {
// 抬起View
case MotionEvent.ACTION_UP:
performClick();
break;
// 按下
case MotionEvent.ACTION_DOWN:
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
// 取消
case MotionEvent.ACTION_CANCEL:
refreshDrawableState();
removeTapCallback();
break;
// 滑動
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若該控制元件可點擊,就一定回傳true
return true;
}
// 若該控制元件不可點擊,就一定回傳false
return false;
}
public boolean performClick() {
if (mOnClickListener != null) {
// 只要通過setOnClickListener()為控制元件View注冊1個點擊事件
// 那么就會給mOnClickListener變數賦值(即不為空)
// 則會往下回呼onClick() & performClick()回傳true
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
View原始碼總結
Android事件分發傳遞到Acitivity后,總是先傳遞到ViewGroup、再傳遞到View,流程總結如下:(假設已經經過了ViewGroup事件分發傳遞并傳遞到View)

總結
重點分析了 dispatchTouchEvent 的事件的流程機制,這一程序主要分 3 部分:
-
1、判斷是否需要攔截 —> 主要是根據 onInterceptTouchEvent 方法的回傳值來決定是否攔截;
-
2、在 DOWN 事件中將 touch 事件分發給子 View —> 這一程序如果有子 View 捕獲消費了 touch 事件,會對 mFirstTouchTarget 進行賦值;
-
3、最后一步,DOWN、MOVE、UP 事件都會根據 mFirstTouchTarget 是否為 null,決定是自己處理 touch 事件,還是再次分發給子 View,
然后介紹了整個事件分發中的幾個特殊的點,
-
1、DOWN 事件的特殊之處:事件的起點;決定后續事件由誰來消費處理;
-
2、mFirstTouchTarget 的作用:記錄捕獲消費 touch 事件的 View,是一個鏈表結構;
-
3、CANCEL 事件的觸發場景:當父視圖先不攔截,然后在 MOVE 事件中重新攔截,此時子 View 會接收到一個 CANCEL 事件,
以上就是本文的全部內容,希望對大家學習 Android 事件分發有所幫助和啟發,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/291075.html
標籤:其他
