天天看點

Android事件處理分發機制的總結:二(事件分發)

之前我們知道觸摸事件是被包裝成MotionEvent進行傳遞的,而該對象是繼承了Parcelable接口,正因為如此,才可以從系統中傳遞到我們的應用中。系統通過AIDL跨程序調用了應用的Activity的dispatchTouchEvent方法,并把MotionEvent對象作為參數傳遞過來。

dispatchTouchEvent就是觸摸事件傳遞的對外接口,無論是系統傳給Activity,還是Activity傳遞給ViewGroup,ViewGroup傳遞給子View,都是直接調用對方的dispatchTouchEvent方法,并傳遞MotionEvent參數。

我們首先來看看Activity中的dispatchTouchEvent邏輯:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();    
        //這是一個空實作的方法,以便子類實作,該方法在Key事件和touch事件的dispatch方法中都被調用,就是友善使用者在事件被傳遞之前做一下自己的處理。
    }
    //這才是事件真正的分發
    if (getWindow().superDispatchTouchEvent(ev)) {
        //superDispatchTouchEvent是一個抽象方法,但是getWindow()擷取的對象實際是FrameWork層的PhoneWindow,該對象實作了這個方法,内部是直接調用DecorView的superDispatchTouchEvent是直接調用dispatchTouchEvent,這樣就傳遞到子View中了   
        return true;
    }
    //如果上面事件沒有被消費掉,那麼就調用Activity的onTouchEvent事件。
    return onTouchEvent(ev);
}

//PhoneWindow的superDispatchTouchEvent方法直接調用了mDecor的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

//mDecor即為Activity真正的根View,我們通過setContentView所添加的内容就是添加在該View上,它實際上就是一個FrameLayout
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent
}
           

至此我們已經至少明白了以下幾點:

1、我們可以重載Activity的onUserInteraction方法,在Down事件觸發傳遞前,實作我們的一些需求,實際上源碼中有很多這樣的方法,再某個方法體的第一行提供一個空實作的回調方法,在某個方法的最後一行提供一個空實作的回調方法,以便子類去實作自己的邏輯,例如AsyncTask就有類似的方式。這些技巧都能很好的提高我們代碼的擴充性。

2、Activity會間接的調用根View的dispatchTouchEvent,并通過if判斷傳回值,如果為true,即向上層傳回true,也就是調用Activity的dispatchTouchEvent的WMS,即作業系統。

3、如果if判斷為false,即根View和根View下的所有子View均為消費掉該事件,那麼下面的代碼就有執行機會,即Activity的onTouchEvent,并把該方法的傳回值作為結果傳回給上層。

接下來我們來研究ViewGroup的dispatchTouchEvent,這是稍微複雜的分發邏輯,上面的根View也就是一個ViewGroup。

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();//擷取事件
    final float xf = ev.getX();//擷取觸摸坐标
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;//擷取目前需要偏移的偏移量量
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;    //目前ViewGroup的視圖矩陣

    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != ;//是否禁止攔截

    if (action == MotionEvent.ACTION_DOWN) {//如果事件是按下事件
        if (mMotionTarget != null) {    //判斷接受事件的target是否為空
            //不為空肯定是不正常的,因為一個事件是由DOWN開始的,而DOWN還沒有被消費,是以目标也不是不可能被确定,
            //造成這個的原因可能是在上一次up事件或者cancel事件的時候,沒有把目标指派為空
            mMotionTarget = null;    //在此處挽救
        }
        //不允許攔截,或者onInterceptTouchEvent傳回false,也就是不攔截。注意,這個判斷都是在DOWN事件中判斷
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            //從新設定一下事件為DOWN事件,其實沒有必要,這隻是一種保護錯誤,防止被篡改了
            ev.setAction(MotionEvent.ACTION_DOWN);
            //開始尋找能響應該事件的子View
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - ; i >= ; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {//如果child可見,或者有動畫,擷取該child的矩陣
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        // 設定系統坐标
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        if (child.dispatchTouchEvent(ev))  {//調用child的dispatchTouchEvent
                            //如果消費了,目标就确定了,以便接下來的事件都傳遞給child
                            mMotionTarget = child;
                            return true;    //事件消費了,傳回true
                        }
                    }
                }
            }
            //能到這裡來,證明所有的子View都沒消費掉Down事件,那麼留給下面的邏輯進行處理
        }
    }
    //判斷是不是up或者cancel事件
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);

    if (isUpOrCancel) {
        //如果是取消,把禁止攔截這個标志位給取消
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; 
    }


    final View target = mMotionTarget;
    if (target == null) {
    //判斷該值是否為空,如果為空,則沒找到能響應的子View,那麼直接調用super的dispatchTouchEvent,也就是View的dispatchTouchEvent
        ev.setLocation(xf, yf);
        return super.dispatchTouchEvent(ev);
    }

    //能走到這裡來,說明已經有target,那也說明,這裡不是DOWN事件,因為DOWN事件如果有target,已經在前面傳回了,執行不到這裡
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {//如果有目标,又非要攔截,則給目标發送一個cancel事件
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setAction(MotionEvent.ACTION_CANCEL);//該為cancel
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
            //調用子View的dispatchTouchEvent,就算它沒有消費這個cancel事件,我們也無能為力了。
        }
        //清除目标
        mMotionTarget = null;
        //有目标,又攔截,自身也享受不了了,因為一個事件應該由一個View去完成
        return true;//直接傳回true,以完成這次事件,好讓系統開始派發下一次
    }

    if (isUpOrCancel) {//取消或者UP的話,把目标指派為空,以便下一次DOWN能重新找,此處就算不指派,下一次DOWN也會先把它指派為空
        mMotionTarget = null;
    }
    //又不攔截,又有目标,那麼就直接調用目标的dispatchTouchEvent
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);

    return target.dispatchTouchEvent(ev);
    //也就是說,如果是DOWN事件,攔截了,那麼每次一次MOVE或者UP都不會再判斷是否攔截,直接調用super的dispatchTouchEvent
    //如果DOWN沒攔截,就是有其他View處理了DOWN事件,那麼接下來的MOVE或者UP事件攔截了,那麼給目标View發送一個cancel事件,告訴它touch被取消了,并且自身也不會處理,直接傳回true
    //這是為了不違背一個Touch事件隻能由一個View處理的原則。
}
再來看看View中的dispatchTouchEvent是如何分發事件的:
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        //判斷mOnTouchListener是否存在,并且控件可點的情況下,執行onTouch,如果onTouch傳回true,就消耗該事件
        return true;
    }
    //如果以上條件都不成立,則把事件交給onTouchEvent來處理
    return onTouchEvent(event);
}
           

View中的處理相當簡單明了,因為不涉及到子View,是以隻在自身内部進行分發。

首先判斷是否設定了觸摸監聽,并且可以響應事件,就交由監聽的onTouch處理。

如果上述條件不成立,或者監聽的onTouch事件沒有消費掉該事件,則交由onTouchEvent進行處理,并把傳回結果交給上層。

繼續閱讀