Android View的事件體系(下)

接上一篇:Android藝術(shù)開發(fā)探索第三章————View的事件體系(上)

3.4 View 的事件分發(fā)機(jī)制

本節(jié)介紹 View 的事件分發(fā)機(jī)制。

這章文字超級(jí)多,不過都是精華,盡量不予以刪減書中文字,留著以后忘記了,可以快速復(fù)習(xí)一遍

3.4.1 點(diǎn)擊事件的傳遞規(guī)則

所謂的點(diǎn)擊事件分發(fā),其實(shí)就是對(duì) MotionEvent 事件的分發(fā)過程,即當(dāng)一個(gè) MotionEvent 產(chǎn)生以后,系統(tǒng)需要把這個(gè)事件傳遞給一個(gè)具體的 View ,而這個(gè)傳遞的過程就是分發(fā)過程。

public boolean dispathTouchEvent ( MotionEvent ev)

用來進(jìn)行事件的分發(fā)。如果事件能夠傳遞給當(dāng)前 View ,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前 View 的 onTouchEvent 和下級(jí) View 的 dispathTouchEvent 方法的影響,表示是否消耗當(dāng)前事件。

public boolean onInterceptTouchEvent( MotionEvent ev)

在上述方法內(nèi)部調(diào)用,用來判斷是否攔截某個(gè)事件,如果當(dāng)前 View 攔截了某個(gè)事件,那么在同一個(gè)時(shí)間序列當(dāng)中,此方法不會(huì)被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件。

public boolean onTouchEvent( MotionEvent ev)

在 dispatchTouchEvent 方法中調(diào)用,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗,則在同一個(gè)事件序列中,當(dāng)前 View 無法再次接收到事件。

我們可以大致了解點(diǎn)擊事件的傳遞規(guī)則:對(duì)于一個(gè)根 ViewGroup 來說,點(diǎn)擊事件產(chǎn)生后,首先會(huì)傳遞給它,這時(shí)它的 dispathTouchEvent 就會(huì)被調(diào)用,如果這個(gè) ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示要攔截當(dāng)前事件,接著事件就會(huì)交給這個(gè) ViewGroup 處理,即它的 onTouch 方法就會(huì)被調(diào)用;如果這個(gè) ViewGroup 的 onInterceptTouchEvent 方法返回 false,就表示不攔截當(dāng)前事件,這時(shí)候當(dāng)前事件就會(huì)繼續(xù)傳遞給它的子元素,就這子元素的 dispathTouchEvent 方法就會(huì)被調(diào)用,如此反復(fù)直到事件被最終處理。

當(dāng)一個(gè) View 需要處理事件時(shí),如果它設(shè)置了 OnTouchListener,那么 OnTouchListene r中的onTooch方法會(huì)被回調(diào)。這時(shí)事件如何處理還要看 onTouch 的返回值,如果返回false,那當(dāng)前的 View 的方法 OnTouchListener 會(huì)被調(diào)用;如果返回 true,那么 onTouchEvent 方法將不會(huì)被調(diào)用。由此可見,給View設(shè)置的 OnTouchListener,其優(yōu)先級(jí)比 onTouchEvent 要高,在onTouchEvent 方法中,如果當(dāng)前設(shè)置的有 OnClickListener,那么它的 onClick 方法會(huì)用。可以看出,平時(shí)我們常用的 OnClickListener,其優(yōu)先級(jí)最低,即處于事尾端。

當(dāng)一個(gè)點(diǎn)擊事件產(chǎn)生后,它的傳遞過程遵循如下順序:Activity > Window > View,即事件總是先傳遞給 Activity, Activity 再傳遞給 Window,最后 Window 再傳遞給頂級(jí) View 頂級(jí) View 接收到事件后,就會(huì)按照事件分發(fā)機(jī)制去分發(fā)事件。考慮一種情況,如果一個(gè) View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 將會(huì)被調(diào)用,依此類推,如果所有的元素都不處理這個(gè)事件,那么這個(gè)事件將會(huì)最終傳遞給 Activity 處理,即 Activity 的 onTouchEvent 方法會(huì)被調(diào)用。

image.png

  這里給出一些結(jié)論:
(1)同一個(gè)事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結(jié)束,在這個(gè)過程中所產(chǎn)生的一系列事件,這個(gè)事件序列以 down 事件開始,中間含有數(shù)量不定的 move 事件,最終以 up 事件結(jié)束。

(2)正常情況下,一個(gè)事件序列只能被一個(gè) View 攔截且消耗。但是通過特殊手段可以做到,比如一個(gè) View 將本該自己處理的事件通過 onTouchEvent 強(qiáng)行傳遞給其他 View 處理。

(3)某個(gè) View 一旦決定攔截,那么這一個(gè)事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),并且它的 onInterceptTouchEvent 不會(huì)再被調(diào)用。

(4)某個(gè) View 一旦開始處理事件,如果它不消耗 ACTION_DOWN 事件( onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不會(huì)再交給它來處理,并且事件將重新交由它的父元素去處理,即父元素的 onTouchEvent 會(huì)被調(diào)用。

(5)如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,此時(shí)父元素的 onTouchEvent 并不會(huì)被調(diào)用,并且當(dāng)前 View 可以持續(xù)收到后續(xù)的事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給 Activity 處理。

(6) ViewGroup 默認(rèn)不攔截任何事件。 Android 源碼中 ViewGroup 的 onInterceptTouchEvent 方法默認(rèn)返回 false。

(7)View 沒有 onInterceptTouchEvent 方法,一旦有點(diǎn)擊事件傳遞給它,那么它的 onTouchEvent 方法就會(huì)被調(diào)用。

(8)View 的 onInterceptTouchEvent 默認(rèn)都會(huì)消耗事件(返回 true),除非它是不可點(diǎn)擊的(clickable 和 longClickable 同時(shí)為 false)。View 的 longClickable 屬性默認(rèn)都為 false ,clickable 屬性要分情況,比如 Button 的 clickable 屬性默認(rèn)為 true ,而 TextView 的 cilckable 為 false。

(9)View 的enable 屬性不影響 onTouchEvent 的默認(rèn)返回值。哪怕一個(gè) View 是 disable 狀態(tài)的,只要它的 clickable 或者 longClickable 有一個(gè)為 true,那么它的 onTouchEvent 就返回 true。

(10)onClick 會(huì)發(fā)生的前提是當(dāng)前 View 是可點(diǎn)擊的,并且它收到了 down 和 up 的事件。

(11)事件傳遞過程是由外向內(nèi)的,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子 View,通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是 ACTION_DOWN 事件除外。

3.4.2 事件分發(fā)的源碼解析

開始分析源碼,沒有源碼一切都是軟并卵。

  • Activity 對(duì)點(diǎn)擊事件的分發(fā)過程
      點(diǎn)擊事件用 MontionEvent 來表示,當(dāng)一個(gè)點(diǎn)擊操作發(fā)生時(shí),最先傳遞給當(dāng)前的 Activity,由 Activity 的 dispatchTouchEvent 來進(jìn)行事件派發(fā),具體的工作是由 Activity 內(nèi)部的 Window 來完成的。Window 會(huì)將事件傳遞給 decor view,decor view 一般就是當(dāng)前界面的底層容器(即 setContentView 所設(shè)置的 View 的父容器),通過 Activity . getWindow . getDecorView() 可以獲得。先分析 Activity 的 dispatchTouchEvent 。
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

事件開始交給 Activity 所屬的 Window 進(jìn)行分發(fā),如果返回 true,整個(gè)事件循環(huán)就結(jié)束了,返回 false 意味著事件沒人處理,所有的 View 的onTouch 都返回了 false,那么 Activity 的onTouchEvent 就會(huì)被調(diào)用。

接下來看 Window 是如何傳遞給 ViewGroup 的。查看源碼我們知道, Window是一個(gè)抽象類,而 Window 的 superDisapatchTouchEvent 方法也是個(gè)抽象方法,因此我們必須找到 Window 的實(shí)現(xiàn)類才行。

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.policy.PhoneWindow, which you should instantiate when needing a
 * Window.  Eventually that class will be refactored and a factory method
 * added for creating Window instances without knowing about a particular
 * implementation.
 */
public abstract class Window {
...
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
...
}

上面這段話的大概意思是:Window 類可以控制頂級(jí) View 的外觀和行為策略,它的唯一實(shí)現(xiàn)位于 android.policy.PhoneWindow 中,當(dāng)你要實(shí)例化這個(gè) Window 類的時(shí)候,你并不知道它的細(xì)節(jié),因?yàn)檫@個(gè)類會(huì)被重構(gòu),只有一個(gè)工廠方法可以使用。盡管這看起來有點(diǎn)模糊,不過我們可以看一下 android.policy.PhoneWindow這個(gè)類,盡管實(shí)例化的時(shí)候會(huì)被重構(gòu),僅是重構(gòu)而已,功能是類似的。

由于 Window 的唯一實(shí)現(xiàn)是 PhoneWindow,因此直接看 PhoneWindow 是如何處理點(diǎn)擊事件的。

public class PhoneWindow extends Window implements MenuBuilder.Callback {
...
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
...
}

到這里邏輯就很清晰了,PhoneWindow 繼承 Window 重寫 superDispatchTouchEvent 將事件直接傳遞給了 mDecor ,這個(gè) mDecor 是什么?請(qǐng)看下面:

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    private DecorView mDecor;

    @Override
    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
    }

我們知道,通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);這中方式就可以獲取 Activity 所設(shè)置的 View,這個(gè) mDecor 顯然就是 getWindow().getDecorView() 返回的 View,而我們通過 setContentView 設(shè)置的 View 是它的一個(gè)子 View。目前事件傳遞到了 DecorView 這里,由于 DecorView 繼承自 FrameLayout 且是父 View ,所以最終事件會(huì)傳遞給 View。從這里開始,時(shí)間已經(jīng)傳遞到了頂級(jí) View 了,即在 Activity 中 setContentView 設(shè)置的 View,另外頂級(jí) View 也叫根 View,頂級(jí) View 一般來說都是 ViewGroup。

這波源碼看懂了,其實(shí)就是一個(gè)繼承重寫然后轉(zhuǎn)移事件的一個(gè)套路。

  • 頂級(jí) View 對(duì)點(diǎn)擊事件的分發(fā)過程
// 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;
            }

從上面代碼我們可以看出,ViewGroup 在如下兩種情況下會(huì)判斷是否要攔截當(dāng)前事件:事件類型為 ACTION_DOWN 或者 mFirstTouchTarget != null。ACTION_DOWN 事件很好理解,mFirstTouchTarget 這個(gè)在后面的代碼邏輯中可以看出來,當(dāng)事件由 ViewGroup 的子元素成功處理時(shí), mFirstTouchTarget 會(huì)被賦值并指向子元素。那么當(dāng) ACTION_DOWN 和 ACTION_UP 事件到來時(shí),由于 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 這個(gè)條件為 false,將導(dǎo)致 ViewGroup 的 onInterceptTouchEvent 不會(huì)再被調(diào)用,并且同一序列中的其他事件都會(huì)默認(rèn)交給它處理。
  當(dāng)然,這里有一種特殊情況,那就是 FLAG_DISALLOW_INTERCEPT 標(biāo)記位,這個(gè)標(biāo)記位是通過 requestDisallowInterceptTouchEvent 方法來設(shè)置,一般用于子 View 中。FLAG_DISALLOW_INTERCEPT 一旦設(shè)置后,ViewGroup 將無法攔截除了 ACTION_DOWN 以外的其他點(diǎn)擊事件。為什么說是除了 ACTION_DOWN 以外的其他事件呢 ?這是因?yàn)?ViewGroup 在分發(fā)事件時(shí),如果是 ACTION_DOWN 就會(huì)重置 FLAG_DISALLOW_INTERCEPT 這個(gè)標(biāo)記位,因此當(dāng)面對(duì) ACTION_DOWN 事件時(shí),ViewGroup 總是會(huì)調(diào)用自己的 onInterceptTouchEvent 方法來詢問自己是否要攔截事件,這一點(diǎn)從源碼中也可以看出來。

// Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

當(dāng) ViewGroup 不攔截事件的時(shí)候,事件會(huì)向下分發(fā)交由它的子 View 進(jìn)行處理,這段源碼如下所示:

final View[] children = mChildren;

    final boolean customOrder = isChildrenDrawingOrderEnabled();
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = customOrder ?
                getChildDrawingOrder(childrenCount, i) : i;
        final View child = children[childIndex];
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            mLastTouchDownIndex = childIndex;
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
    }

上面這段代碼邏輯也很清晰,首先遍歷 ViewGroup 的所有子元素,然后判斷子元素是否能夠接收到點(diǎn)擊事件。是否能夠接收點(diǎn)擊事件主要由兩點(diǎn)來衡量:子元素是否在播動(dòng)畫和點(diǎn)擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)。如果某個(gè)子元素滿足這兩個(gè)條件,那么事件就會(huì)傳遞給它來處理。可以看到,dispatchTransformedTouchEvent 實(shí)際上調(diào)用的就是子元素的 dispatchTouchEvent 方法,在它的內(nèi)部有如下一段內(nèi)容,而在上面的代碼中 child 傳遞的不是 null,因此它會(huì)直接調(diào)用子元素的 dispatchTouchEvent 方法,這樣事件就交由子元素處理了,從而完成了一輪事件分發(fā)。

 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...
 }---->>>
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
      

如果子元素的 dispatchEvent 返回 true,這時(shí)我們暫時(shí)不用考慮事件在子元素內(nèi)部是怎么分發(fā)的,那么 mFirstTouchTarget 就會(huì)被賦值同時(shí)跳出 for 循環(huán),如下所示:

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
      ...
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
      ...
    }

這幾行代碼完成了 mFirstTouchTarget 的賦值并終止對(duì)子元素的遍歷。如果子元素的 dispatchTouchEvent 返回 false,ViewGroup 就會(huì)把事件分發(fā)給下一個(gè)子元素。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

其實(shí) mFirstTouchTarget 真正的賦值過程是在 addTouchTarget 內(nèi)部完成的,從下面的 addTouchTarget 方法內(nèi)部結(jié)構(gòu)可以看出,mFirstTouchTarget 是一種單鏈表結(jié)構(gòu),mFirstTouchTarget 是否被賦值,將直接影響到 ViewGroup 對(duì)事件的攔截策略,如果 mFirstTouchTarget 為 null,那么 ViewGroup 就默認(rèn)攔截接下來同一序列中所有的點(diǎn)擊事件。

如果遍歷所有的子元素后事件都沒有被適合地處理,這包含兩種情況:第一種是 ViewGroup 沒有子元素,第二種是子元素處理了點(diǎn)擊事件,但是在 dispatchTouchEvent 中返回了 false。一般是第二種情況。在這兩個(gè)情況下 ViewGroup 會(huì)自己處理點(diǎn)擊事件。

// Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }

注意上面這段代碼,這里第三個(gè)參數(shù) child 為 null,從前面的分析可以知道,它會(huì)調(diào)用 super.dispatchTouchEvent(event),很顯然,這里就轉(zhuǎn)到了 View 的 dispatchTouchEvent 方法,即點(diǎn)擊事件開始交由 View 來處理。

  • View 對(duì)點(diǎn)擊事件的處理過程

View 對(duì)點(diǎn)擊事件的處理過程稍微簡(jiǎn)單一些,注意這里的 View 不包含 ViewGroup。先看它的 dispatchTouchEvent 方法,如下所示:

 public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
        ...
        if (onFilterTouchEventForSecurity(event)) {
         ...
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
      ...
        return result;
    }

View 對(duì)點(diǎn)擊事件的處理過程就比較簡(jiǎn)單,因?yàn)?View 是一個(gè)單獨(dú)的元素,它不需要向下傳遞事件,只能自己處理。從上面的源碼可以看出 View 對(duì)點(diǎn)擊事件的處理過程,首先會(huì)判斷有沒有設(shè)置 OnTouchListener,如果 OnTouchListener 中的onTouch 方法返回 true,那么OnTouchListener 就不會(huì)被調(diào)用,可見 OnTouchListener 的優(yōu)先級(jí)高于 onTouch,這樣做的好處是方便在外界處理點(diǎn)擊事件。
  
  接著再分析 OnTouchEvent 的實(shí)現(xiàn)。先看當(dāng) View 處于不可用狀態(tài)下點(diǎn)擊時(shí)間的處理過程,如下所示。很顯然,不可用狀態(tài)下的 View 照樣會(huì)消耗點(diǎn)擊時(shí)間,盡管它看起來不可用。

public boolean onTouchEvent(MotionEvent event) {
      ...
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            //禁用視圖,點(diǎn)擊消費(fèi)仍然觸摸事件,它只是不回應(yīng)他們
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        ...
}

接著,如果 View 設(shè)置有代理,那么還會(huì)執(zhí)行 TouchDelegate 的 onTouchEvent 方法,這個(gè) onTouchEvent 的工作機(jī)制看起來和 OnTouchListener 類似,這里不深入研究了。

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

下面再看一下 onTouchEvent 中對(duì)點(diǎn)擊事件的具體處理,如下所示。

 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
 (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // 如果我們已經(jīng)沒有焦點(diǎn),我們應(yīng)該在觸摸模式。
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // 按鈕被釋放之前,我們實(shí)際上顯示為按下。使它顯示按下狀態(tài)現(xiàn)在(調(diào)度點(diǎn)擊),以確保用戶看到它。
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                        ...
                    break;
                ...
            return true;

從上面代碼來看,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一個(gè)為 true,那么它就會(huì)消耗這個(gè)事件,即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 狀態(tài)。然后就是當(dāng) ACTION_UP 事件發(fā)生時(shí),會(huì)觸發(fā) performClick 方法,如果 View 設(shè)置了 OnClickListener,那么 performClick 方法內(nèi)部會(huì)調(diào)用它的 onClick 方法,如下所示:

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

View 的 LONG_CLICKABLE 屬性默認(rèn)為 false,而 CLICKABLE 屬性是否為 false 和具體的 View 有關(guān),確切來說可點(diǎn)擊的 View 其 CLICKABLE 為 true,不可點(diǎn)擊的 View 其 CLICKABLE 為 false,比如 Button 是可點(diǎn)擊的,TextView 是不可點(diǎn)擊的。通過 setClickable 和 setLongClickable 可以分別改變 View 的CLICKABLE 設(shè)為 true,setOnLongClickListener 則會(huì)自動(dòng)將 View 的 LONG_CLICKABLE 設(shè)為 true,這一點(diǎn)從源碼中可以看出來,如下所示:

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

到這里,點(diǎn)擊事件的分發(fā)機(jī)制的源碼實(shí)現(xiàn)已經(jīng)分析完了,結(jié)合 3.4.1 節(jié)中的理論分析和相關(guān)結(jié)論,可以更好的理解事件分發(fā)。

3.5 View 的滑動(dòng)沖突

本節(jié)開始介紹:滑動(dòng)沖突。前面 4 節(jié)均是為本節(jié)服務(wù)的,通過本節(jié)的學(xué)習(xí),滑動(dòng)沖突將不再是個(gè)問題。

3.5.1 常見的滑動(dòng)沖突場(chǎng)景

常見的滑動(dòng)沖突場(chǎng)景可以簡(jiǎn)單分為三種:

  • 場(chǎng)景 1 —— 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致;
  • 場(chǎng)景 2 —— 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致;
  • 場(chǎng)景 3 —— 上面兩種情況嵌套。
滑動(dòng)沖突的場(chǎng)景

  先說場(chǎng)景 1,主要是將 ViewPager 和 Fragment 配合使用所組成的頁面滑動(dòng)效果。在這種效果中,可以通過左右滑動(dòng)來切換頁面,但是每個(gè)頁面內(nèi)部往往又是一個(gè) ListView。本來這種情況下是有滑動(dòng)沖突的,但是 ViewPager 內(nèi)部處理了這種滑動(dòng)沖突,因此采用 ViewPager 時(shí)我們我無須關(guān)注這個(gè)問題,如果我們采用的不是 ViewPager 而是 ScrollView 等,那就必須手動(dòng)處理滑動(dòng)沖突了,否則造成的后果就是內(nèi)外兩層只能有一層滑動(dòng),這是因?yàn)閮烧咧g的滑動(dòng)時(shí)間有沖突。
  再說場(chǎng)景 2,這種情況稍微復(fù)雜一些,當(dāng)內(nèi)外兩層都在同一個(gè)方向可以滑動(dòng)的時(shí)候,顯然存在邏輯問題。因?yàn)楫?dāng)手指開始滑動(dòng)的時(shí)候,系統(tǒng)無法知道用戶到底是想讓哪一層滑動(dòng),所以當(dāng)手指滑動(dòng)的時(shí)候就會(huì)出現(xiàn)問題,要么只有一層能滑動(dòng),要么就是內(nèi)外兩層都滑動(dòng)的很卡頓。
  最后說場(chǎng)景 3,場(chǎng)景 3 是場(chǎng)景 1、2兩種情況的嵌套。在許多應(yīng)用中會(huì)有這么一個(gè)效果:內(nèi)存有一個(gè)場(chǎng)景 1中的滑動(dòng)效果,然后外層又有一個(gè)場(chǎng)景 2 的滑動(dòng)效果。具體來說就是,外部有一個(gè) SlideMenu 效果,然后內(nèi)部有一個(gè) ViewPager,ViewPager 的每一個(gè)頁面又是一個(gè) ListView。雖然說場(chǎng)景 3的滑動(dòng)沖突看起來更加復(fù)雜,但是它是幾個(gè)單一的滑動(dòng)沖突疊加的,因此只需要分別處理內(nèi)層和中層、中層和外層之間的滑動(dòng)沖突即可,而具體的處理方法其實(shí)和場(chǎng)景 1、2相同的。

3.5.2 滑動(dòng)沖突的處理規(guī)則

一般來說,不管滑動(dòng)沖突多么復(fù)雜,它都有既定的規(guī)則,根據(jù)這些規(guī)則我們就可以選擇合適的方法去處理。

對(duì)于場(chǎng)景1,它的處理規(guī)則是:當(dāng)用戶左右滑動(dòng)時(shí),需要讓外部的 View 攔截點(diǎn)擊事件,當(dāng)用上下滑動(dòng)的時(shí)候,需要讓內(nèi)部 View 攔截點(diǎn)擊事件。具體來說是:根據(jù)水平滑動(dòng)還是豎直滑動(dòng)來判斷到底由誰來攔截事件,如圖 滑動(dòng)過程示意圖 所示,根據(jù)滑動(dòng)過程中兩個(gè)點(diǎn)之間的坐標(biāo)就可以的出來到底是水平滑動(dòng)還是豎直滑動(dòng)。如何根據(jù)坐標(biāo)來得到滑動(dòng)的方向呢?這很簡(jiǎn)單,有很多可以參考,比如可以根據(jù)滑動(dòng)路徑和水平方向所形成的夾角,也可以根據(jù)水平方向和豎直方向上的距離來判斷,某些特殊時(shí)候還可以根據(jù)水平和豎直方向的速度來做判斷。這里我們使用哭了差來做判斷。

滑動(dòng)過程示意圖

  對(duì)于場(chǎng)景 2 來說,比較特殊,它無法根據(jù)滑動(dòng)的角度、距離差以及速度差來做判斷,但是這個(gè)時(shí)候一般都能在業(yè)務(wù)上找到突破口,比如業(yè)務(wù)上有規(guī)定:當(dāng)處于某種狀態(tài)時(shí)需要外部 View 響應(yīng)用戶的滑動(dòng),而處于另外一種狀態(tài)時(shí)則需要內(nèi)部 View 來響應(yīng) View 的滑動(dòng),根據(jù)這種業(yè)務(wù)上的需求我們也能得出響應(yīng)的處理規(guī)則,有了處理規(guī)則同樣可以進(jìn)行下一步處理。這種場(chǎng)景通過通過文字描述可能比較抽象,等下會(huì)通過實(shí)際例子來演示。

對(duì)于場(chǎng)景 3 來說,它的滑動(dòng)規(guī)則就更復(fù)雜了,和場(chǎng)景 2 一樣,它也無法直接根據(jù)滑動(dòng)的角度、距離差以及速度差來做判斷,同樣還是智能從業(yè)務(wù)上找突破口。

3.5.3 滑動(dòng)沖突的解決方式

針對(duì)場(chǎng)景 1 中的滑動(dòng),我們可以根據(jù)滑動(dòng)的距離差來進(jìn)行判斷,這個(gè)距離差就是所謂的滑動(dòng)規(guī)則。如果用 ViewPager 去實(shí)現(xiàn)場(chǎng)景 1 中的效果,我們不需要手動(dòng)處理滑動(dòng)沖突,因?yàn)?ViewPager 已經(jīng)幫我們做了,所以這里不采用 ViewPager。

  • 外部攔截法

所謂的外部攔截法,就是點(diǎn)擊事情都先進(jìn)過父容器的攔截處理,如果父容器需要此事件就攔截,如果不需要就不攔截,這樣就可以解決滑動(dòng)沖突的問題,這種方法比較符合點(diǎn)擊事件的分發(fā)機(jī)制。外部攔截法需要重寫父容器的 onInterceptTouchEvent 方法,在內(nèi)部做相應(yīng)的攔截即可,這種方法的偽代碼如下。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean onIntercept = false;
        switch (ev.getAction()) {
            case ACTION_DOWN:
                onIntercept = false;
                break;
            case ACTION_MOVE:
                if (父容器需要當(dāng)前點(diǎn)擊事件) {
                    onIntercept = true;
                } else {
                    onIntercept = false;
                }
                break;
            case ACTION_UP:
                onIntercept = false;
                break;
            default:
                break;
        }
        return onIntercept;
    }

上述代碼是外部攔截法的典型邏輯,正對(duì)不同的滑動(dòng)沖突,只需要修改父容器需要當(dāng)前點(diǎn)擊事件這個(gè)條件即可,其他均不需要做修改并且也不能修改。這里對(duì)上述代碼再描述一下,在 onInterceptTouchEvent 方法中,首先是 ACTION_DOWN 這個(gè)事件,父容器必須返回 false,即不攔截 ACTION_DOWN 事件,因?yàn)橐坏└溉萜鲾r截了ACTION_DOWN ,那么后續(xù)的 ACTION_MOVE 和 ACTION_UP 都會(huì)交由父容器處理,事件就沒辦法再傳遞給子元素了;其次就是 ACTION_MOVE 事件,這個(gè)事件可以根據(jù)需求來決定是否攔截;最后 ACTION_UP 事件,這里必須要返回 false,因?yàn)?ACTION_UP 事件本身沒有太多意義。

考慮到一種情況,假設(shè)事件交由子元素處理,如果父容器在 ACTION_UP 時(shí)返回了 true,就會(huì)導(dǎo)致子元素?zé)o法接收到 ACTION_UP 事件,這個(gè)時(shí)候子元素的 onClick 事件就無法觸發(fā),但是父容器比較特殊,一旦它開始攔截任何一個(gè)事件,那么后續(xù)的事件都會(huì)交給它來處理,而 ACTION_UP 作為最后一個(gè)事件也必定可以傳遞給父容器,幾遍父容器的 onInterceptTouchEvent
方法在 ACTION_UP 時(shí)返回了 false。

  • 內(nèi)部攔截法

內(nèi)部攔截法指父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進(jìn)行處理,這種方法和 Android 中的事件分發(fā)機(jī)制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起來比較復(fù)雜。它的偽代碼如下,我們需要重寫子元素的 dispatchTouchEvent 方法:

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
            if (父容器需要此類點(diǎn)擊事件) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

上述代碼是內(nèi)部攔截法的典型代碼,當(dāng)面對(duì)不同的花哦東策略時(shí)只需要修改里面的條件即可。除了子元素需要做處理以外,父元素也要默認(rèn)攔截除了 ACTION_DOWN 以外的其他事件,這樣當(dāng)子元素調(diào)用 parent.requestDisallowIntercptTouchEvent(false) 方法時(shí),父元素才能繼續(xù)攔截所需的事件。

注意一點(diǎn):ACTION_DOWN 事件不收 FLAG_DISALLOW_INTERCEPT 這個(gè)標(biāo)記位的控制,所以使用內(nèi)部攔截法,父容器就不能攔截 ACTION_DOWN 事件。

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

下面做一個(gè)實(shí)例來介紹兩種方法。我們來實(shí)現(xiàn)一個(gè)類似于 ViewPager 中嵌套 ListView 的效果。為了實(shí)現(xiàn) ViewPager 的效果,我們定義一個(gè)類似于水平的 LinerLayout 的東西,只不過它可以水平滑動(dòng),初始化時(shí)我們?cè)谒膬?nèi)部添加若干個(gè) ListView,這樣一來,由于它內(nèi)部的 ListView 可以豎直滑動(dòng),而它本身又可以水平滑動(dòng),因此一個(gè)典型的滑動(dòng)沖突場(chǎng)景就出現(xiàn)了,并且這種沖突屬于類型1的沖突。


沖突事件 1 實(shí)例

  首先來看一下 Activity 中的初始化代碼,如下所示。

public class DemoActivity_1 extends AppCompatActivity {

    private static final String TAG = "DemoActivity_1";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo_1);
        initView();
    }
    //初始化View
    private void initView() {
        LayoutInflater inflater = getLayoutInflater();
        HorizontalScrollViewEx listContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
        //獲取屏幕尺寸
        int widthPixels = getScreenMetrics(this).widthPixels;
        int heightPixels = getScreenMetrics(this).heightPixels;
        LogUtil.log(widthPixels + "===" + heightPixels);
        for (int i = 0; i < 3; i++) {
            ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout, listContainer, false);
            layout.getLayoutParams().width = widthPixels;
            TextView textView = (TextView) layout.findViewById(R.id.title1);
            textView.setText("page" + (i + 1));
            layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
            createList(layout);
            listContainer.addView(layout);
        }
    }
    //創(chuàng)建ListView
    private void createList(ViewGroup layout) {
        ListView listView = (ListView) layout.findViewById(R.id.list);
        ArrayList<String> mDatas = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            mDatas.add("name " + i);
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.content_list_item, R.id.name, mDatas);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Toast.makeText(DemoActivity_1.this, "position = " + position, Toast.LENGTH_SHORT).show();
            }
        });
    }
//獲取屏幕尺寸
private DisplayMetrics getScreenMetrics(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        return dm;
    }
}

上述代碼很簡(jiǎn)單,就是創(chuàng)建 3 個(gè)ListView 并且把 ListView 加入到我們自己定義的HorizontalScrollViewEX 中,這里 HorizontalScrollViewEX 是父容器,而 ListView 就是子元素,關(guān)于 HorizontalScrollViewEX 的代碼需要在書中第四章會(huì)有詳細(xì)的介紹,本文就先不做介紹了。

首先采用外部攔截法來解決這個(gè)問題,按照前面的分析,我們只需要修改父容器需要攔截事件的條件即可。對(duì)于本例來說,父容器的攔截條件就是滑動(dòng)過程中水平距離差比豎直距離差大,在這種情況下,父容器就攔截當(dāng)前點(diǎn)擊事件。

public class HorizontalScrollViewEx extends ViewGroup {
...
       @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;
            //abortAnimation這一句話主要是為了優(yōu)化滑動(dòng)體驗(yàn)而加入的,可要可不要
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            //當(dāng)水平方向滑動(dòng)距離大于豎直方向滑動(dòng)距離就返回true,攔截事件,反之則不攔截
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }
...
}

當(dāng)水平方向滑動(dòng)距離大于豎直方向滑動(dòng)距離就返回true,父容器攔截事件,ListView 就無法獲取點(diǎn)擊事件,反之父容器不攔截 ACTION_MOVE 事件,事件就傳遞給了 ListView ,這樣 ListView 就能上下滑動(dòng)了,如此滑動(dòng)沖突就解決了。
  考慮到一種情況,如果用戶正在水平滑動(dòng),但是在水平滑動(dòng)停止之前如果用戶再迅速進(jìn)行上下滑動(dòng),就會(huì)導(dǎo)致夾棉在水平方向無法滑動(dòng)到終點(diǎn),為了避免這中情況,當(dāng)水平方向正在滑動(dòng)時(shí),下一個(gè)序列的點(diǎn)擊時(shí)間任然交給父容器來處理。

public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    // 分別記錄上次滑動(dòng)的坐標(biāo)
    private int mLastX = 0;
    private int mLastY = 0;
    // 分別記錄上次滑動(dòng)的坐標(biāo)(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    ...

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

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
       ...
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            scrollBy(-deltaX, 0);
            break;
        }
        case MotionEvent.ACTION_UP: {
            int scrollX = getScrollX();
            int scrollToChildIndex = scrollX / mChildWidth;
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.getXVelocity();
            if (Math.abs(xVelocity) >= 50) {
                mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
            } else {
                mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
            }
            mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
            int dx = mChildIndex * mChildWidth - scrollX;
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        } else {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;

        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

如果采用內(nèi)部攔截法也是可以的,按照前面對(duì)內(nèi)部攔截法的分析,我們只需要修改 ListView 的 dispatchTouchEvent 方法中的父容器的攔截邏輯,同時(shí)讓父容器攔截 ACTION_MOVE 和 ACTION_UP 事件即可。為了重寫 ListView 的 dispatchTouchEvent 方法,我們必須要自定義一個(gè) ListView。

public class ListViewEx extends ListView {
    private static final String TAG = "ListViewEx";

    private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;

    // 分別記錄上次滑動(dòng)的坐標(biāo)
    private int mLastX = 0;
    private int mLastY = 0;

    ...
    public void setHorizontalScrollViewEx2(
            HorizontalScrollViewEx2 horizontalScrollViewEx2) {
        mHorizontalScrollViewEx2 = horizontalScrollViewEx2;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

}

除了對(duì) ListView 所做的修改,我們還需要修改 HorizontalScrollViewEx。

public class HorizontalScrollViewEx2 extends ViewGroup {
...
     @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            mLastX = x;
            mLastY = y;
          //  if (!mScroller.isFinished()) {
    //            mScroller.abortAnimation();
      //          return true;
            }
            return false;
        } else {
            return true;
        }
...
    }

上面代碼就是內(nèi)部攔截法的實(shí)例,其中 mScroller.abortAnimation(); 這一句不是必須的,是為了優(yōu)化體驗(yàn)增加的,內(nèi)部攔截法的操作要稍微復(fù)雜一些,因此推薦采用外部攔截法來解決常見的滑動(dòng)沖突。

前面說過,只要我們根據(jù)場(chǎng)景 1 的情況來得出通用的解決方案,那么對(duì)于場(chǎng)景2 和場(chǎng)景 3 來說我們只需要修改相關(guān)滑動(dòng)規(guī)則的邏輯即可,下面我們就來演示如何利用場(chǎng)景 1得出的通用解決方案來解決更復(fù)雜的滑動(dòng)沖突,這里只詳細(xì)分析場(chǎng)景 2 中的滑動(dòng)沖突,對(duì)于場(chǎng)景 3 的疊加沖突,都可以拆解為單一的滑動(dòng)沖突圖,解決方案和 場(chǎng)景 1、2 的解決思想一致,場(chǎng)景 3 就不分析了。

下面用過一個(gè)實(shí)際的例子來分析場(chǎng)景 2,首先我們可以提供一個(gè)可以上下滑動(dòng)的父容器SickLayout,它看起來就像一個(gè)可以上下滑動(dòng)的豎直的 LinearLayout ,然后在它的內(nèi)部分別放一個(gè) Header 和 ListView,這樣內(nèi)外兩層都能上下滑動(dòng),于是就形成了場(chǎng)景2中的滑動(dòng)沖突。

public class StickyLayout extends LinearLayout {

    private int mTouchSlop;
    private int mLastX = 0;
    private int mLastY = 0;

    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    public StickyLayout(Context context) {
        super(context);
    }
    ...
    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        int intercepted = 0;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastXIntercept = x;
                mLastYIntercept = y;
                mLastX = x;
                mLastY = y;
                intercepted = 0;
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
                    intercepted = 0;
                } else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
                    intercepted = 0;
                } else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
                    intercepted = 1;
                } else if (mGiveUpTouchEventListener != null) {
                    if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
                        intercepted = 1;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = 0;
                mLastYIntercept = mLastYIntercept = 0;
                break;
        }
        return intercepted != 0 && mIsSticky;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mIsSticky) {
            return true;
        }
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                mHeaderHeight += deltaY;
                setHeaderHeight(mHeaderHeight):
                break;
            case MotionEvent.ACTION_UP:
                int destHeight = 0;
                if (mHeaderHeight <= mOriginalHeaderHeight * 0.5) {
                    destHeight = 0;
                    mStatus = STATUS_COLLAPSED;
                } else {
                    destHeight = mOriginalHeaderHeight;
                    mStatus = STATUS_EXPANDED;
                }
                this.smoothSetHeaderHeight(mHeaderHeight, destHeight, 500);
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }
...
}

從上面的代碼來看,這個(gè) SitckyLayout 的實(shí)現(xiàn)有點(diǎn)復(fù)雜,在第 4 章會(huì)詳細(xì)介紹這個(gè)自定義 View 的實(shí)現(xiàn)思想,這里有個(gè)大概的印象就即可。下面我們主要看它的 onInterceptTouchEvent 方法中對(duì) ACTION_MOVE 的處理。

@Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
            switch (event.getAction()) {
                ...
                case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
                    intercepted = 0;
                } else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
                    intercepted = 0;
                } else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
                    intercepted = 1;
                } else if (mGiveUpTouchEventListener != null) {
                    if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
                        intercepted = 1;
                    }
                }

分析上面代碼的邏輯,父容器是 StickyLayout,子元素是 ListVIew。首先,當(dāng)時(shí)間落在 Header 上面時(shí)父容器不會(huì)攔截事件;接著如果豎直距離差小于水平距離差,那么父容器也不會(huì)攔截時(shí)間;然后,當(dāng) Herader 是展開狀態(tài)并且向上滑動(dòng)時(shí)父容器攔截事件。另一種情況,當(dāng) ListView 滑動(dòng)到頂部了并且向下滑動(dòng)時(shí),父容器也會(huì)攔截事件,進(jìn)過層層判斷就可以達(dá)到我們想要的效果了。另外。giveUpTouchEvent 是一個(gè)接口方法,由外部實(shí)現(xiàn),在本例中主要是用來判斷 ListView 是否滑動(dòng)到頂部,它的具體實(shí)現(xiàn)如下:

private boolean giveUpTouchEvent(MotionEvent event) {
        if (expandableListView.getFirstVisiblePosition() == 0) {
            View view = expandableListView.getChildAt(0);
            if (view != null && view.getTop() >= 0) {
                return true;
            }
        }
        return false;
    }

上面這個(gè)例子比較復(fù)雜,需要多多體會(huì)其中的寫法和思想。

掌握住上面說的通用方法,其他基本都是一些邏輯問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容