通過簡單的demo來解析事件分發機制,主要通過簡單的三層Activity–>ViewGrop–>View來分析事件分發機制。先通過一張簡單的事件分發流程圖來看看事件分發的基本流程。
Activity與View是沒有onInterceptTouchEvent這個方法的,也就是說這兩者沒法使用該方法攔截事件,我們先按照上面流程圖來建一個demo,看看事件分發是否與我們流程圖所畫的一樣。如下圖:
在沒有攔截任何事件的情況下可以看到列印的日志,從Activity開始一層一層的把事件傳遞給最底層的View,最底層的View的onTouchEvent()沒有消耗事件的情況下,又向上依次按照層級去觸發onTouchEvent()(在沒有消費事件的情況下,也就是傳回true),最終還是傳遞給了最上的Activity的onTouchEvent()來處理事件。然後我們來依次驗證其它流程
驗證dispatchTouchEvent
- ViewGroup 的dispatchTouchEvent() 傳回true,其它方法傳回值不變保持super看看事件流程 可以看到dispatchTouchEvent()傳回true,事件就直接結束了不會往下傳遞。Activity或者View的也是一樣的,可以自行驗證。
- ViewGroup 的dispatchTouchEvent() 傳回false或者View的dispatchTouchEvent()傳回false的日志圖分别如下 可以看到兩者的日志都如流程圖所示,如果傳回的是false則會像上一級的onTouchEvent傳遞。(ps:細心的可能會看到上面的圖的日志列印,在日志最後總會多出幾個不像流程圖裡所畫的日志,比如MainActivity-----------dispatchTouchEvent(),MainActivity-----------onTouchEvent() 可能會問不是到onTouchEvent()就為止了?這裡就需要從源碼解釋了,由于篇幅問題這裡暫時不作詳細說明,可以自己去檢視源碼,事件分發,當你阻止他繼續分發後,自身這一層沒有消費掉這個事件,還往上層傳遞的話,下一次判斷就直接忽略了,是以直接就是最上層的dispatchTouchEvent()後,就直接onTouchEvent,因為ViewGroup或View的dispatchTouchEvent傳回false,沒法傳遞事件。 除非,你擡起手指在點選一次,它才會重新來開始判斷,有興趣的可以從源碼分析,這裡我截圖都是截所有列印的日志)
dispatchTouchEvent()的總結
- 當dispatchTouchEvent()傳回false,不像下分發事件時,無論是ViewGroup還是View都會向它的上一層的onTouchEvent()傳遞(會逆向向上傳遞),不會執行自身的onTouchEvent()。Activity的則會直接事件結束。
- 當都是預設的super. 則事件将會繼續向下分發,直到事件被消費為止。
- 當傳回true時,表示事件直接被消費,這個事件也就停止分發且不會逆向向上傳遞,直接結束了。
驗證onInterceptTouchEvent()
1.ViewGroup的onInterceptTouchEvent()傳回true,其它方法保持初始狀态值
2.ViewGroup的onInterceptTouchEvent()傳回false,或者super.onInterceptTouchEvent(ev),則事件會依次向下傳遞直到被消費為止,就跟初始化的日志圖是一樣的,這裡就不上圖了。
onInterceptTouchEvent()的總結
onInterceptTouchEvent()是ViewGroup特有的方法,View和Activity中沒有這個方法,當傳回false/super時事件将會正常向下分發,分發至下級的dispatchTouchEvent方法;傳回true時,則表示ViewGroup容器攔截後續事件,會執行該ViewGroup的onTouchEvent()方法,如果自身onTouchEvent()沒有消費掉該事件,則會通過onTouchEvent()向上傳遞,直到事件被消費
驗證onTouchEvent()
1.ViewGroup的onTouchEvent()傳回true,自身消耗掉該事件,其它方法不攔截也不消耗事件
2.View的onTouchEvent()傳回true
3.ViewGroup或者View的onTouchEvent()傳回false,其它保持原樣,則日志輸出圖如正常流程
(ps:這紅色框框框起來的部分呢,上面幾個圖也是,忘記框起來了,就如前面的ps所說。最終消費掉該事件的在哪一層,下次判斷就會省去那些沒有意義的判斷,比如第一個框框圖,雖然第一次判斷走到了View的onTouchEvent,但是它沒有消耗掉該事件,而是向上傳遞給了ViewGroup的onTouchEvent()消費掉了,源碼裡面的的判斷下一次就會直接省去View裡面的判斷,直接就是Activity–>Group。也可以這樣說就是一個事件一旦交給一個View處理,那麼它就必須消耗處理掉,否則同一事件序列中剩下的事件就不再交給它來處理了,短時間内。這裡需要自己看源碼分析,是以我這裡的截圖,沒必要糾結紅色框裡面的列印周期)
onTouchEvent()的總結
傳回true則立即消費掉事件,事件将不會向上傳遞,事件到此終止。傳回false/super則不消費掉此次事件,事件将會層層向上傳遞,直到被消費。
總結一下事件分發中這幾個方法的傳回不同值時的表現
- dispatchTouchEvent和onTouchEvent這兩個方法呢無論是Activity,ViewGroup還是View,隻要傳回了true就表示,該事件到此就終止了,不會往下或者上傳遞事件了,什麼都結束了。
- dispatchTouchEvent和onTouchEvent這兩個方法傳回false/super時呢,對于ViewGroup或者View來說,事件都回傳給父控件的onTouchEvent處理。如果是Activity呢就直接結束了,因為它是最上層了到此就終止了。
-
onInterceptTouchEvent方法預設是不會去攔截事件的,因為子View也需要這個事件,是以onInterceptTouchEvent()攔截器傳回 super.onInterceptTouchEvent() 和 false是一樣的,是不會攔截的,事件會繼續往子View的dispatchTouchEvent()傳遞;如果需要攔截呢就要傳回true。
(ps:最好自己根據上面的流程圖寫代碼跟蹤一下日志,就清晰了)
關于onTouch(),onClick()的優先級和影響
給view設定setOnTouchListener和setOnClickListener事件,其它方法預設super,初始化看一下日志輸出
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("TAG","EventView-----------OnTouchListener()");
return false;
}
});
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("TAG","EventView-----------OnClickListener()");
}
});
在預設都不攔截的情況下,onTouch的優先級高于onTouchEvent,onTouchEvent高于onClick.
我們将onTouch傳回值改為true看看
可以看到onTouch傳回true以後View的包含onTouchEvent()事件的後面方法都不會執行了。被onTouch()消耗完了。onClick()是優先級最低的。在源碼裡面我們可以看到,這裡貼上的是View裡面的部分源碼,ViewGroup的也差不多。在dispatchTouchEvent()的方法中
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;//是否消耗事件
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//若設定了OnTouchListener,則先調用onTouch()。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//若onTouch()沒有消耗事件則調用onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
onTouchEvent()中則包含onClick的代碼塊事件中的performClickInternal()就能找到點選事件
public boolean onTouchEvent(MotionEvent event) {
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
可以看到mOnTouchListener.onTouch的優先級是高于onTouchEvent的。
onClick(),setOnClickListener()時,如果該View的onTouchEvent()不是super.onTouchEvent(event)時,無論設定是return false還是true該事件不會響應。如下圖
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TAG","EventView-----------onTouchEvent()");
// return super.onTouchEvent(event);
return false;
}
設定false
設定true
隻有當View的onTouchEvent()傳回super.onTouchEvent(event)時,setOnClickListener()才會生效,預設為消費了該事件,不會往上在傳遞給ViewGroup的onTouchEvent()處理。
ViewGroup中的requestDisallowInterceptTouchEvent() 設定是否允許攔截
在ViewGroup中如果在onInterceptTouchEvent()中攔截了事件,但是子View在某些情況下又需要該事件怎麼辦?在ViewGroup的dispatchTouchEvent()的源碼中我們可以發現disallowIntercept 設定是否攔截,跟随這個我們可以看到requestDisallowInterceptTouchEvent()設定為true的時候,可以不攔截onInterceptTouchEvent()
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
// 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;
}
......
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
我們修改一下EventView和EventGroup中的代碼,使其在滑動的時候攔截事件Group自己消耗
EventView
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
// getParent().requestDisallowInterceptTouchEvent(true);
Log.i("TAG", "You down EventView");
break;
case MotionEvent.ACTION_UP:
Log.i("TAG", "You up EventView");
break;
case MotionEvent.ACTION_MOVE:
Log.i("TAG", "You move EventView");
}
return true;
}
EventGroup
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
case MotionEvent.ACTION_MOVE: //表示父類需要
return true;
case MotionEvent.ACTION_UP:
return true;
default:
break;
}
return false; //如果設定攔截,除了down,其他都是父類處理
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
由于我們EventGroup在滑動的時候,return true自己消耗了事件,則EventView不會響應事件輸出日志
但是我們要在滑動的時候EventView也要響應自己的事件,把上面我們注釋的代碼塊解除看看日志
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
Log.i("TAG", "You down EventView");
break;
case MotionEvent.ACTION_UP:
Log.i("TAG", "You up EventView");
break;
case MotionEvent.ACTION_MOVE:
Log.i("TAG", "You move EventView");
}
return true;
}
很明顯getParent().requestDisallowInterceptTouchEvent(true); 這樣即使ViewGroup在MOVE的時候return true,子View依然可以捕獲到MOVE以及UP事件。也就是說子元素能夠通過調用requestDisallowIntercept(boolean b)來控制父容器能否調用onInterceptTouchEvent(),是否進行事件攔截。