1.View點擊事件的傳遞規則
首先,用戶觸摸屏幕的時候系統必須對事件做出相應反應.而這個事件就是產生一個MotionEvent然后按照一定的規則傳遞給每一個View去進行相應的處理.這就是我們所謂的事件分發了.點擊事件的分發主要設計一下幾個主要的方法:
用來進行事件的分發.如果有事件傳遞給當前的View,那么該View一定會去調用這個方法
返回值受當前View的onTouchEvent和下級View的dispatchTouchEvent的影響
返回值表示當前的事件時候已經被處理完成
public boolean dispatchTouchEvent(MotionEvent e)
這個方法在上面那個方法中內部調用,用來判斷是否攔截這個事件
如果當前View攔截了某個事件 那么在同一個事件序列中就不會再次被調用
返回值表示是否攔截當前事件
public boolean onInterceptTouchEvent(MotionEvent e)
也是在第一個方法中去調用 用來處理攔截下來的事件
返回值為真表示該事件已經被處理 否則 沒有處理
在同一事件序列中View無法再次接收到事件
public boolean onTouchEvent(MotionEvent e)
可以用一段偽代碼來表示一下三者的關系:
public boolean dispatchTouchEvent(MotionEvent e) {
boolean consume = false;
if (onInterceptTouchEvent(e)) {
consume = onTouchEvent(e);
} else {
cnsume = childView.dispathcTouchEvent(e);
}
return cosume;
}
從上面的代碼中我們基本可以總結出這樣的結論:
對于一個根ViewGroup來說,當接收到一個MotionEvent的時候:
調用dispatchTouchEvent方法調用onInterceptTouchEvent方法返回值為true,則表示攔截當前事件,調用onTouchEvent來處理這個事件
返回值為false,則當前事件將會被傳遞給childView,childView繼續調用dispatchTouchEvent方法
如此往復,直至事件被處理.
當一個View需要處理一個事件的時候
如果該View設置了onTouchListener,則會調用onTouch方法如果onTouch方法返回false,則去調用onTouchEvent如果設置了onClickListener,那么在onTouchEvent方法中將會調用onClick方法
反之,onTouchEvent則不會被調用
當一個事件產生的時候,它的傳遞過程遵循這樣的過程:Activity->Windows->View;事件總是先傳遞給Activity,Activity在傳遞給Windows,Windows在傳遞給View;如果View將事件處理了,則該事件相應就結束了.否則,事件將一級一級的繼續返回,最終會傳遞給Activity的onTouchEvent處理.
@Override
public boolean onTouchEvent(MotionEvent event) {
Toast.makeText(MainActivity.this, event.getAction() + "我是Activity", Toast.LENGTH_SHORT).show();
return super.onTouchEvent(event);
}
當你沒有給任何控件設置相應事件的時候(也就是都會返回false),那么你就會看到Activity的onTouchEvent被調用了.
在開發藝術這本書中提到了幾個結論:
1.同一個事件序列是指手指接觸屏幕那一刻起,到手指離開屏幕那一刻結束,在這個過程中所有產生的事件都屬于這一個事件序列.包括一個ACTION_DOWN,一個ACTION_UP和n個ACTION_MOVE;
2.某一個View一旦決定攔截事件,那么這一事件序列都只能由它來處理.
這個結論認真想了一下,似乎有點問題;假如這個View我設置了onTouchListener,但是我依然返回false,這個事件序列仍然會傳遞給父View,當然了,這個View也只能接收到一個ACTION_DOWN事件,ACTION_UP和ACTION_MOVE不會接收到.假如這個View同時還設置了onClickListener,onTouchEvent返回false的時候事件會交給onTouchEvent處理這個事件,不會在交給父View處理了.這個問題還是需要結合源碼來看一下;
3.某個View一旦開始處理一個事件,如果它不消耗ACTION_DOWN事件,那么同一時間序列也不會交給他來處理了.
4.如果View不消耗ACTION_DWON以外的事件,那么這個點擊事件就會消失,不會在交還給父View處理.最后會交回activity處理.
5.ViewGroup默認不攔截任何事件,他的onInterceptTouch方法默認返回false;View沒有onInterceptTouch方法,收到事件他的onTouchEvent事件就會被調用.
6.View的onTouchEvent默認都會消耗事件(返回true),除非他是不可點擊的(clickable和龍Clickable同時為false).View的longClickable默認都是false,clickable分情況.
2.Activity對事件的分發
點擊事件用MotionEvent來表示,當點擊事件發生的時候,事件最先傳遞給當前Activity,由Activity的dispatchTouchEvent來進行事件派發,具體工作是由Activity內部的Windows來處理的.Windows會將事件傳遞給decor view,即當前View的root view.先看一下Activity的dispatchTouchEvent源碼:
public boolean diapatchTouchEvent(MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindows.superDispatchTouchEvent()) {
return true;
}
return onTouchEvent(e);
}
這里有個onUserInteraction
方法,點進去發現這個方法是一個空方法,文檔是這樣寫的:
Called whenever a key, touch, or trackball event is dispatched to the activity. Implement this method if you wish to know that the user has interacted with the device in some way while your activity is running. This callback and onUserLeaveHint() are intended to help activities manage status bar notifications intelligently; specifically, for helping activities determine the proper time to cancel a notfication. All calls to your activity’s onUserLeaveHint() callback will be accompanied by calls to onUserInteraction(). This ensures that your activity will be told of relevant user activity such as pulling down the notification pane and touching an item there. Note that this callback will be invoked for the touch down action that begins a touch gesture, but may not be invoked for the touch-moved and touch-up actions that follow.
大體意思就是說onUserInteraction是幫助我們知道用戶開始和屏幕進行交互的回調函數.另外,還會和onUserLeaveHint
一起更加智能的管理狀態欄通知.
Called as part of the activity lifecycle when an activity is about to go into the background as the result of user choice. For example, when the user presses the Home key, onUserLeaveHint() will be called, but when an incoming phone call causes the in-call Activity to be automatically brought to the foreground, onUserLeaveHint() will not be called on the activity being interrupted. In cases when it is invoked, this method is called right before the activity’s onPause() callback. This callback and onUserInteraction() are intended to help activities manage status bar notifications intelligently; specifically, for helping activities determine the proper time to cancel a notfication.
這里這兩個方法對我們不是很重要了,根據分析可以知道,activity通過windows來分發事件,當所有的view都沒有接收處理事件的時候,activity就會自己調用自己的onTouchEvent()來處理這個事件了.
繼續看getWindows.superDispatchTouchEvent()
,window
是個抽象類,
Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc. The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.
根據文檔的描述,我們可以知道window
的唯一實現類是android.view.PhoneWindow
,那么他的dispatchTouchEvent
是怎么實現的呢?
//PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent e) {
return mDecor.superDispatchTouchEvent(e);
}
這里的思路很清晰了,直接分發給mDecor處理,那么DecorView是什么東西呢?關于activity的層次點擊這里
//this is the top-level view of the window,containing the window decor(裝飾)
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
private DecorView mDecor;
@Override
public final View getDecorView() {
if (null == mDecor) {
installDecor();
}
return mDecor;
}
到了這事件會繼續分發,到我們通過setContentView設置的ViewGroup那里繼續處理.
3.ViewGroup對事件的分發
// Check for interception.
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;
}
這段代碼是來判斷是否要來攔截當前的點擊事件的.可以看出當這個事件是一個事件序列的開端,也就是一個ACTION_DOWN,就用去調用onInterceptTouch方法去判斷是否要去攔截這個事件;或者當mFirstTouchTarget不為空的時候,也會去判斷.相反,就不會攔截了.這也說明了一個事件如果View不去處理他的ACTION_DWON事件為什么就能在去處理其他的事件了.
當ViewGroup不處理事件要繼續分發的時候代碼是這樣的:
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.
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
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);
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();
}
邏輯也比清晰,遍歷ViewGroup的所有子View,找出能接受事件的所有元素;要滿足兩個條件:1.坐標是否落在子View中2.是否正在播放動畫.滿足這兩個條件,就會分發給他來處理,要是返回了true就表示事件已經被處理,mFirstTouchTarget就會被賦值并終止此次分發,否則繼續分發過程.
4.View對點擊事件的處理過程
//View#dispatchTouchEvent
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
這里相對比較簡單,首先判斷是否設置了onTouchListener,如果設置了就去調用onTouch方法,如果返回了false,則去調用onTouchEvent方法; 在view設置了onClickListener或者onLongClickListener后,會自動將CLICKABLE或者LONG_CLICKABLE變成ture;
最后三張圖十分清晰
效果圖如下:
View不處理事件流程圖(View沒有消費事件)
View處理事件

事件攔截

附錄
以后每個知識點的實踐學習代碼會上傳到我的GitHub,歡迎大家一起學習-.-~