通俗理解Android中View的事件分發機制及滑動衝突處理

LeBron_Six發表於2016-02-25
說起Android滑動衝突,是個很常見的場景,比如SliddingMenu與ListView的巢狀,要解決滑動衝突,不得不提及到View的事件分發機制。

一、Touch事件傳遞規則分析
首先,我們要知道Touch事件是包裝在MotionEvent物件中的,在手指與螢幕接觸過程中產生一系列事件,典型的事件有以下三種:
ACTION_DOWN:手指剛接觸螢幕的瞬間
ACTION_UP:手指剛離開螢幕的瞬間
ACTION_MOVE:手指在螢幕上滑動

那麼,Android中Touch事件是一個怎樣的傳遞過程呢?

   事件分發:public boolean dispatchTouchEvent(MotionEvent ev)

Touch事件發生時Activity的dispatchTouchEvent(MotionEvent ev)方法會將事件傳遞給最外層View的dispatchTouchEvent(MotionEvent ev)方法,該方法對事件進行分發。分發邏輯如下:
    如果return true,事件會由當前View的dispatchTouchEvent方法進行消費,同時事件會停止向下傳遞;
    如果return false,事件分發分為兩種情況:
        如果當前 View 獲取的事件直接來自 Activity,則會將事件返回給Activity的onTouchEvent進行消費;
        如果當前 View 獲取的事件來自外層父控制元件,則會將事件返回給父View的onTouchEvent進行消費。

    如果return super.dispatchTouchEvent(ev),事件會自動的分發給當前View的onInterceptTouchEvent方法。

   事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)

上面已經提到,如果在dispatchTouchEvent返回super.dispatchTouchEvent(ev),那麼事件將會傳遞到onInterceptTouchEvent方法,該方法對事件進行攔截。攔截邏輯如下:
    如果return true,則表示攔截該事件,並將事件傳遞給當前View的onTouchEvent方法;
    如果return false,則表示不攔截該事件,並將該事件交由子View的dispatchTouchEvent方法進行事件分發,重複上述過程;

    如果return super.onInterceptTouchEvent(ev),預設表示攔截該事件,並將事件傳遞給當前View的onTouchEvent方法,和return true一樣。

   事件響應:public boolean onTouchEvent(MotionEvent ev)

上面已經提到,在dispatchTouchEvent(事件分發)返回super.dispatchTouchEvent(ev)並且onInterceptTouchEvent(事件攔截返回true或super.onInterceptTouchEvent(ev)的情況下,那麼事件會傳遞到onTouchEvent方法,該方法對事件進行響應。響應邏輯如下:
    如果return true,則表示響應並消費該事件;
    如果return fasle,則表示不響應事件,那麼該事件將會不斷向上層View的onTouchEvent方法傳遞,直到某個View的onTouchEvent方法返回true,如果到了最頂層View還是返回false,那麼認為該事件不消耗,則在同一個事件系列中,當前View無法再次接收到事件,該事件會交由Activity的onTouchEvent進行處理;
    如果return super.dispatchTouchEvent(ev),則表示不響應事件,結果與return false一樣。
這裡也順便說一下,如果一個View同時監聽了onTouch事件和onClick事件,則在onTouchEvent裡面應該返回false,否則點選事件就無法監聽到。後面會提到這一點。

上述三個方法到底有什麼區別與聯絡呢?我們通過一段虛擬碼來表示:

public boolean dispatchTouchEvent(MotionEvent ev){
	boolean consume = false;
	if(onInterceptTouchEvent(ev)){                  // 如果onInterceptTouchEvent返回true
		consume = onTouchEvent(ev);             // 則交由該View的onTouchEvent方法
	} else {
		consume = child. dispatchTouchEvent(ev); // 否則交由子View的dispatchTouchEvent事件進行分發
	}
	return consume; // 如果成功消費該事件,則返回true,然後停止傳遞,否則返回false
}
那麼,接下來就總結一下事件的傳遞的規則。
    (1)當一個點選事件產生後,它的傳遞過程遵循的規則如下:Activity->Window->View。頂級View接收到事件之後,就會按相應規則去分發事件。如果一個View的onTouchEvent方法返回false,那麼將會交給父容器的onTouchEvent方法進行處理,逐級往上,如果所有的View都不處理該事件,則交由Activity的onTouchEvent進行處理,就跟工作中遇到了難題,逐級找領導解決一個道理,領導解決不了,再找上一級領導。
    (2)正常情況下,一個事件序列只能被一個View攔截且消耗,某個View一旦進行事件攔截,那麼這一個事件序列都只能交由他處理,並且onInterceptTouchEvent也不會被再次呼叫,因此,正常情況下一個事件是不能交給兩個View來處理的,當然,特殊做法就是在View的onTouchEvent,處理完之後再返回false,強行交給其他View處理。
    (3)如果某一個View開始處理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),則同一事件序列比如接下來進行ACTION_MOVE,則不會再交給該View處理,就像工作中做一件事情,你要嘛做完,要嘛你就不要做這件事了。
    (4)在Android中,ViewGroup預設返回false,即不攔截任何事件。
    (5)諸如TextView、ImageView這些不作為容器的View,一旦接受到事件,就呼叫onTouchEvent方法,它們本身沒有onInterceptTouchEvent方法。正常情況下,它們都會消耗事件(返回true),除非它們是不可點選的(clickable和longClickable都為false),那麼就會交由父容器的onTouchEvent處理。
    (6)View的enable屬性不影響onTouchEvent的預設返回值,只要它clickable或者longClickable為true,則onTouchEvent就會返回true。
    (7)點選事件分發過程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法。也就是說,我們平時呼叫的setOnClickListener,優先順序是最低的,所以,onTouchEvent或OnTouchListener的onTouch方法如果返回true,則不響應onClick方法...


二、滑動衝突處理過程分析
滑動衝突的場景常見於滑動巢狀,就是一個頁面中可能有兩個或兩個以上的View同時可以滑動,那麼就可能會導致只有其中的一個View能滑動。一個最簡單的螢幕觸控動作觸發了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE—>...->ACTION_MOVE->ACTION_UP。
滑動衝突場景主要有三種:
(1)一個頁面中同時存在左右滑動和上下滑動。
    讓外部的View攔截滑動事件,判斷滑動的特徵,如果水平滑動距離>豎直滑動距離,則為水平滑動,反之為豎直滑動。假設內部View可以水平滑動,外部View可以豎直滑動,那麼在外部View的onInterceptTouchEvent方法判斷,如果觸控事件為水平滑動,則應該放行,也就是返回false,然後交給內部View來處理,那麼內部子View就可以實現水平滑動。當然,還有一種方法就是外部View不攔截,交給內部View處理,如果內部View有需要就自己消耗掉,否則交給上一層,但是這樣違反了事件分發機制,所以需配合requestDisallowInterceptTouchEvent(MotionEvent ev)進行處理,這裡就不細說了,有興趣的童鞋可以研究一下。
(2)同時存在兩個豎直或水平滑動
    這個主要還得根據具體的需求分析。最簡單的加入是兩個ScrollView巢狀,一般可以判斷ACTION_DOWN在那個View上,就執行那個View的滑動事件。
(3)就是(1)和(2)同時存在的情況
    實際上也得看具體業務需求找到突破點,但是處理方式本質上來說都是差不多的,就是要根據滑動策略,來干擾事件分發機制。

附上一段虛擬碼來理清一下思路:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) { // 外部View攔截事件
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
        }
        mLastXIntercept = x; // 分別記錄上次滑動座標
        mLastYIntercept = y;

        return intercepted; // 看是否需要傳遞給內部View處理
    }


參考:

Android開發藝術探索

相關文章