源碼探索系列12---關於事件分發機制

關於View的事件分發,實質就是關於MotionEvent時間的分發
再簡單點說就是通過一堆判斷,最後決定這個MotionEvent給誰用的問題。

  • 三巨頭
    分發過程中有主要涉及到三個人:
    dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent()
    這三者的關係如下

     public boolean dispatchTouchEvent(MotionEvent ev) {
          boolean belongToMe=false;
          if(onInterceptTouchEvent(ev)){
               belongToMe=onTouchEvent(ev);
          }else{
              belongToMe=child.dispatchTouchEvent(ev);
          }
          return belongToMe;
    }
    

    但有點擊事件產生的時候, dispatchTouchEvent被調用,然後給onInterceptTouchEvent看下要不要攔截,攔截下來的就調用onTouchEvent處理下。如果不攔截,就傳遞給子view去做,重複這個流程。
    不過需要說的是,這個onInterceptTouchEvent()是ViewGroup的,View裏面沒有這個。
    另外這個事件還受OnTouchListener這個的影響,如果我們設置了監聽,且他的onTouch()事件返回真,那麼事件是不會發到onTouchEvent裏面去的。即前者有更高的優先級。

  • 傳遞順序
    事件的傳遞順序是從Activity傳起,最後到我們的各種View裏面去的,即使父傳給子的關係。
    如果傳到底部的onTouchEvent也沒有人出來處理這個MotionEvent的話,最終這個事件會像遞歸一樣,跑回來Activity,然後他的onTouchEvent函數被調用.
    圖1

即:MotionEvent—->Activity->widnow->DecorView->ViewGroup->View->ViewGroup->DecorView->window->Activity;
就像下面這樣的:

Created with Raphaël 2.1.0ActivityActivityWidnowWidnowDecorViewDecorViewViewGroupViewGroupViewView我要去給Activity 送點擊事件咯啊哈,我收到了MotionEventdispatchTouchEventdispatchTouchEventdispatchTouchEventdispatchTouchEvent搞不定啊搞不定啊搞不定啊搞不定啊onTouchEvent()

起航

API:23

我們看下我們的Activity的處理

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

他會調用window的時間分發,把時間分發下去,如果返回的是false,再調用回自己的onTouchEvent()
這裏的getWindow()返回的是Windows類,一個抽象類,他的具體實現是PhoneWidnow
看下我們的PhoneWindow裏面寫的內容:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

跑去了mDecor即DecorView裏面去了

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker 

這DecorView是PhoneWindow裏面的一個內部類,繼承FrameLayout。
我們在Activity裏面通過setContentView(R.layout.activity_main);來設置我們的界面,而這個函數生成的View,即我們的界面是他的子View。
所以他的分發事件我們看下,是直接調用super的。這樣再去看頂部的那張圖1,ViewGroup下面一堆的View。就可以知道事件最後會分發到我們的View去。

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}

好了,我們繼續看下那個ViewGroup裏面的內容

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

      ...

    //1.  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();
    }

   //2.  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;
    } 


   //3.分發事件 
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = customOrder
                ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(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();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
    }


        // 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);
        } else {
           ...
        }

     ...
    return handled;
}

這個過程真的挺長的,一百多行,不過在看多了AMS裏面的內容,這個也就一般般的感覺了。
我們慢慢說起,

  1. 首先第一步。
    我們看下第二個函數裏面的內容

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    

    這裏他會去設置一個 FLAG_DISALLOW_INTERCEPT的標記,關於他,真的是看得好累啊。
    下次補充。可以看下這篇文章

  2. 看下攔截裏面的內容

     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;
        } 
    

    他判斷這個事件是否爲Action_Down或者mFirstTouchTarget != null來進一步選擇是否要攔截。
    前面的判斷條件好理解,後面這個mFirstTouchTarget表示的意思是, 當事件由子View成功處理後,mFirstTouchTarget會被賦值並指向childView,就是說,如果這時間被childView處理了,這標記就不是空,因此ViewGroup不再做攔截,並且事件將繼續默認都交給這個ChildView。因此這個onInterceptTouchEvent()並不是每次都回被調用,雖然我開頭那樣寫,看起來像每次都要攔截的樣子。

  3. 事件分發
    在事件分發部分的內容,他先看下這個Child是在播動畫,或者這個child的區域在不再這個Event的範圍內的,不在範圍就不發給這個child。

    if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                continue;
            }
    

    如果沒播而且在這個範圍內,就發送事件給她

    dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
    

    具體的內容是調用他的dispatchTouchEvent(),就像下面代碼一樣。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    
            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;
            }               
            ...
    }
    

    如果這個處理返回的handled是 true,那麼我們看到下面的內容:

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
    

    他標記新的touchTarGet,然後退出循環,另外在addTouchTarget()函數裏面

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

    我們看到了mFirstTouchTarget = target 這句話,前面我們在攔截的時候,有用到這個作爲一個判斷條件!判斷是否要對事件攔截。
    對於循環一圈分發完後,如果都沒人處理的話,即沒有一個ChildView或者ChildView返回了false的情況。

     // 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);
      } 
    

    這時候我們的ViewGroup就自己處理了。這個dispatchTransformedTouchEvent()我們前面有提到,因child參數被設成null,我們知道他會調用 handled = super.dispatchTouchEvent(event);這句。
    這句跑去調用的就是View的dispatchTouchEvent()去了。

小結:
這裏我們可以做個簡單的總結,當我們ViewGroup在分發事件的過程中,如果自己的childView沒一個處理好了事件,那麼這事件會從ViewGroup轉到View去分發。

前進 —— View的事件處理

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;
}

我們截取重要部分。
他看下我們有沒設置onTouchListener,如果有調用,並且如果返回的是true,那麼就結束了,不會再去調用onTouchEvent了,沒有的話纔去調用onTouchEvent。
這個onTouchevent還是挺長的,基本都是對event的Action()做處理,爲何不分割成幾個小函數呢,不就容易看多了。
哎,這裏弄個大概的樣子,方便掌握整體,清楚順序邏輯。

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == 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.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

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

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                 ...
                 break;

            case MotionEvent.ACTION_DOWN:
                 ...
                 break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                 ...
                 break;
        }

        return true;
    }

    return false;
}

我們來看下開頭的

   if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == 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.
         return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

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

這裏說的內容是,當我們設置我們的View是Disabled的狀態,不過還ClickAble的話,就消耗掉事件。
這裏補充一點:
我們平常的LONG_CLICKABLE默認是false,而CLICKABLE就分情況了,例如那個Textview就默認是false。Button默認是true。有時我習慣用Textview來替代Button做一些事,所以老要加這個熟悉的設置….

接下來就到了一句有趣的了,如果我們給View設置了代理,就調用我們的代理 onTouchEvent()去幹活。
這麼久都沒有設置過view.setTouchDelegate()有點意思,查了下,可以用來擴大觸摸點擊區域

接着看下面的內容

if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
    switch (event.getAction()) { 
            ...
    }
    return true;
}

return false;

我們的View有一個特效,只要是可點擊的狀態,不管你是不是Enable,都能消耗掉MotionEvent!

我們看下其中的一個case情況

case MotionEvent.ACTION_UP:
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { 
        boolean focusTaken = false;
        ...
        if (!mHasPerformedLongPress) {
            // 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();
                }
            }
        }
        ...
        removeTapCallback();
    }
    break;

這裏面說了一件重要的事,當我們申起手的時候,會觸發點擊事件。

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;
}

好啦,到這裏,我們的事件基本就處理完了,從Activity到最後我們的View的過程。
不過還是有一些內容沒說,下次有空記得再補充吧!

後記

在這個過程看到了關於Touch事件的委託。
Window類的具體實現的PhoneWindow,和裏面的DecorView.
重要的是其中我們熟悉的每次設置界面都調用的函數,看來下次的目標就是PhoeWindow咯。


參考資料:

FLAG_DISALLOW_INTERCEPT: 探究requestDisallowInterceptTouchEvent失效的原因

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章