View事件傳遞機制
所謂點選事件的事件分發,其實就是對MotionEvent事件的分發過程。此過程由三個很重要的方法來共同完成: dispatchTouchEvent , onInterceptTouchEvent , onTouchEvent 。
同一個事件序列:指的是從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件以down事件開始,中間含有數量不定的move事件,最終以up事件結束。
View事件傳遞
當一個點選事件產生後,傳遞過程遵循如下順序: Activity —— Window —— View/ViewGroup ,頂級view收到事件後就會按照事件分發機制分發。
dispatchTouchEvent()
事件的分發受本層的onInterceptTouchEvent方法、onTouchEvent返回值和下級view的dispatchTouchEvent結果影響。
-
返回true,表示該事件可能被本層或被下層處理了,事件不再分發。
有以下幾種情況:
- onInterceptTouchEvent攔截了事件(返回true),且本層onTouchEvent處理了事件(返回true);
- 下層元素的dispatchTouchEvent返回值為true,事件被下層處理掉了。
-
返回false,表示事件由下層上拋或本層沒處理,繼續上拋。
有以下幾種情況:
- onInterceptTouchEvent攔截了事件,但本層的onTouchEvent沒處理該事件(返回false),該事件上拋給父容器onTouchEvent處理,若父容器的onTouchEvent返回false,其dispatchTouchEvent也返回false,重複事件上拋,直到最後activity的onTouchEvent處理掉。
- onInterceptTouchEvent沒攔截該事件(返回false),但下層的dispatchTouchEvent因該層的onTouchEvent返回false,表示事件上拋到本層了。
onInterceptTouchEvent()
事件的攔截(false和預設實現父類方法都是不做攔截處理,直接傳遞到子view的dispatchTouchEvent)
- 返回true,將事件進行攔截,並將攔截到的事件交由本層控制元件的 onTouchEvent 進行處理;
- 返回false,不對事件進行攔截,該事件被分發到子View,並由子View的dispatchTouchEvent分發。
- 預設返回super.onInterceptTouchEvent(ev),事件預設不會被攔截,交由子View的dispatchTouchEvent進行處理。
如果當前view攔截了該事件,則在同事件序列中後續事件都會直接交給該view處理,不再呼叫onInterceptTouchEvent方法了。如果想提前處理所有點選事件,可呼叫dispatchTouchEvent()方法。
onTouchEvent()
事件的響應(只有false表示不執行此事件)
- 返回true,執行了此次事件,此時事件終結,將不會繼續後續的冒泡。
- 返回false,該view的父容器onTouchEvent將會被呼叫,若所有層級onTouchEvent都不處理,最後由activity的onTouchEvent處理。
- 預設返回super.onTouchEvent(ev),處理的邏輯和返回true時相同。
補充知識點:
- view對事件的處理按onTouchListener->onTouchEvent->onClickListener優先順序執行。
- TextView預設clickable為false,longClickable為false,則onTouchEvent不消耗事件,但setOnclickListener會改變clickable值為true,消耗事件。
下圖恰當的表明了事件傳遞過程中三者的關係(非原始碼):

總結
- 如果當前view攔截了該事件,則在同事件序列當中後續事件都會直接交給該view處理 (除非通過onTouchEvent上拋事件),並且同事件序列中的後續事件不再觸發onInterceptTouchEvent方法。
- 如果當前view開始處理該事件,但該view的onTouchEvent返回false,則該事件將上拋給父容器的onTouchEvent處理,同事件序列中的後續事件都由父容器處理。
- View 和activity沒有onInterceptTouchEvent方法,但有dispatchTouchEvent和onTouchEvent。
- 呼叫父容器的requestDisallowInterceptTouchEvent(boolean)方法,可干預父容器對事件的攔截,但無法攔截ACTION_DOWN事件。
View的滑動衝突
常用兩種解決滑動衝突的思路:外部攔截法和內部攔截法。
- 外部攔截法: 父容器呼叫onInterceptTouchEvent 方法,且ACTION_DOWN事件一定不攔截,根據情況控制其他事件的返回值,若返回 false則表示不攔截,事件交給子元素處理。 【推薦】
- 內部攔截法: 子元素的dispatchTouchEvent 方法中根據情況呼叫parent.requestDisallowInterceptTouchEvent(false)表示子元素不處理該事件。若父容器要處理該事件,則父容器中要 預設攔截除了ACTION_DOWN以外的事件 。
案例1:在子元素listview中遮蔽父容器scrollview的事件攔截。
mLisetview.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { if(motionEvent.getAction() == MotionEvent.ACTION_UP){ scrollView.requestDisallowInterceptTouchEvent(false); }else{ //遮蔽父控制元件的攔截事件 scrollView.requestDisallowInterceptTouchEvent(true); } return false; } });
同向滑動衝突時,點選區域在listview內,則滑動事件交給listview處理,遮蔽父容器scrollview的事件攔截。
案例2:橫向滑動viewgroup中的子元素與viewgroup事件攔截。
- 在螢幕上橫向滑動時,事件由viewgroup攔截並滑動;
- 點選viewgroup內的子元素時,該子元素獲得點選事件;
外部攔截法如下,即在viewgroup中針對不同操作處理事件攔截。輔助程式碼可忽略。
Scroller scroller; int childWidth; VelocityTracker velocityTracker; int lastTouchX; int nearlyChildIndex;//偏移對應的最近item索引 int lastInterceptX, lastInterceptY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean isIntercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: isIntercepted = false; if (!scroller.isFinished()) { scroller.abortAnimation(); isIntercepted = true; } break; case MotionEvent.ACTION_MOVE: int offsetX = x - lastInterceptX; int offsetY = y - lastInterceptY; //橫向滑動大於縱向滑動時 攔截事件 if (Math.abs(offsetX) > Math.abs(offsetY)) { isIntercepted = true; //記錄事件攔截時座標 lastTouchX = x; } else { isIntercepted = false; } break; case MotionEvent.ACTION_UP: isIntercepted = false; break; } lastInterceptX = x; lastInterceptY = y; return isIntercepted; }
viewgroup的ACTION_DOWN預設不攔截事件,不然後續同序列事件都直接由viewgroup處理量。
攔截規則:橫向滑動大於縱向滑動時,由viewgroup攔截事件並滑動操作。