Android程式設計——Touch 事件的分發和消費機制
介紹
Android 中與 Touch 事件相關的方法包括:
dispatchTouchEvent(MotionEvent ev)
onInterceptTouchEvent(MotionEvent ev)
onTouchEvent(MotionEvent ev)
能夠響應這些方法的控制元件包括:ViewGroup 及其子類、Activity。方法與控制元件的對應關係如下表所示:
從這張表中我們可以看到 ViewGroup 及其子類對與 Touch 事件相關的三個方法均能響應,而 Activity 對 onInterceptTouchEvent(MotionEvent ev) 也就是事件攔截不進行響應。
另外需要注意的是 View 對 dispatchTouchEvent(MotionEvent ev) 和 onInterceptTouchEvent(MotionEvent ev) 的響應的前提是可以向該 View 中新增子 View,如果當前的 View 已經是一個最小的單元 View(比如 TextView),那麼就無法向這個最小 View 中新增子 View,也就無法向子 View 進行事件的分發和攔截,所以它沒有 dispatchTouchEvent(MotionEvent ev) 和 onInterceptTouchEvent(MotionEvent ev),只有 onTouchEvent(MotionEvent ev)。
Touch 事件分析
▐ 事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
Touch 事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會以隧道方式(從根元素依次往下傳遞直到最內層子元素或在中間某一元素中由於某一條件停止傳遞)將事件傳遞給最外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法,並由該 View 的 dispatchTouchEvent(MotionEvent ev) 方法對事件進行分發。dispatchTouchEvent 的事件分發邏輯如下:
如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;
如果 return false,事件分發分為兩種情況:
如果當前 View 獲取的事件直接來自 Activity,則會將事件返回給 Activity 的 onTouchEvent 進行消費;
如果當前 View 獲取的事件來自外層父控制元件,則會將事件返回給父 View 的 onTouchEvent 進行消費。
如果返回系統預設的 super.dispatchTouchEvent(ev),事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。
▐ 事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)
在外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系統預設的 super.dispatchTouchEvent(ev) 情況下,事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件攔截邏輯如下:
如果 onInterceptTouchEvent 返回 true,則表示將事件進行攔截,並將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;
如果 onInterceptTouchEvent 返回 false,則表示將事件放行,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發;
如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件預設會被攔截,並將攔截到的事件交由當前 View 的 onTouchEvent 進行處理。
▐ 事件響應:public boolean onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 並且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情況下 onTouchEvent 會被呼叫。onTouchEvent 的事件響應邏輯如下:
如果事件傳遞到當前 View 的 onTouchEvent 方法,而該方法返回了 false,那麼這個事件會從當前 View 向上傳遞,並且都是由上層 View 的 onTouchEvent 來接收,如果傳遞到上面的 onTouchEvent 也返回 false,這個事件就會“消失”,而且接收不到下一次事件。
如果返回了 true 則會接收並消費該事件。
如果返回 super.onTouchEvent(ev) 預設處理事件的邏輯和返回 false 時相同。
到這裡,與 Touch 事件相關的三個方法就分析完畢了。下面的內容會通過各種不同的的測試案例來驗證上文中三個方法對事件的處理邏輯。
Touch 案例介紹
當一個Touch事件(觸控事件為例)到達根節點,即Acitivty的ViewGroup時,它會依次下發,下發的過程是呼叫子View(ViewGroup)的dispatchTouchEvent方法實現的。簡單來說,就是ViewGroup遍歷它包含著的子View,呼叫每個View的dispatchTouchEvent方法,而當子View為ViewGroup時,又會通過呼叫ViwGroup的dispatchTouchEvent方法繼續呼叫其內部的View的dispatchTouchEvent方法。上述例子中的訊息下發順序是這樣的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent方法只負責事件的分發,它擁有boolean型別的返回值,當返回為true時,順序下發會中斷。在上述例子中如果⑤的dispatchTouchEvent返回結果為true,那麼⑥-⑦-③-④將都接收不到本次Touch事件。來個簡單版的程式碼加深理解:
public boolean dispatchTouchEvent(MotionEvent ev){
....//其他處理,在此不管
View[] views=getChildView();
for(int i=0;i<views.length;i++){
//判斷下Touch到螢幕上的點在該子View上面
if(...){
if(views[i].dispatchTouchEvent(ev))
return true; } }
...//其他處理,在此不管 }
public boolean dispatchTouchEvent(MotionEvent ev){
....//其他處理,在此不管 return false; }
在此可以看出,ViewGroup的dispatchTouchEvent是真正在執行“分發”工作,而View的dispatchTouchEvent方法,並不執行分發工作,或者說它分發的物件就是自己,決定是否把touch事件交給自己處理,而處理的方法,便是onTouchEvent事件
ViewGroup還有個onInterceptTouchEvent,看名字便知道這是個攔截事件。這個攔截事件需要分兩種情況來說明:
1.假如我們在某個ViewGroup的onInterceptTouchEvent中,將Action為Down的Touch事件返回true,那便表示將該ViewGroup的所有下發操作攔截掉,這種情況下,mTarget會一直為null,因為mTarget是在Down事件中賦值的。由於mTarge為null,該ViewGroup的onTouchEvent事件被執行。這種情況下可以把這個ViewGroup直接當成View來對待。
2.假如我們在某個ViewGroup的onInterceptTouchEvent中,將Acion為Down的Touch事件都返回false,其他的都返回True,這種情況下,Down事件能正常分發,若子View都返回false,那mTarget還是為空,無影響。若某個子View返回了true,mTarget被賦值了,在Action_Move和Aciton_UP分發到該ViewGroup時,便會給mTarget分發一個Action_Delete的MotionEvent,同時清空mTarget的值,使得接下去的Action_Move(如果上一個操作不是UP)將由ViewGroup的onTouchEvent處理。
總結
1.Touch事件分發中只有兩個主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三個相關事件。View包含dispatchTouchEvent、onTouchEvent兩個相關事件。其中ViewGroup又繼承於View。
2.ViewGroup和View組成了一個樹狀結構,根節點為Activity內部包含的一個ViwGroup。
3.觸控事件由Action_Down、Action_Move、Aciton_UP組成,其中一次完整的觸控事件中,Down和Up都只有一個,Move有若干個,可以為0個。
4.當Acitivty接收到Touch事件時,將遍歷子View進行Down事件的分發。ViewGroup的遍歷可以看成是遞迴的。分發的目的是為了找到真正要處理本次完整觸控事件的View,這個View會在onTouchuEvent結果返回true。
5.當某個子View返回true時,會中止Down事件的分發,同時在ViewGroup中記錄該子View。接下去的Move和Up事件將由該子View直接進行處理。由於子View是儲存在ViewGroup中的,多層ViewGroup的節點結構時,上級ViewGroup儲存的會是真實處理事件的View所在的ViewGroup物件:如ViewGroup0-ViewGroup1-TextView的結構中,TextView返回了true,它將被儲存在ViewGroup1中,而ViewGroup1也會返回true,被儲存在ViewGroup0中。當Move和UP事件來時,會先從ViewGroup0傳遞至ViewGroup1,再由ViewGroup1傳遞至TextView。
6.當ViewGroup中所有子View都不捕獲Down事件時,將觸發ViewGroup自身的onTouch事件。觸發的方式是呼叫super.dispatchTouchEvent函式,即父類View的dispatchTouchEvent方法。在所有子View都不處理的情況下,觸發Acitivity的onTouchEvent方法。
7.onInterceptTouchEvent有兩個作用:1.攔截Down事件的分發。2.中止Up和Move事件向目標View傳遞,使得目標View所在的ViewGroup捕獲Up和Move事件。
實用
一般在新聞的頁面中ViewPager中巢狀一個輪播圖,請求父控制元件不要攔截
事件分發
當滑動到小viewpager的最後一個介面的時候,外面的viewpager要將小的viewpager的觸控事件攔截,當滑動小的viewpager的不是最後一個介面的時候,外面的viewpager不攔截小viewpager的觸控事件
讓小viewpager進行滑動操作
建立自定義Viewpager進行操作
//事件分發的
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//請求父控制元件不要攔截事件,true:不攔截,false:攔截
//getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
//1.需要判斷是左右滑動還是上下滑動,因為只有左右才是viewpager手動滑動的操作
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//getParent().requestDisallowInterceptTouchEvent(false);
//獲取按下的x和y的座標
downX = (int) ev.getX();
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//獲取移動的x和y的座標
int moveX = (int) ev.getX();
int moveY = (int) ev.getY();
//判斷是上下還是左右滑動
if (Math.abs(moveX-downX) > Math.abs(moveY-downY)) {
//左右
//從右往左,如果是最後一個條目,父控制元件攔截事件,實現切換介面的操作,如果不是最後一個條目,切換下一張圖片
//getAdapter() : 獲取ViewPager設定的adapter
if (downX - moveX > 0 && getCurrentItem() == getAdapter().getCount()-1) {
getParent().requestDisallowInterceptTouchEvent(false);
}else if(downX - moveX > 0 && getCurrentItem() < getAdapter().getCount()-1){
getParent().requestDisallowInterceptTouchEvent(true);
}
//從左往右,如果是第一個條目,父控制元件攔截事件,開啟側拉選單,如果不是第一個條目,切換到上一張圖片
else if(downX - moveX < 0 && getCurrentItem() == 0){
getParent().requestDisallowInterceptTouchEvent(false);
}else if(downX - moveX < 0 && getCurrentItem() > 0){
getParent().requestDisallowInterceptTouchEvent(true);
}
}else{
//上下
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
佈局檔案中使用
<com.itheima.zhbj97.ui.RoolViewPager
android:id="@+id/menunewscenteritem_vp_viewpager"
android:layout_width="match_parent"
android:layout_height="185dp"
></com.itheima.zhbj97.ui.RoolViewPager>
ViewPager和View的事件響應規則
如果是緩慢的移動很短的距離,viewpager和view的事件都會執行
如果是快速滑動很長的距離,view的事件會執行cancel事件,結束view的觸控操作,只去viewpager的事件
具體操作
//設定view的觸控事件事件,實現按下viewpager停止自動滑動,擡起,viewpager重新進行自動滑動操作
rootView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下viewpager停止滑動
handler.removeCallbacksAndMessages(null);//取消handler傳送延遲訊息,如果是null,全部handler都會被取消傳送訊息
break;
case MotionEvent.ACTION_UP:
//擡起viewpager重新滑動
handler.sendEmptyMessageDelayed(0, 3000);
break;
case MotionEvent.ACTION_CANCEL:
//view的事件取消執行的操作
handler.sendEmptyMessageDelayed(0, 3000);
break;
}
//如果想要事件執行,返回true,返回事件不執行
return true;
}
});