view事件分發原始碼理解
有些困難無法逃避,沒辦法,那就只有去解決它。view事件分發對我而言是一塊很難啃的骨頭,看了《安卓開發藝術探索》關於這個知識點的講解,看了好幾遍,始終不懂,最終通過除錯分析結果,看部落格,再回過頭看,總算能瞭解個大概。真的只能說大概,因為我在理解的過程中,還是會刻意忽略掉不少我不懂的又會誘導我深入分析的知識點,這些知識點就像歧路亡羊中的歧路,當我在不斷分叉的歧路中走的越遠,我離要找的羊也就越遠,羊就是對整個分發體系的整體把控。就說到這了,開始探索之旅吧!
1.事件從Activity如何分發至主佈局xml檔案生成的view樹。
//所有觸控事件先從activity中的這個方法開始。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//這是個空方法,不用管
onUserInteraction();
}
//第一步:
//所以事件的分發都先從底下的這個if判斷中展開,這個方法必走,注意先會讓Activity附屬的window進行分發,
// 如果返回true,那麼事件迴圈結束
//如果返回false,那麼就呼叫底下的onTouchEvent.這裡重點要看wondow是怎麼把事件分發下去的。
//window作為抽象類,從它的註釋介紹中得知它的唯一實現類是PhoneWindow,接著看這個類的superDispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
以下是PhoneWindow中的superDispatchTouchEvent()方法
//第二步:
//phonewindow將事件傳遞給了mDecor,他是類的成員變數。位於144行,接著看DecorView
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
以下是DecorView中的superDispatchTouchEvent()方法
//第三步:DecorView繼承FrameLayout,FrameLayout又是繼承於ViewGroup,
// 這個FrameLayout就是我們日常寫的activity的父佈局,在ViewGroup中重寫了
//dispatchTouchEvent,快去看看
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView是一個FrameLayout佈局,它由上下兩部分組成,上面是actionBar,下面是我們最最親愛的在setContentView()方法中傳進xml佈局檔案,生成的檢視組。上面的事件已經分發至DecorView了,現在我們將樣式設為為noActionBar,那麼DecorView佈局中就只有一個contentView佈局。在上面的方法中,由於FrameLayout沒有重寫分發方法,所以會接著向上查詢分發方法,最終找到ViewGroup中的dispatchTouchEvent方法,而這個viewGroup中的第一個子view就是contentView生成的檢視組。接下來看看viewGroup中的分發方法。說實話,前面鋪墊了那麼多,完成可以當課外知識瞭解,畢竟我們經常會預設事件分發就是從contentView這個檢視組進行分發的。好,來看重頭戲:(為了減輕閱讀壓力,以下是閹割版)
TouchTarger mFirstTouchTarget = null;
//每一個MotionEvent包括一個dowm,n個move,一個up,這一系列的action,都會觸發這個方法,
//所以這個方法被呼叫的次數就是(1+n+1)
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//第一步:判斷viewGroup自己是否要攔截,如果不攔截一般存在兩種情況:
//1.使用者的第一個動作是按下
//2.mFirstTouchTarget有值,從後面得知,當前的viewGroup中的某個子view處理了這個事件,mFirstTouchTarget才被附上值。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//不重寫的話,onInterceptTouchEvent預設返回false
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//第二步:viewGroup自己不攔截,就把事件下發給它的子view
if (!intercepted){
//遍歷viewGroup底下的所有子view
for (int i = childrenCount - 1; i >= 0; i--) {
//省略程式碼:如果子view不再當前點選區域內,continue;
//能走到這裡的,表示子view都在點選區域內,所以事件能傳遞給它
//這裡也分兩種情況:
//1.child為viewGroup,就在這將整個方法重調一次
//2.child 為view,view重寫了分發方法,程式碼在後面會貼出,先提一下它的特徵:
//view沒有子元素下發,所以它的dispatchTouchEvent就只是處理事件。
if (child.dispatchTouchEvent()) {
//能走到這裡的,表示事件被處理。
mFirstTouchTarget = object;
break;
}
}
}
//第三步:viewGroup自己處理,分兩種情況:
//1.intercepted = true;表示一開始viewGroup就決定自己處理
//2.intercepted = false,結果遍歷完子元素他們都沒處理,導致mFirstTouchTarget = null
if (mFirstTouchTarget == null) {
//由於viewGroup繼承view,所以這裡也是呼叫view的分發方法,來處理事件。
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
上面的精簡程式碼,多看幾遍,相信你能瞭解viewGroup對motionEvent的分發有個清晰的瞭解。已經可以看到不管事件如何傳遞,最終都會呼叫到view.dispatchTouchEvent方法,我們在看看它的邏輯。先來個定心丸,最難的程式碼就是上面那部分,接下來的都是小菜:
//view
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;
//當前view是否設定了OnTouchListener,以及監聽事件中的onTouch()
//是否返回ture
if (li.mOnTouchListener != null
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果上面的if成立,那就沒這個if什麼事了。所以onTouchEvent也就不會呼叫。
//當然,我們也要看看result=false;進入onTouchEvent瞅一瞅:
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
public boolean onTouchEvent(MotionEvent ev){
boolean clickable = 可點選 or 可長按;
if (clickable) {
switch (action){
//從這看出,我們最常用的clickListenr在手指擡起才會觸發
case up:
if (li != null && li.mOnClickListener != null) {
li.mOnClickListener.onClick(this);
}
break;
case down:
//
break;
//其他情況
}
//這個return true很容易被人忽視,也是說只要可點選,最終onTouchEvent都會消耗這個事件
return true;
}
return false;
}
縱觀view的分發方法,說白了就是處理MotionEvent,而且view也沒有onInterceptTouchEvent()方法。至此,原始碼解析完了,我們看用個例項來增加理解。我們自定義一個LinearLayout和Button。
public class MyLayout extends LinearLayout {
private static final String TAG = "DispatchActivity";
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG,"layout "+MotionEvent.actionToString(ev.getAction()));
//這裡的return值會做修改
return true;
}
}
Button:
public class MyButton extends Button {
private static final String TAG = "DispatchActivity";
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "button: "+MotionEvent.actionToString(event.getAction()));
//這裡的return值會做修改
return true;
}
}
xml佈局
<com.lq.testlayoutinflate.dispatchevent.MyLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.lq.testlayoutinflate.dispatchevent.MyButton
android:id="@+id/btn_DispatchActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="click me"/>
</com.lq.testlayoutinflate.dispatchevent.MyLayout>
我們要測的分3種情況:1.MyLayout 的dispatchTouchEvent()返回ture,Button的 dispatchTouchEvent()分別返回true,false,super.dispatchTouchEvent();
前面說過,我們的activity生成的contentView是新增到FrameLayout中的:所以第開始的分發是從FrameLayout的父類viewGroup開始分發的。當我們在按鈕上按鈕,然後進行移動,在擡起手指,這是的log日誌:這裡的3種情況日誌是一樣的
layout ACTION_DOWN
layout ACTION_MOVE
layout ACTION_MOVE
layout ACTION_MOVE
layout ACTION_UP
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//以1起始為第一次進入這個方法:
//1.0:第一個動作按下,這裡符合,進入if
//2.0:mFirstTouchTarget有了值,再次進入if
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//onInterceptTouchEvent預設返回false。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//1.1:走到這,說明FramaLayout沒有攔截:
//2.1:接下來的和上一次情況類似,繼續輸出不同動作的log
if (!intercepted){
//1.2:FrameLayout就只有一個子節點,即contentView,也就是我們xml的根佈局MyLayout
for (int i = childrenCount - 1; i >= 0; i--) {
//1.3: MyLayout輸出layout ACTION_DOWN日誌,然後返回true,為mFirstTouchTarget附上了值。
if (child.dispatchTouchEvent()) {
mFirstTouchTarget = object;
break;
}
}
}
if (mFirstTouchTarget == null) {
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
可以看到,父佈局MyLayout重寫了分發方法,也就是它處理了事件,所以mFirstTouchTarget指向了它。button沒有得到分發事件。
第二種情況:
1.MyLayout 的dispatchTouchEvent()返回false,Button的 dispatchTouchEvent()分別返回true,false,super.dispatchTouchEvent();3種情況下的log日誌始終為:
layout ACTION_DOWN.這時的日誌與上次有所不同,它的move和up事件都沒有傳遞到MyLayout.這是怎麼回事,我們再來分析下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//1.0:第一個動作按下,這裡符合,進入if
//2.0:mFirstTouchTarget==null,actionMasked ==move or up,使得進入else
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//onInterceptTouchEvent預設返回false。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//1.1:走到這,說明FramaLayout沒有攔截:
//2.1 intercepted = true;不進入if
if (!intercepted){
//1.2:FrameLayout就只有一個子節點,即contentView,也就是我們xml的根佈局MyLayout
for (int i = childrenCount - 1; i >= 0; i--) {
//1.3: MyLayout輸出layout ACTION_DOWN日誌,然後返回false,所以mFirstTouchTarget依舊為null。
if (child.dispatchTouchEvent()) {
mFirstTouchTarget = object;
break;
}
}
}
if (mFirstTouchTarget == null) {
//1.4:呼叫:view.dispatchTouchEvent處理事件,由於這個方法我們未重寫,也不會有log輸出
//2.2同上,無log輸出
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
第三種情況:
1.MyLayout 的dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()分別返回true,false,super.dispatchTouchEvent();3種情況下的log日誌分別為:
layout ACTION_DOWN
button: ACTION_DOWN
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_UP
button: ACTION_UP
---------------------------------
layout ACTION_DOWN
button: ACTION_DOWN
-------------------------------
layout ACTION_DOWN
button: ACTION_DOWN
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_MOVE
button: ACTION_MOVE
layout ACTION_UP
button: ACTION_UP
(1)第一種情況分析:dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()返回true
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
boolean intercepted = false;
//1.0:FrameLayout第一個動作按下,這裡符合,進入if
//2.0 MyLayout第一個動作按下,這裡符合,進入if
//3.0 接下來的動作時move,由於mFirstTouchTarget!=null,所以進入if
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//onInterceptTouchEvent預設返回false。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//1.1:走到這,說明FramaLayout沒有攔截:
//2.1:走到這,說明MyLayout沒有攔截:
//3.1:接下來的情況同上。
if (!intercepted){
//1.2:FrameLayout就只有一個子節點,即contentView,也就是我們xml的根佈局MyLayout
//2.2:MyLayout 就只有一個子節點,即MyButton。
for (int i = childrenCount - 1; i >= 0; i--) {
//1.3: MyLayout輸出layout ACTION_DOWN日誌,然後返回super.dispatchTouchEvent,
//現在進入下一個方法塊2.0,(執行完2.4再看這裡,接受到2.4返回的false,由於只有button這一個子節點,迴圈結束,這次執行1.4)。
//2.3: MyButton輸出button ACTION_DOWN日誌,然會返回true。之後,mFirstTouchTarget被附上了值。
if (child.dispatchTouchEvent()) {
mFirstTouchTarget = object;
break;
}
}
}
//1.4:直接返回handled,無log日誌輸出。
//2.4: handled = false,返回至1.3處,
if (mFirstTouchTarget == null) {
handled = super.dispatchTouchEvent(ev);
}
return handled;
}
(2)第二種情況分析:dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()返回false
其實這種情況與第一種情況有兩處註釋修改一下,讀者應該就一目瞭然了。
//2.3: MyButton輸出button ACTION_DOWN日誌,然會返回false。之後,mFirstTouchTarget仍未null。
//3.0 接下來的動作時是move,並且mFirstTouchTarget==null,所以不進入if,intercepted = true。
(3)第三種情況分析:dispatchTouchEvent()返回super.dispatchTouchEvent(),Button的 dispatchTouchEvent()返回super.dispatchTouchEvent()。
從log日誌種可以看到與第一種情況是一樣的。也就是說button分發方法中的 return super.dispatchTouchEvent()預設就是返回ture。為啥這樣,看看程式碼你就會明白。首先我們先回到
註釋2.3處:
//MyButton輸出button ACTION_DOWN日誌,然會返回return super.dispatchTouchEvent()。
這時就會呼叫view.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;
//由於MyButton沒有設定點選事件,所以這裡的if不滿足
if (li.mOnTouchListener != null
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//result=false;如果onTouchEvent也返回ture,那結果不就和return true如出一轍了。
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
讀者可以再回頭看看上面的onTouchEvent()方法,在這個方法中,這要view是可以點選的,那麼這個方法預設就會返回true。在該方法的最後一行註釋中,我特地提到過,就是它太容易被人忽視了。
到這,例子的9種子情況都分析完了,程式碼不多,註釋倒寫了一籮筐,主要要幫助各位理解。畢竟程式設計師最討厭的兩件事就是寫程式碼時加註釋和看沒有註釋的程式碼。還有關於怎麼處理事件分發,我沒有體到,我認為只有你懂得了以上這些基本分發流程,再去看別人怎麼解決滑動衝突的程式碼,腦子就不會那麼迷茫了。