一 概述
觸摸事件的分發(fā)機制是安卓開發(fā)中的基礎(chǔ)知識,但這塊知識又有點繞,總是讓人覺得似懂非懂。其實安卓事件傳遞就是把用戶觸摸屏幕時的touch事件封裝成MotionEvent對象在Activity、ViewGroup和View中傳遞并處理該touch事件的過程。
二 觸摸事件分發(fā)的方法
現(xiàn)在我們知道觸摸事件是在Activity、ViewGroup和View中進行傳遞的,對應(yīng)的方法如下:
- Activity
Activity不對觸摸事件進行攔截,收到觸摸事件后直接分發(fā)給ViewGroup,如果所有的view最后都沒有處理該觸摸事件,會調(diào)用Activity的onTouchEvent方法進行處理,因此Activity處理觸摸事件的方法為:
dispatchTouchEvent
onTouchEvent - ViewGroup
當(dāng)ViewGroup收到觸摸事件后,它可以分發(fā)給自己的子View但在分發(fā)之前可以判斷是否需要攔截該觸摸事件,也可以調(diào)用自己的onTouchEvent方法處理觸摸事件,因此ViewGroup處理觸摸事件的方法有三個:
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent - View
View和Activity一樣可以接收和處理觸摸事件但不能攔截觸摸事件,畢竟View下面也沒有子View存在了,攔截沒有意義。故View中的方法為:
dispatchTouchEvent
onTouchEvent
三 觸摸事件的傳遞機制
- 在Activity中
當(dāng)用戶點擊屏幕時Activity最先收到觸摸事件此時會調(diào)用Activity的dispatchTouchEvent方法,源碼如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看到Activity調(diào)用getWindow().superDispatchTouchEvent(ev)方法繼續(xù)把觸摸事件傳遞給包含的View,如果有View處理了該事件則返回true,事件傳遞結(jié)束;如果沒有View處理該事件則調(diào)用Activity的onTouchEvent(ev)方法處理該事件,無論在Activity的onTouchEvent(ev)方法中是否消費該事件,該事件的傳遞都結(jié)束了。
- 在ViewGroup中
當(dāng)在Activity中調(diào)用getWindow().superDispatchTouchEvent(ev)方法時,Touch事件會被傳遞給Activity包含的最外層ViewGroup,然后層層向下傳遞。我們分析ViewGroup是如何處理Touch事件的。
當(dāng)Touch事件傳遞到ViewGroup會先調(diào)用ViewGroup的dispatchTouchEvent方法:
/**
* 源碼分析:ViewGroup.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
... // 僅貼出關(guān)鍵代碼
// ViewGroup每次事件分發(fā)時,都需調(diào)用onInterceptTouchEvent()詢問是否攔截事件
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認是false),可通過調(diào)用requestDisallowInterceptTouchEvent()修改
// 判斷值2: !onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不攔截事件),就會讓第二個值為true,從而進入到條件判斷的內(nèi)部
// b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會讓第二個值為false,從而跳出了這個條件判斷
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 通過for循環(huán),遍歷了當(dāng)前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 判斷當(dāng)前遍歷的View是不是正在點擊的View,從而找到當(dāng)前被點擊的View
// 若是,則進入條件判斷內(nèi)部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 條件判斷的內(nèi)部調(diào)用了該View的dispatchTouchEvent()
// 即 實現(xiàn)了點擊事件從ViewGroup到子View的傳遞(具體請看下面的View事件分發(fā)機制)
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
// 調(diào)用子View的dispatchTouchEvent后是有返回值的
// 若該控件可點擊,那么點擊時,dispatchTouchEvent的返回值必定是true,因此會導(dǎo)致條件判斷成立
// 于是給ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即把ViewGroup的點擊事件攔截掉
}
}
}
}
}
}
}
/**
* 作用:是否攔截事件
* 說明:
* a. 返回true = 攔截,即事件停止往下傳遞(需手動設(shè)置,即復(fù)寫onInterceptTouchEvent(),從而讓其返回true)
* b. 返回false = 不攔截(默認)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
從代碼中可以看出在ViewGroup的dispatchTouchEvent方法中先判斷該ViewGroup是否攔截該Touch事件,如果攔截了,Touch事件就不會往下傳遞而是直接調(diào)用ViewGroup的onTouchEvent方法處理事件;如果沒有攔截則遍歷所有子View找到正在點擊的那個View并把Touch事件傳遞給它。
3.在View中
View收到Touch事件后會先調(diào)用View的dispatchTouchEvent方法,在dispatchTouchEvent方法中調(diào)用該View的onTouchEvent方法去處理該Touch事件,如果該View的onTouchEvent方法返回true則表示該View消費了該事件,否則會繼續(xù)調(diào)用其父View的onTouchEvent方法去處理該事件,直到某個View的onTouchEvent方法消費了該事件,或者傳遞到Activity的onTouchEvent方法,則事件傳遞結(jié)束。
在View的onTouchEvent方法中如果接收并消費了ACTION_DOWN事件,則該View會接收到后續(xù)的ACTION_MOVE、ACTION_UP等事件;反之,如果該View沒有消費ACTION_DOWN事件則后續(xù)的事件不會再傳遞給該View。
四 注意事項
- ViewGroup的onInterceptTouchEvent方法默認返回false,ViewGroup進行事件分發(fā)都會調(diào)用該方法,但是一旦onInterceptTouchEvent方法返回true則表示該ViewGroup攔截了觸摸事件,后續(xù)進行事件分發(fā)不再調(diào)用onInterceptTouchEvent方法。
舉個栗子:我們在onInterceptTouchEvent方法中判斷是ACTION_MOVE事件就返回true,在該ViewGroup的子View可以收到ACTION_DOWN事件,如該子View消費了ACTION_DOWN事件,則在第一個ACTION_MOVE事件到來時,ViewGroup會攔截該事件,但是并不會調(diào)用ViewGroup的onTouchEvent方法,同時把ACTION_CANCEL事件傳遞給子View。后續(xù)的事件都不會傳遞給子View了,而是直接調(diào)用ViewGroup的onTouchEvent方法去處理。 - View的onTouch方法會先于onTouchEvent方法執(zhí)行,如下所示(onClick在onTouchEvent方法中執(zhí)行):
button1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("onTouch","touch:button1");
//返回true不執(zhí)行onClick方法
//返回false接著執(zhí)行onClick方法
return false;
}
});
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("button1","點擊了:button1");
}
});
- View的onClick方法是在ACTION_UP事件之后執(zhí)行的,并不是在ACTION_DOWN事件到來時就執(zhí)行。因此如果在父View中攔截了ACTION_MOVE或者ACTION_UP事件,是不會執(zhí)行該方法的。
- 可以調(diào)用
getParent().requestDisallowInterceptTouchEvent(true)方法請求父View不要攔截Touch事件。注意這個方法不能在子View初始化時調(diào)用(無效),最好在子View接收到Touch事件也就是在子View的dispatchTouchEvent方法中調(diào)用。調(diào)用完該方法后,父View以及父View的父View就不會再調(diào)用onInterceptTouchEvent方法去判斷是否攔截了。
好了,觸摸事件的傳遞機制就講到這里啦,有不對的地方歡迎留言指正。