老規矩,看源碼一定要帶著問題、推測或結論去看,不能看到太深,要能剎得車~不然會陷在源碼中。像我這樣的小菜鳥,總是在看過源碼之后才知道Android源碼設計的強大,所以在此總結一下源碼中是如何處理事件分發的。
注意:本文中所有源碼分析部分均基于 API25 版本,由于安卓系統源碼改變很多,可能與之前版本有所不同,但基本流程都是一致的。
本文將對ViewGroup的dispatchTouchEvent()做了一個比較全面的注釋,并形成了簡化后的偽代碼輔助理解。
單個View分析
首先我們需要先引發一個問題。需要自定義一個View,重載構造方法,并在Activity中實現兩個接口,然后來看一下他都點擊處理情況.
button= (WidgetButton) findViewById(R.id.btn_content);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG,"事件");
}
});
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(TAG,"Touch");
return false;
}
});
當我們點擊這個自定義控件時
I/widget: Touch
I/widget: Touch
I/widget: Touch
I/widget: 事件
但是如果我們將OnTouchListener中的OnTouch返回true
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(TAG,"Touch");
return true;
}
});
點擊控件的結果是
I/widget: Touch
I/widget: Touch
I/widget: Touch
好了問題出現
為什么setOnTouchListener中的onTouch返回true后OnClick就不執行了呢?我們點進去看setOnTouchListener和setOnClickListener做了什么?
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
這里發現將監聽賦給了mOnTouchListener和mOnClickListener,我們暫時記住這兩個變量名。
我們要知道當我們點擊自定義View控件時,首先會調用父類(View)的dispatchTouchEvent方法。然后看一下這里邊幾句重要的代碼。
9999 boolean result = false;
...
10017 if (li != null && li.mOnTouchListener != null
10018 && (mViewFlags & ENABLED_MASK) == ENABLED
10019 && li.mOnTouchListener.onTouch(this, event)) {
10020 result = true;
10021 }
10023 if (!result && onTouchEvent(event)) {
result = true;
10025 }
這里我們可以看到之前記住的那個變量名mOnTouchListener,這里判斷如果它的onTouch返回true的時候result賦值為ture。(在源碼中如果有些變量不太明白干什么的,千萬不要糾結,這里判斷的這幾個條件都會滿足)繼續往下看,如過result為false,因為用這里用的是&&所以在第一個條件未滿足的情況下是不會調用第二的條件的,但是重點就在這個第二個條件中(onTouchEvent方法)。
11185 case MotionEvent.ACTION_UP:
...
11216 performClick();
在performClick方法的5637行會調用 li.mOnClickListener.onClick(this); 是不是之前賦值的那個變量名。所以這里可以知道,當mOnTouchListener.onTouch()為true時就不會調用onTouchEvent()方法,但是mOnClickListener.onClick()在onTouchEvent() - up事件 - performClick()方法中,所以也不會調用。
Activity - ViewGroup 分析
為什么要從Activity說起呢。事件收集之后最先傳遞給 Activity, 然后依次向下傳遞。
首先自定義ViewGroup,我這里繼承的是RelativeLayout因為它也是繼承自ViewGroup并且沒有對觸摸事件進行處理,然后重載構造方法,并重寫三個與觸摸事件有關的方法
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG," WidgetViewGroup onTouchEvent "+event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(TAG," WidgetViewGroup onInterceptTouchEvent "+ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG," WidgetViewGroup dispatchTouchEvent"+ ev.getAction());
return super.dispatchTouchEvent(ev);
}
在MainActivity中重寫兩個與觸摸事件有關的方法,為什么是兩個呢,因為在Activity中沒有Intercept事件,因為沒有意義,如果攔截了會導致點擊什么效果都沒有。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG," MainActivity dispatchTouchEvent "+ ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG," MainActivity onTouchEvent "+ event.getAction());
return super.onTouchEvent(event);
}
點擊屏幕時輸出如下 這里0表示Down 2表示Move 1表示Up
I/widget: MainActivity dispatchTouchEvent 0
I/widget: WidgetViewGroup dispatchTouchEvent0
I/widget: WidgetViewGroup onInterceptTouchEvent 0
I/widget: WidgetViewGroup onTouchEvent 0
I/widget: MainActivity onTouchEvent 0
I/widget: MainActivity dispatchTouchEvent 2
I/widget: MainActivity onTouchEvent 2
I/widget: MainActivity dispatchTouchEvent 1
I/widget: MainActivity onTouchEvent 1
這樣就知道了觸摸事件之間執行順序的情況。
好了問題出現
為什么會這樣調用呢?那么先從Activity的dispatchTouchEvent看起。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//這個方法是個空方法,是給用戶進行重寫的
}
//這里說明如果這個判斷返回true就不執行Activity的onTouchEvent()方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
這里有簡單的注釋,那判斷中的方法是什么呢?進去看一下。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
可以發現他是Window抽象類的一個抽象方法,在文件開始的注釋中說明了Window抽象類僅有一個實現類PhoneWindow,那么就得去PhoneWidow找這個方法的實現。
PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
這里調用的DecorView的方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
這里調用的父類的方法,但是父類FrameLayout并沒有這個方法,只好再去FrameLayout的類ViewGroup中去找。最終在ViewGroup中找到了相應的方法(dispatchTouchEvent),并做了很多操作,那么我們看一下做了什么操作。2145行開始
//判斷是否有觸摸設備 比如觸摸筆一類的
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
//這是一個賦值功能 AccessibilityService 可以不用手指進行點擊 比如搶紅包
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {//事件安全檢查
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/**
* 第一步:對于ACTION_DOWN進行處理(Handle an initial down)
* 因為ACTION_DOWN是一系列事件的開端,當是ACTION_DOWN時進行一些初始化操作.
* 從源碼的注釋也可以看出來:清除以往的Touch狀態(state)開始新的手勢(gesture)
* cancelAndClearTouchTargets(ev)中有一個非常重要的操作:
* 將mFirstTouchTarget設置為null!!!!
* 隨后在resetTouchState()中重置Touch狀態標識
* */
// Handle an initial down.
//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.
//這個方法用于清理標志,里邊有一個TouchTarget判讀是否是第一次觸發Down,如果是什么都不處理,如果不是將一些標志全部置為初始化
cancelAndClearTouchTargets(ev);
//重置所有接觸狀態,準備一個新的觸摸循環
resetTouchState();
}
/**
* 第二步:檢查是否要攔截(Check for interception)
* 在dispatchTouchEvent(MotionEventev)這段代碼中
* 使用變量intercepted來標記ViewGroup是否攔截Touch事件的傳遞.
* 該變量在后續代碼中起著很重要的作用.
*
* 攔截 intercepted =true
*/
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//正因為這個||所以在move事件是也能夠攔截
//這里的 (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0是否為true取決于 requestDisallowInterceptTouchEvent(?)
// 這個方法是用于是否允許父類進行攔截 true 為不讓父控件攔截。這里個人理解為判斷子View是否有調用這個方法。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//這里的攔截并沒有返回 只是將intercepted 設為 實現方法中返回的值 標示是否做攔截
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;
}
/**
* 第三步:檢查cancel(Check for cancelation)
*
*/
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
/**
* 第四步:事件分發(Update list of touch targets for pointer down, if needed)
*/
//不是ACTION_CANCEL并且ViewGroup的攔截標志位intercepted為false(不攔截)
//intercepted 未被攔截 如果攔截了會跳過這里邊的方法
if (!canceled && !intercepted) {
//之前說的那個賦值功能
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//判斷是否是Down事件 還有一個 多手指的判斷
//第二次Move事件時不會執行這里的代碼 所以不會遍歷子控件,由于move事件頻繁調用 這是對move事件的一個優化
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 = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
//重排序 按找Z來排序
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
//是否是用戶自己排序
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//事件分發 倒序 遍歷 重排序后的集合 后添加的先接收
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//拿到第i個View 相當于 preorderedList.get(childIndex )
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
//賦值功能
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//這里判斷這個View能不能夠接收事件
//clickable Invisiable 點擊事件不在 view范圍內(通過pointInView) 正在動畫
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
/**
* 執行到了下面
* child 絕對會接受到事件
*/
//如果只分析Down操作 這里返回空 如果是move它就不為null
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.
//如果能夠找到接收事件的target直接break
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
/**
* 真正做事件分發
* child 不為空
*
*
* 如果子類 onTouch 返回true (根據子類onTouchEvent 來返回)
*/
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();
//將child添加到Target 并賦給mFirstTouchTarget 做為將要接收move事件的view
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();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
//如果該ViewGroup攔截事件為true 那么mFirstTouchTarget 為null (會跳過上面的方法直接到這)
//如果沒有攔截并且點擊鎖定子view那么mFilrstTouchTarget不為null
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
/**
* 真正做事件分發
* child 為空
*/
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;
//如果第三個參數為null將會調用ViewGroup的onTouchEvent(),如果不為null將不會調用ViewGroup的onTouchEvent()調用的是子View的onTouchEvent()
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;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
這就是整個dispatchTouchEvent()的代碼,重要的都做了注釋。還有一部分重要的代碼是
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
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);
}
}
之前一直提到的真正的事件分布,傳view和不傳view 的情況,如過view為Null調用ViewGroup的super.dispatchTouchEvent(),相當于調用ViewGroup的onTouchEvent(),如果不為null這調用子view的dispatchTouchEvent()。
經過源碼的分析,可以簡化一個偽源碼,如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (!onInterceptTouchEvent(ev)) {//如果沒有攔截
//之前會判斷是否存在這個 符合要求的child
//這個返回結果相當于子View的onTouchEvent的返回結果
if(child.dispatchTouchEvent(transformedEvent)){
//target是鎖定事件的那個view之后的move事件就發生在它身上 mFirstTouchTarget 默認為null
mFirstTouchTarget = target;
}
}
//這個判斷相當于沒有攔截或不存在符合條件的子view
if(mFirstTouchTarget == null){
handled = onTouchEvent();//調用自身的onTouchEvent();
}else{
handled = true;
}
return handled;
}
通過源碼可以直接的了解出 Activity - ViewGroup - View 的一個事件傳遞情況。這樣也就解釋了上面的問題(個人理解,如果是ViewGroup - ViewGroup 將會是一個遞歸的情況)
總結
- 事件分發中用到了責任鏈模式,上層View可以攔截事件自己處理,也可以發布給子View,如果子View處理不了還可以返回到上層View進行處理,既保證了事件的有序性,又非常的靈活。
- View 的 dispatchTouchEvent 主要用于調度自身的監聽器和 onTouchEvent。
- View的事件的調度順序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
- 不論 View 自身是否注冊點擊事件,只要 View 是可點擊的就會消費事件。
- 事件是否被消費由返回值決定,true 表示消費,false 表示不消費,與是否使用了事件無關。
- ViewGroup 中可能有多個 ChildView 時,將事件分配給包含點擊位置的 ChildView。
- 只要接受 ACTION_DOWN 就意味著接受所有的事件,拒絕 ACTION_DOWN 則不會收到后續內容。
- (源碼理解)ViewGroup的dispatchTouchEvent 方法處理了所有觸摸操作,onInterceptTouchEvent和onTouchEvent這兩個方法并沒有返回結果,只是返回true、false告訴dispatchTouchEvent應該做啥。
這篇文章是在我學習的基礎上進行了總結,可想而知我還是個很小的菜鳥,如果其中有錯誤還請指出,我會盡快修改文章,并改正自己的理解,謝謝。