文章目錄
- 一.Activity的層級結構
- 二.事件分發的基礎認識
- 1.事件分發是什么
- 2.事件分發程序中的三個重要方法
- 二.事件分發的程序了解
- 1.圖解:
- 1.1基本程序圖解
- 1.2 關于ACTION_MOVE 和 ACTION_UP
- 2. demo驗證上述程序
- 三.onTouch、onClick、onLongClick的呼叫順序
- 四.事件分發原始碼分析
- 1.Activity的事件分發
- 2.ViewGroup的事件分發機制
- 3.View的事件分發機制
- 五.事件分發的總結
- 六.參考資料
一.Activity的層級結構
在了解View的事件分發之前,先了解下Activity的層級結構,便于更好的理解事件的傳遞順序,
層級結構圖

二.事件分發的基礎認識
1.事件分發是什么
事件分發就是對MotionEvent事件進行分發的程序,即當一個MotionEvent產生后,系統需要把這個事件傳遞(處理)給一個具體的View,這個程序就是分發程序,
2.事件分發程序中的三個重要方法
| 方法 | 作用 | 呼叫時刻 | 回傳值 |
|---|---|---|---|
| dispathchTouchEvent | 進行事件的分發 | 事件傳遞給當前View時呼叫 | 是否消耗當前事件 |
| onInterceptTouchEvent | 判斷是否攔截某個事件 | 在ViewGroup的dispatchTouchEvent()內部呼叫 | 表示是否攔截當前事件 |
| onTouchEvent | 處理點擊事件 | 在dispathchTouchEvent內部呼叫 | 表示是否消耗當前事件 |
三者之間的關系用偽代碼表示
public boolen dispatchTouchEvent(){
boolen consume = false;
if(onInterceptTouchEvent){
consume = onTouchEvent(ev); //如果被攔截,呼叫當前viewGroup的onTouchEvent
}else{
consume = child.dispatchTouchEvent(); //如果未被攔截,呼叫當前的子view的dispatchTouchEvent,即事件傳遞給子view
}
return consume;
}
二.事件分發的程序了解
事件是用戶與螢屏發生互動時產生的,而Activity則是Android中負責與用戶發生互動的組件,所以事件的傳遞,首先是到達Activity,再通過內部傳遞之后,到達我們的布局檔案中的layout和View,事件發生之后,需要進行回應處理,再傳遞的程序中,由上往下,都有可能有機會處理一個事件序列,如果從Activity往下,到最終的View,事件都沒有得到處理,則事件又從下往上,回到Activity,如果回到Activity之后,Activity沒有處理這個事件,那么這個事件就會自動結束,
1.圖解:
1.1基本程序圖解

對上圖的小總結:
- 當事件傳遞到一個ViewGroup上面時,ViewGroup會觸發dispatchTouchEvent方法,隨后呼叫onInterceptTouchEvent方法確認是否攔截此事件,最后如果事件是自己來處理的話,則呼叫onTouchEvent方法,
- 在ViewGroup類中,onInterceptTouchEvent方法總是回傳false,表示默認是不攔截事件的,除非去重寫ViewGroup類來回傳true,而onTouchEvent方法的回傳值表示是否消費(回傳true則消費)此事件,消費的意思就是說ViewGroup自己處理了這個事件,不再傳遞到上一層的onTouchEvent去,
- 在View中,與ViewGroup相比,同樣有dispatchTouchEvent方法和onTouchEvent方法,但是沒有onInterceptTouchEvent這個方法,因為在一個View中,已經是View樹的葉子節點,它沒有下一級的視圖嵌套,所以不需要決定是否攔截事件,它自己就可以處理事件了,
- 在View類中,只要該View是可以點擊的,那么默認都會在onTouchEvent回傳true,表示自己消費了這個事件,不再傳遞到上一級ViewGroup去,
1.2 關于ACTION_MOVE 和 ACTION_UP
上面的講解都是針對的ACTION_DOWN,ACTION_MOVE和ACTION_UP和ACTION_DOWN在傳遞程序中和ACTION_DOWN并不相同,簡單來說,只有前一個事件回傳了true時,才會收到ACTION_MOVE和ACTION_UP的事件,并且而最侄訓將ACTION_MOVE和ACTION_UP分發到消費到ACTION_DOWN的View手中,在分發的程序中,ACTION_MOVE和ACTION_UP與ACTION_DOWN分發的路線可能不回完全相同,
例如:
紅色的箭頭代表ACTION_DOWN 事件的流向
藍色的箭頭代表ACTION_MOVE 和 ACTION_UP 事件的流向



總結:如果在同一個事件序列里面,如果ACTION.DOWN事件不被這個View做出消耗,則后面陸續的事件序列則不會傳遞到這個View來
2. demo驗證上述程序
我們分別創建RelativeLayoutA、RelativeLayoutB,都繼承自RelativeLayout,也等同于是ViewGroup,再創建一個MyView繼承自Button類,也等同于是繼承View,
在RelativeLayoutA類,RelativeLayoutB類中重寫上面提到的三個方法,分別列印出他們的方法名,在MyView類中重寫dispatchTouchEvent方法和onTouchEvent方法,列印他們的方法名,
//RelativeLayoutA的代碼,RelativeLayoutB和MyView與這類似,這里不展示
public class RelativeLayoutA extends RelativeLayout {
private String TAG = "RelativeLayoutA";
public RelativeLayoutA(Context context) {
super(context);
}
public RelativeLayoutA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RelativeLayoutA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
}
布局檔案:
在RelativeLayoutA中嵌套RelativeLayoutB,RelativeLayoutB中嵌套MyView
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.three_viewtext.RelativeLayoutA
android:layout_width="300dp"
android:layout_height="300dp"
android:id="@+id/RelativeLayoutA"
android:layout_centerInParent="true"
android:background="#ffff00">
<com.example.three_viewtext.RelativeLayoutB
android:layout_width="150dp"
android:layout_height="150dp"
android:id="@+id/RelativeLayoutB"
android:layout_centerInParent="true"
android:background="#00ff00">
<com.example.three_viewtext.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/Button"
android:text="按鈕"
android:layout_centerInParent="true"
/>
</com.example.three_viewtext.RelativeLayoutB>
</com.example.three_viewtext.RelativeLayoutA>
</RelativeLayout>
-
第一種情況,直接運行程式點擊按鈕,列印日志如下:
在點擊按鈕時的事件列是:DOWN->IP,如上圖,上半部分為DOWN的事件呼叫順序,下半部分為UP的事件呼叫順序,
因為Button默認是可以點擊的(即使我們并沒有設定點擊監聽事件),所以MyView列印出了onTouchEvent,隨后回傳了true,這個ACTION.DOWN事件就被MyView消耗掉了,
-
第二種情況, 把MyView的onTouchEvent事件回傳false,編譯運行后點擊中間的按鈕,再看下列印:

可以看到,從Activity->view事件都沒有得到處理,則事件又從下往上,回到Activity,log只是列印出了ACTION.DOWN的列印,并沒有像上面的log一樣列印出ACTION.UP,UP事件沒有傳遞到ViewGroup和View中
在這個Log里面由于MyView不處理事件,而RelativeLayoutB和RelativeLayoutA其實也是不處理自己事件的,最后交由了更高級別的ViewGroup(Activity)去回應了,所以后面的ACTION.UP不會再傳遞到這幾個控制元件上來了, -
在RelativeLayoutB中讓onInterceptTouchEvent回傳true,表示RelativeLayoutB會攔截事件自己處理,不分發給下一級View樹處理,編譯運行后點擊中間的按鈕,我們再來看看Log:
從這個Log中可以看出,MyView并沒有列印出來,說明他沒有接收到事件,因為RelativeLayoutB已經把事件給攔截了,就不再分發給MyView,而RelativeLayoutB把事件攔截了后自己呼叫onTouchEvent,默認是沒有消耗事件的,所以才會再呼叫RelativeLayoutB的onTouchEvent方法,同樣,和2一樣,UP事件沒有傳遞到ViewGroup和View中,
注意事件攔截和事件消費是兩回事,事件攔截說的是不把事件發給下一級View,而事件消費說的是處理完這個事件還要不要讓上一級也處理,如果消費了事件那么就不會再讓上一級處理這個事件,
三.onTouch、onClick、onLongClick的呼叫順序
setOnTouchListener方法,通過設定監聽后可以在觸摸的時候回呼onTouch方法
在剛才的demo中,設定View的onTouch、onClick監聽,,這里只做一個總結:
- 在日常中我們給View設定點擊事件其實回應優先級是最低的,因為他需要同時接收到ACTION_DOWN和ACTION_UP事件后才會觸發,而onTouch方法則是在設定監聽后,只要有事件到來,則會觸發一次,它比onTouchEvent優先被回應,
- onTouch比onClick方法多了一個回傳值,其回傳值也表示了是否消耗事件,如果回傳了true則不會再呼叫onTouchEvent方法
- onClick方法是在onTouchEvent里面被回呼的,如果onTouch回傳了true,onTouchEvent不會被呼叫,那么onClick也就不會被呼叫,
- 當View接收到ACTION_DOWN的時候,并且不松開大概0.5s的時候(log從onTouchEvent到onLongClick的執行時間差大概就是0.5s)會執行onLongClick,當接收到ACTION_UP的時候再執行onClick,而如果onLongClick方法中回傳了true,則onClick就不會再執行,
四.事件分發原始碼分析
事件分發其實包含了三部分的事件分發,即:
- Activity的事件分發
- ViewGroup的事件分發
- View的事件分發,
1.Activity的事件分發
當一個點擊操作發生時,事件最先是傳遞給當前的Activity,由Activity的dispatchTouchEvent進行事件委派
//Activity的dispatchTouchEvent原始碼
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); //空方法
}
//委派給DecorView
if (getWindow().superDispatchTouchEvent(ev)) {
//如果下層處理了事件,回傳true結束
return true;
}
//事件沒有得到處理,呼叫Activity的onTouchEvent
return onTouchEvent(ev);
}
/*
getWindow()獲得是Window的抽象類,而window的唯一實作就PhoneWindow,下面是PhoneWindow.superDispatchTouchEvent原始碼
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
//Decor即是DecorView,所以phoneWindow將事件傳遞給了DecorView
return mDecor.superDispatchTouchEvent(event);
}
/*
DecorView.superDispatchTouchEvent原始碼
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
//等于開始呼叫ViewGroup的dispatchTouchEvent,即到此為止,
//事件從Activity傳到了ViewGroup
}
/*
Activity.onTouchEvent
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
總結:
2.ViewGroup的事件分發機制
這里只展示其中一些關鍵代碼
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
final boolean intercepted;// 檢查是否要攔截
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
/*
FLAG_DISALLOW_INTERCEPT設定后,ViewGroup無法欄除ACTION_DOWN之外的其他點擊直接,
原因:在ViewGroup分發事件時,如果是ACTION_DOWN,會重置這個標志位
設定方法: requestDisallowInterceptTouchEvent
*/
if (!disallowIntercept) {// 只有允許攔截才執行onInterceptTouchEvent方法
intercepted = onInterceptTouchEvent(ev);//呼叫onInterceptTouchEvent方法
ev.setAction(action); // restore action in case it was changed
} else {
//不允許攔截,直接設為false
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
/*
在這種情況下,actionMasked != ACTION_DOWN && mFirstTouchTarget == null
說明沒有一個子View要去處理ACTION_DOWN事件,導致mFirstTouchTarget還是空的,
沒有指向要處理事件的子View,所以接下來的其他事件,都不再繼續分發下去了,而且攔截了事件讓自己處理,
*/
intercepted = true;
}
- FLAG_DISALLOW_INTERCEPT標志位
//原始碼2662行
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
FLAG_DISALLOW_INTERCEPT是一個標記位,通過 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 可以設定它,一般是用在子View里面,如果FLAG_DISALLOW_INTERCEPT被設定了后,ViewGroup就無法攔截ACTION_DOWN以外的其他事件,這是因為ACTION_DOWN事件會重置FLAG_DISALLOW_INTERCEPT標記位
//原始碼(2650行),
if (actionMasked == MotionEvent.ACTION_DOWN) {
//down事件,做重置狀態的操作,
cancelAndClearTouchTargets(ev);
resetTouchState(); //會對FLAG_DISALLOW_INTERCEPT進行重置
}
首先cancelAndClearTouchTargets方法會遍歷清除所有的target,導致mFirstTouchTarget=null,然后在呼叫resetTouchState,重置觸摸狀態
resetTouchState的原始碼:
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
//重置標志位
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
分析到這里可得出如下結論:
- 當ViewGroup要攔截事件后,那么后續到來的事件會直接交給他處理,而不會再呼叫onInterceptTouchEvent詢問是否攔截,
- 當呼叫requestDisallowInterceptTouchEvent方法設定了不允許攔截的標記位后,在ACTION_DOWN事件的時候會被重置掉而不起作用,也就是說requestDisallowInterceptTouchEvent方法針對的是ACTION_DOWN以外的其他事件,并且是在不攔截ACTION_DOWN事件的情況下才會起作用,
3. ViewGroup不攔截事件時,事件會向下分發交給他的子View進行處理
//原始碼2722行
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);
/*
判斷子元素是否能接收到點擊事件,兩個衡量標準
child.canReceivePointerEvents():是否在播影片
isTransformedTouchPointInView()點擊事件的坐標是否落在子元素區域內
*/
if(!child.canReceivePointerEvents()
||!isTransformedTouchPointInView(x,y,child,null)){
continue;
}
newTouchTarget=getTouchTarget(child);// 查找child對應的TouchTarget
if(newTouchTarget!=null){
// 比如在同一個child上按下了多跟手指
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
//子View已經在自己的范圍內得到了觸摸,
//除了它正在處理的那個,給它一個新的指標,
newTouchTarget.pointerIdBits|=idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent()實際上呼叫子元素的dispatchTouchEvent
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添加到touch鏈的頭部
// 會更新 mFirstTouchTarget
newTouchTarget=addTouchTarget(child,idBitsToAssign);
alreadyDispatchedToNewTouchTarget=true;// 記錄ACTION_DOWN事件已經被處理了,
break;
}
}
總結:

3.View的事件分發機制
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//首先判斷是否設定OnTouchListener,如果OnTouchListener中的onTouch方法中回傳true,那么onTouchEvent就不會被呼叫,
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;// 如果被onTouch處理了,則直接回傳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;
}
總結:

五.事件分發的總結
- 同一事件序列是指從手指接觸到螢屏的那一刻起,到手指離開的螢屏的那一瞬間結束,在這個程序中所產生的一系列的事件,這個事件以down事件開始,中間含有數量不等的move事件,最終以up事件結束
- 正常情況下,一個事件序列只能被一個View攔截且消耗,這一條的原因可以參考(3),因為一旦一個元素攔截了此事件,那么同一事件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但通過特殊手段可以做到,比如同一個事件序列中的事件不能分別由兩個View同時處理,但通過特殊手段/可以做到,比如一個View將本該自己處理的事情通過onTouchEvent強行傳遞給其他View處理,
- 某個View一旦決定攔截,那么這一個事件序列都只能由他處理,并且onInterceptTouchEvent都不會在被呼叫,
- 某個View如果不消耗ACTION_DOWN事件,那么同一事件序列的其他事件也不會交給他處理,并且把事件重新交給它的父元素處理,即呼叫父元素的onTouchEvent,
- ViewGroup默認不攔截任何事件,即原始碼中onInterceptTouchEvent方法中默認 回傳false
- View沒有onIntercept方法,一旦有點擊事件傳遞給他,那么他的onTouchEvent就會被呼叫
- View的onTouchEvent默認都會消耗事件,除非他是不可點擊事件,(clickbale和longClickable同時為false),Veiw的longClickable默認屬性都是false,chickable屬性要分情況,比如button的clicjable屬性默認是true,而TextView的clickanle的默認屬性為fasle,
六.參考資料
View的事件傳遞及分發機制
Android之View篇2————View的事件分發
Android事件處理機制:事件分發、傳遞、攔截、處理機制的原理分析(上)
Android事件處理機制:事件分發、傳遞、攔截、處理機制的原理分析(中)
Android事件處理機制:事件分發、傳遞、攔截、處理機制的原理分析(下)
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/267406.html
標籤:其他
