博客原文鏈接:https://zhujun2730.github.io/2015/11/08/touchevent/
對于大多數(shù)Android開發(fā)者來說却音,Android的事件分發(fā)機制一直以來都是一塊心頭病改抡。似懂非懂的狀態(tài),應(yīng)該是大多數(shù)人的真實寫照系瓢。最近在看任玉剛老師寫的《Android開發(fā)藝術(shù)探索》阿纤,算是做個讀書筆記吧,希望能提供多一點啟發(fā)夷陋、多一點角度的理解欠拾。
一、事件分發(fā)機制的一些概念
事件分發(fā)的本質(zhì):其實就是對MotionEvent事件的分發(fā)過程骗绕。
1.1 為什么要有事件機制 藐窄?
當你在一個布局中,有一個LinearLayout酬土,里面又有一個小的LinearLayout荆忍,然后在這個小的LinearLayout中又有一個View,這個時候撤缴。這個時候刹枉,你點擊這個View,為什么LinearLayout不會響應(yīng)屈呕?其實你點擊的也是LinearLayout的區(qū)域啊微宝。帶著這個疑問,就可以猜想到了虎眨,事件分發(fā)機制蟋软,其實就是為了統(tǒng)一協(xié)調(diào)這些view的事件。
在這里安利一篇愛哥的《Android事件分發(fā)完全解析之為什么是她》嗽桩,這篇較生動的講解了事件機制的由來岳守,十分推薦一看。
1.2 MotionEvent 主要分為以下幾個事件類型:
- ACTION_DOWN 手指開始觸摸到屏幕的那一刻響應(yīng)的是DOWN事件
- ACTION_MOVE 接著手指在屏幕上移動響應(yīng)的是MOVE事件
- ACTION_UP 手指從屏幕上松開的那一刻響應(yīng)的是UP事件
所以事件順序是: ACTION_DOWN -> ACTION_MOVE -> ACTION_UP
1.3 事件分發(fā)機制的三個主要方法:
-
public boolean dispatchTouchEvent(MotionEvent event) —— 分發(fā)事件
作用是用來進行事件的分發(fā)碌冶。一般在這個方法里必須寫 return super.dispatchTouchEvent 棺耍。如果不寫super.dispatchTouchEvent,而直接改成return true 或者 false种樱,則事件傳遞到這里時便終止了,既不會繼續(xù)分發(fā)也不會回傳給父元素俊卤。
-
public boolean onInterceptTouchEvent(MotionEvent event) —— 攔截事件
只有ViewGroup才有這個方法嫩挤。View只有dispatchTouchEvent和onTouchEvent兩個方法。因為View沒有子View消恍,所以不需要攔截事件岂昭。而ViewGroup里面可以包裹子View,所以通過onInterceptTouchEvent方法狠怨,ViewGroup可以實現(xiàn)攔截约啊,攔截了的話邑遏,ViewGroup就不會把事件繼續(xù)分發(fā)給子View了,也就是說在這個ViewGroup中的子View都不會響應(yīng)到任何事件了恰矩。onInterceptTouchEvent 返回true時记盒,表示ViewGroup會攔截事件。
-
public boolean onTouchEvent(MotionEvent event) —— 消費事件
onTouchEvent 返回true時外傅,表示事件被消費掉了纪吮。一旦事件被消費掉了,其他父元素的onTouchEvent方法都不會被調(diào)用萎胰。如果沒有人消耗事件碾盟,則最終當前Activity會消耗掉。則下次的MOVE技竟、UP事件都不會再傳下去了冰肴。
需要注意的一些事項:
- 一般我們在自定義ViewGroup時不會攔截Down事件,因為一旦攔截了Down事件榔组,那么后續(xù)的Move和Up事件都不會再傳遞下去到子元素了熙尉,事件以后都會只交給ViewGroup這里。
- 一個Down事件分發(fā)完了之后瓷患,還有回傳的過程骡尽。因為一個事件分發(fā)包括了Action_Down、Action_Move擅编、Action_Up這幾個動作攀细。當手指觸摸到屏幕的那一刻,首先分發(fā)Action_Down事件爱态,事件分發(fā)完后還要回傳回去谭贪,然后繼續(xù)從頭開始分發(fā),執(zhí)行下一個Aciton_Move操作锦担,直到執(zhí)行完Action_Up事件俭识,整個事件分發(fā)過程便到此結(jié)束。
1.4 事件分發(fā)機制的三個主要方法的關(guān)系:
【注:ViewGroupA洞渔、ViewGroupB套媚、View的布局結(jié)構(gòu)參考下面的布局圖】
當事件分發(fā)到ViewGroupA時,會執(zhí)行到ViewGroupA的dispatchTouchEvent方法磁椒。剛剛提到了堤瘤。在這里必須寫成return super.dispatchTouchEvent(ev);因為事件的分發(fā)需要ViewGroupA 在父類ViewGroup的dispatchTouchEvent中才能進行事件分發(fā)。否則不這樣寫浆熔,事件根本無法繼續(xù)分發(fā)下去本辐。
class ViewGroupA {
public boolean dispatchTouchEvent(MotionEvent ev){
return super.dispatchTouchEvent(ev);
}
}
在ViewGroup的dispatchTouchEvent源碼中,簡單化的歸納了事件分發(fā)的整個流程。該代碼出自任老師之手慎皱。
【當consume 返回 true 時老虫,表明事件已經(jīng)被消費了∶6啵】
class ViewGroup {
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(MotionEvent ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
}
當ViewGroupA 的 onInterceptTouchEvent 方法返回true時祈匙,表示它要攔截事件,此時會執(zhí)行它自己的onTouchEvent方法地梨。當返回false時菊卷,表明它不想攔截,則事件會傳遞給子View child宝剖。于是開始執(zhí)行child.dispatchTouchEvent(ev)洁闰。
我們來看View的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
從View的dispatchTouchEvent方法中可以得出一個結(jié)論:
事件一旦分發(fā)到了View万细,則默認一定會執(zhí)行它的onTouchEvent方法扑眉,除非符合了if的三個條件
所以View的 onTouchEvent 方法如果返回true,則它的dispatchTouchEvent的返回值也會返回true赖钞。在ViewGroup 的dispatchTouchEvent 中則 consume 的值為true腰素,表示事件被消費。
結(jié)論:View / ViewGroup 事件消費是在onTouchEvent方法中被消費的雪营。
二弓千、事件分發(fā)機制的流程
下面通過demo案例來演示,詳細的說明事件分發(fā)的流程献起。ViewGroupA包裹ViewGroupB,ViewGroupB里面又包裹一個View洋访。我們現(xiàn)在來分析下它的事件分發(fā)執(zhí)行的流程。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<me.anany.ViewGroupA
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_bright">
<me.anany.ViewGroupB
android:layout_width="300dp"
android:layout_height="300dp"
android:background="@android:color/holo_green_dark">
<me.anany.CustomView
android:id="@+id/btn"
android:text="Button"
android:background="@android:color/holo_red_dark"
android:layout_width="100dp"
android:layout_height="100dp"
/>
</me.anany.ViewGroupB>
</me.anany.ViewGroupA>
</RelativeLayout>
2.1 點擊View區(qū)域但View不消耗事件
當一個事件產(chǎn)生后谴餐,它的傳遞流程是從:Activity -> Window ->View
下圖描述了姻政,當點擊View時,事件分發(fā)的執(zhí)行流程岂嗓、以及事件回傳的流程汁展。
流程圖解析:
- 事件分發(fā)
當在屏幕上點擊一個View時,首先執(zhí)行到的是MainActivity的dispatchTouchEvent方法厌殉,這里便是事件分發(fā)的起點食绿。紅色箭頭流向便是事件分發(fā)的流向。
事件傳遞到ViewGroupA時公罕,因為它不攔截事件器紧,所以它要先去問它的子控件ViewGroupB是否要消費事件,然后將事件分發(fā)給ViewGroupB熏兄。事件到了ViewGroupB時,它不攔截事件,所以它也要先去問它的子控件們要不要消費事件摩桶,然后將事件分發(fā)給View桥状。事件到了View時開始執(zhí)行dispatchTouchEvent,因為已經(jīng)到了最底層了硝清,View接下來便開始執(zhí)行onTouchEvent方法來決定是否消費事件辅斟。
- 事件回傳
由于View沒有消費事件,所以它開始回傳信息芦拿,(紫色箭頭的流向便是事件回傳方向)士飒,以告訴ViewGroupB我不消費事件了,view 的 onTouchEvent 便return false蔗崎。然后ViewGroupB才開始有權(quán)利決定我是否要開始消費事件(因為它已經(jīng)問過它的子控件是否要消費事件了酵幕,而它的子控件并沒有消費),所以開始執(zhí)行ViewGroupB的onTouchEvent方法缓苛,由于ViewGroupB也不消費事件芳撒,所以它也 return false 。事件繼續(xù)回傳給ViewGroupA未桥,這個時候它終于開始有權(quán)利決定我是否要消費事件了笔刹,所以開始執(zhí)行ViewGroupA的onTouchEvent方法,由于ViewGroupA也不感興趣不消費事件冬耿,所以它也return false舌菜。最終你們這些孩兒們都不消費事件,那事件最終只能扔給MainActivity去消費了亦镶。
下面這段Log日月,便記錄了事件分發(fā)的整個過程。由于ViewGroupA染乌、ViewGroupB山孔、View 在 Action_Down事件時,就沒有消費事件荷憋。所以后續(xù)的事件MOVE台颠、UP都只由MainActivity來處理了。
2.2 點擊View區(qū)域且View消耗事件
流程圖解析:
- 事件分發(fā)
當在屏幕上點擊一個View時勒庄,首先執(zhí)行到的是MainActivity的dispatchTouchEvent方法串前,這里便是事件分發(fā)的起點。紅色箭頭流向便是事件分發(fā)的流向实蔽。
事件傳遞到ViewGroupA時荡碾,因為它不攔截事件,所以它要先去問它的子控件ViewGroupB是否要消費事件局装,然后將事件分發(fā)給ViewGroupB坛吁。事件到了ViewGroupB時劳殖,它不攔截事件,所以它也要先去問它的子控件們要不要消費事件拨脉,然后將事件分發(fā)給View哆姻。事件到了View時開始執(zhí)行dispatchTouchEvent,因為已經(jīng)到了最底層了玫膀,View接下來便開始執(zhí)行onTouchEvent方法來決定是否消費事件矛缨。
- 事件回傳
由于View消費了事件,所以它開始回傳帖旨,(紫色箭頭的流向便是事件回傳方向)箕昭,以告訴ViewGroupB我已經(jīng)消費事件了,view 的 onTouchEvent 便return true解阅。然后ViewGroupB 收到了View return true 就知道事件已經(jīng)被View消費掉了落竹,所以不會執(zhí)行ViewGroupB的onTouchEvent方法,只能往上回傳 return true 去告訴ViewGroupA 事件已經(jīng)被消費掉了瓮钥,你沒機會了 筋量。然后事件繼續(xù)回傳給ViewGroupA,A收到return true 便知道 事件被消費了碉熄,所以它也return true桨武。最終事件回傳到了MainActivity,由于事件被消費了锈津,所以不會執(zhí)行MainActivity的onTouchEvent方法呀酸。接下來又開始執(zhí)行Move事件了,流程又和之前的一樣重新開始處理琼梆。
下面這段Log性誉,便記錄了事件分發(fā)的整個過程。當Down事件被View消費后茎杂,事件會重新開始從ViewGroupA错览、ViewGroupB 這樣下來進行分發(fā),直到UP事件結(jié)束煌往。
結(jié)論:onTouchEvent被View消費后倾哺,ViewGroupA、ViewGroupB的onTouchEvent都不會執(zhí)行
2.3 點擊ViewGroupB區(qū)域但不消耗事件
這里的流程就不細說了刽脖,前面已經(jīng)詳細描述了兩遍了羞海。總的來說就是曲管,ViewGroupB和ViewGroupA都不消費事件却邓,那最終只能交給老大MainActivity去消費事件。
先看Log院水,為什么這里ViewGroupB并沒有攔截View 但是View完全接受不到事件呢腊徙?
我們來看ViewGroup的dispatchTouchEvent源碼
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
...
if (isTransformedTouchPointInView(x,y,point)) {
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
從源碼中可以看到简十,執(zhí)行到if (isTransformedTouchPointInView) 這行代碼時,就是去判斷當前點擊的坐標是否屬于View的區(qū)域內(nèi)撬腾,假如是勺远,就開始執(zhí)行View的dispatchTouchEvent方法。很顯然在這里點擊的ViewGroupB區(qū)域时鸵,并不在View的范圍內(nèi),所以事件也不會分發(fā)到View厅瞎。
2.4 點擊View區(qū)域饰潜,View消耗事件,但設(shè)置了View.onTouchListener
設(shè)置View.onTouchListener中的onTouch()方法 return true和簸。
當事件分發(fā)到View時彭雾,我們先來看View的dispatchToucnEvent源碼:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
看到?jīng)]验烧,當mOnTouchListener.onTouch(this, event)這個條件為true的時候寨辩,View的dispatchTouchEvent方法將直接return true划址。后續(xù)也不會執(zhí)行View的onTouchEvent方法了嗅蔬。
結(jié)論:View的mOnTouchListener.onTouch方法優(yōu)先于View的onTouchEvent方法被執(zhí)行兴垦。
附上log: