天天看點

Android Touch 事件的分發和消費機制

之前就處理過一個ViewPager和HorizontalListView上下滑動事件的沖突,當時也就随便照網上找到的方法改了改,自己對事件分發和消費仍然是一知半解,這下可好,最近又遇見了一個ScrollView和MapView的上下滾動事件沖突的問題,網上找了不少方法,可試了好幾個居然都沒能解決這個問題,無奈隻好研究下原理性的問題,果然了解了原理之後竟是如此之簡單啊。

下面先介紹Android中Touch事件的分發和消費,這部分是轉載自http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html的:

Android 中與 Touch 事件相關的方法包括:dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent ev);能夠響應這些方法的控件包括:ViewGroup、View、Activity。方法與控件的對應關系如下表所示:

Touch 事件相關方法   方法功能     ViewGroup          View             Activity     
  public boolean dispatchTouchEvent(MotionEvent ev) 事件分發   Yes  Yes  Yes
  public boolean onInterceptTouchEvent(MotionEvent ev)  事件攔截   Yes  Yes  No
  public boolean onTouchEvent(MotionEvent ev) 事件響應   Yes  Yes  Yes

從這張表中我們可以看到 ViewGroup 和 View 對與 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)。

 ▐ 事件分發: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,事件分發分為兩種情況:
  1. 如果目前 View 擷取的事件直接來自 Activity,則會将事件傳回給 Activity 的 onTouchEvent 進行消費;
  2. 如果目前 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 事件相關的三個方法就分析完畢了。

好了,了解了上面說的原理後,來說說我的問題和解決方法——在一個豎直方向的ScrollView裡面又嵌入了一個百度的MapView,于是MapView上面想觸摸來上下地圖時會牽引ScrollView上下滑動,而地圖卻不能正确滑動。

我的解決方案是——

  1. 自己繼承一個ScrollView,重寫其中的onInterceptTouchEvent(),讓這個方法恒傳回false,也就是說這個ScrollView對于Touch事件永遠都不攔截,而是分發給他的子View們。
  2. 對于要解決事件沖突的子View(這裡是一個MapView)設定其OnTouchListener(),在onTouch事件中永遠傳回true,也就是永遠消費這個事件不讓它的任何父View有機會處理這個事件。

相關的代碼很少,我貼一下

public class ScrollViewWithMapView extends ScrollView {

    public ScrollViewWithMapView(Context context) {
        super(context);
    }

    public ScrollViewWithMapView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ScrollViewWithMapView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
           return false;
    }

}
           
mapView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return true;
            }
        });
           

如此一來,在MapView上的觸摸滑動時,ScrollView就完全不能參與進來,這時MapView正确響應各種事件,而在ScrollView的其他地方,滾動功能則完全正常,就是這麼簡單!