Android 滑動衝突處理筆記

整理自:《Android 藝術探索》

關於事件傳遞機制部分:點擊事件分發機制 關鍵源碼筆記


1、衝突的幾種場景

  1. 外部滑動與內部滑動方向不一致
  2. 外部滑動與內部滑動方向一致
  3. 上述兩種情況的嵌套

2、解決衝突的前提

制定好規則,即什麼情況由外部的父容器攔截處理,什麼時候分發給內部的子控件處理。

3、解決方法

(1)外部攔截法

即事件先經過父容器的攔截處理,如果父容器需要此事件就攔截,否則就分發給子控件。

該方法的實現需要重寫父容器的 onInterceptTouchEvent() 方法,僞代碼如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    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: {
            // 根據實際情況來判斷是否攔截
            if (父容器需要攔截) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        // 
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

需要注意的幾個點:

  • 1、通常情況下,對於 DOWN 事件必須返回 false,因爲如果父容器攔截了 DOWN 事件,則後續的 MOVE、UP 事件就不會分發給子控件處理,而直接交由父容器處理。

  • 2、對於 ACTION_UP 事件,在父容器中(onInterceptTouchEvent() 方法中)必須返回 false,因爲 ACTION_UP 事件本身沒有太多意義。

    注意,這裏返回 false 是爲了在父容器不攔截 MOVE 事件的時候能夠使得 UP 事件正常傳遞到子控件,因爲如果攔截父控件攔截了 MOVE 事件,則會把 mFirstTouchTarget 清空,此時對於 UP 事件本身就無法傳遞到子控件中了。

    但是在父控件沒有攔截 MOVE 事件的時候,如果父容器在 ACTION_UP 事件時返回了 true,則子控件就無法接收到該事件了,此時子控件的 onClick 事件就無法觸發。不過對於父容器來說,即使 ACTION_UP 事件在 onInterceptTouchEvent() 方法中返回了 false,但是此時已經傳遞到父容器了 dispatchTouchEvent() 中(因爲是在 dispatchTouchEvent() 中通過 onInterceptTouchEvent() 來判斷是否攔截事件)。

(2)內部攔截法

父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要該事件就直接消耗掉,否則就交由父容器進行處理。

注意,“父容器不攔截任何事件” 指的是在邏輯上不攔截任何事件,而由子元素的自行判斷。

但是在具體代碼的實現上,則稍有不同:

首先,需要重寫子元素的 dispatchTouchEvent() 方法:

public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
       		// true 表示不允許父控件攔截
       		// 當傳遞到這裏來的時候,表示父控件的 mGroupFlags 已經被重置過了
       		// 因此這裏設置爲不允許攔截時不會受重置的影響
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要此類點擊事件) {
            	// 不過此次的 MOVE 事件會交由當前控件的 super.onTouchEvent(event),
            	// 而不會回傳給父容器的 onTouchEvent() 了
            	// 之後的第一次事件也不會傳遞給父容器的 onTouchEvent(),
            	// 因此第一次的要轉換成 ACTION_CANCEL 事件傳遞給 mFirstTouchTarget 中的子 View,
            	// 並清空 mFirstTouchTarget,第二次及以後的纔會正常的傳遞給父容器的 onTouchEvent()。
            	// 當然上述是在當前代碼邏輯下的場景,否則視具體代碼而定。
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
    }
    mLastX = x;
    mLastY = y;
    return super.onTouchEvent(event);
}

在子元素的 onTouchEvent() 中,需要對 ACTION_DOWN 事件調用父容器的 requestDisallowInterceptTouchEvent(true),將父容器的 mGroupFlags 設置爲不允許攔截狀態,同時該方法裏面也會依次將父容器的父容器的 mGroupFlags 也設置爲不允許攔截狀態。

然後還需要重寫父容器的 onInterceptTouchEvent() 方法,在實際的代碼上對非 ACTION_DOWN 進行攔截。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}

需要進行上述兩步的原因如下:

當父容器的 mGroupFlags 設置爲不允許攔截狀態時,此時對於 ACTION_DOWN 事件是無法干預的,因爲每次 ACTION_DOWN 事件傳遞到父容器時,都會先重置其 mGroupFlags 爲允許攔截狀態(如後面源碼所示)。

而當 mGroupFlags 爲允許攔截狀態時,ACTION_DOWN 事件又會先傳遞到父容器的 onInterceptTouchEvent() 去進行判斷是否攔截,如果攔截了,則會導致子控件無法接收點擊事件。

因此在父容器中必須不攔截 ACTION_DOWN 事件,而對於後續事件,如果子元素設置了:

getParent().requestDisallowInterceptTouchEvent(false);

則表示子元素希望父容器攔截,因此父容器的 onInterceptTouchEvent() 又要對非 ACTION_DOWN 事件默認進行攔截,否則父容器無法正常的對後續事件進行攔截。


補充源碼

// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            // 重置 mGroupFlags 爲可攔截狀態
            resetTouchState();
        }
        // 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;
        }
        ...
	}
	...
}	        
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章