天天看點

Android事件分發學習

需要搞懂的疑問

  • 當按住一個在Linearlayout裡的button後滑動出這個button,為什麼這個button還能繼續接收處理觸摸事件
  • 觸摸事件是如何傳遞給Activity,才繼續進行Activity->Window->View的分發的
  • 事件分發遞歸調用的流程整理
  • 上層ViewGroup設定onTouchListener并且在onTouch方法傳回true,為什麼它的子view還是可以接收到觸摸事件
  • 了解為什麼設定setEnable(false)後,onTouchEvent()事件還可以發生,onclick事件為什麼沒有
  • 當touch事件繼續傳遞給一個坐标不在該view内的view對象時,它是怎麼處理的
  • 當一個viewGroup有并列的子view時,是先把事件傳遞給哪個子view的
  • MotionEvent裡的x,y坐标都是相對于誰的坐标值

事件源碼分析

簡化ViewGroup的dispatchTouchEvent()方法如下:

//子view可設定該變量為true禁止攔截touch事件
private boolean isDisallowIntercept;

public boolean dispatchTouchEvent(MotionEvent ev) {
    //********預先定義處理結果變量********
    boolean handled = false;
    //重置标記位和變量
    if(actionMasked == MotionEvent.ACTION_DOWN){
        mFirstTouchTarget = null; //mFirstTouchTarget設定為null,并且清空單連結清單
        isDisallowIntercept = false; //重置失效攔截标志位,是以ACTION_DOWN的時,父view想攔截是一定可以攔截的
    }

    //********檢查是否攔截********
    final boolean intercepted;
    //如果ACTION_DOWN的時候攔截了事件,則mFirstTouchTarget=null,後續的ACTION_MOVE和ACTION_UP事件
    // 則不會進入這個判斷,onInterceptTouchEvent也就沒有再執行的機會和必要了
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        if (!isDisallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }

    //********判斷分發********
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    if (!canceled && !intercepted) {
        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != ) {
                final View[] children = mChildren;
                for (int i = childrenCount - ; i >= ; i--) {
                    final View child = (preorderedList == null)
                            ? children[childIndex] : preorderedList.get(childIndex);
                    //如果之前該child已經處理過該序列事件中的事件,找到後直接傳回
                    newTouchTarget = getTouchTarget(child);
                    if (newTouchTarget != null) {
                        break;
                    }

                    //調用child的dispatchTouchEvent()傳回,如果傳回true,則建立新的TouchTarget并加在連結清單頭
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
            }

            //如果周遊中沒找到處理的子view,并且單連結清單不為null,則newTouchTarget指向連結清單尾部元素
            if (newTouchTarget == null && mFirstTouchTarget != null) {
                newTouchTarget = mFirstTouchTarget;
                while (newTouchTarget.next != null) {
                    newTouchTarget = newTouchTarget.next;
                }
            }
        }
    }

    //********處理分發結果********
    if (mFirstTouchTarget == null) {
        // 沒有子view處理,則自己調用自己的處理方法,看自己是否處理
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            //如果是上面處理ACTION_DOWN時新到的處理子view,則直接傳回true
            if (alreadyDispatchedToNewTouchTarget && newTouchTarget == mFirstTouchTarg
                handled = true;
            } else {
                //可以看到intercepted到後面處理時還是有用的,如果一系列事件開始已經交給某個子view處理
                //當處理到中間某一個事件的時候突然攔截
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                //如果攔截了某個中間事件,則會給之前處理的子view發送CANCEL事件
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        //如果是第一次循環,且連結清單隻有一個頭元素,則next=null,相當于清空了mFirstTouchTarget
                        //則下一次事件來的時候intercepted直接為true,不會再傳遞給子view處理
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    return handled;
}
           

簡化View的dispatchTouchEvent()方法如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        //判斷OnTouchListener是否不為空,并且viw是否是ENABLED,如果都是則會執行OnTouchListener
        //的onTouch()事件。如果的onTouch事件傳回true,則直接傳回,不會執行view本身的onTouchEvent方法
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //當上面的判斷傳回false時,才有機會執行view本身的onTouchEvent方法
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, );
    }
    //OnTouchListener的onTouch()和view本身的onTouchEvent隻要有一個傳回true,
    // 則表示該view消費事件
    return result;
}
           

簡化View的onTouchEvent()方法如下:

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) != ) {
            setPressed(false);
        }
        //如果view的ENABLED為false,則當view的clickable或者longClickable有一個是true的話,則
        //消費事件,但是什麼都不做,是以onClick()方法也不會觸發。否則則直接傳回不消費事件
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    //如果設定了TouchDelegate類,則調用TouchDelegate的onTouchEvent()方法處理,
    //如果TouchDelegate的onTouchEvent()傳回true則直接傳回消費事件。
    //從TouchDelegate類文檔看該類用于幫助擴大touch區域
    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:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != ;
                //當在PREPRESSED或者PRESSED狀态的時候才進入
                if ((mPrivateFlags & PFLAG_PRESSED) !=  || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    if (prepressed) {
                        setPressed(true, x, y);
                    }
                    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) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            //觸發OnClickListener的onClick()的方法,用post方式
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
                    removeTapCallback();
                }
                break;
            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }
                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();
                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    //設定為PREPRESSED狀态
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    //檢查是否為長按事件
                    checkForLongClick();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;
            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);
                //移動出view所在的區域
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != ) {
                        removeLongPressCallback();
                        //當移出view區域時,如果目前為PRESSED狀态,則會清除PRESSED狀态,并且重新整理view
                        //比如view背景設定為一個selector,則會恢複顯示normal狀态下的背景
                        setPressed(false);
                    }
                }
                break;
        }
        return true;
    }
    return false;
}
           

小結

由于ViewGroup的onInterceptTouchEvent()方法預設實作為直接傳回false和當ViewGroup沒找到子view處理touch事件的時候,會調用super.dispatchTouchEvent(event),即view的dispatchTouchEvent()方法,是以從上面的源碼可以得出下面結論:

  • 當某一個ViewGroup或者View的dispatchTouchEven()方法傳回true的時,則相對于它們的父view表示該ViewGroup或者View消費了這次touch事件。對于ViewGroup有可能是自身的OnTouchListener或onTouchEvent方法消費了,也有可能是子view消費了,而對于View而言,則隻能是前者。但對于它們父view來說是沒有差別的,父view的mFirstTouchTarget中儲存的是該ViewGroup或者View。
  • 整個調用過程有點類遞歸的感覺,遞歸的深度則是整個布局中view層次的深度,是以會有往下深入和網上冒出的過程,俗稱的往下隧道方式,往上冒泡方式的傳遞過程。而且這樣的過程最多隻有ACTION_DOWN的時候能夠完整走一遍,當ACTION_MOVE和ACTION_UP的時候,DecorView的mFirstTouchTarget已經記錄了消費的直接子view,如果這個直接子view是ViewGroup的話,則它的mFirstTouchTarget又記錄了它裡面消費事件的子view,如此循環,最後交給最深的處的消費子view,沒有了ACTION_DOWN時周遊尋找消費子view的過程。
  • 每一個事件ViewGroup都會周遊mFirstTouchTarget單連結清單裡所有記錄的view,如果不是cancelChild則會給他們發送正常事件,如果是則會給該child發送ACTION_CANCEL事件,并且從連結清單中移除該child。判斷一個view是不是cancelChild則通過判斷該child的标記位和ViewGroup是否攔截來決定,隻有有一個成立,則是cancelChild。不管是發送正常事件還是ACTION_CANCEL事件,都會調用子view的dispatchTouchEven()方法,隻要有一個傳回true,則ViewGroup會向上傳回true表示消費該事件。
  • 當動态改變ViewGroup的onInterceptTouchEvent()方法的傳回值時,比如在ACTION_DOWN時傳回false不攔截,ACTION_MOVE的時候傳回true,則ViewGroup在第一次ACTION_MOVE事件到來時把事件轉變為ACTION_CANCEL發送給之前處理事件的子view,并且會把mFirstTouchTarget置為null,下次ACTION_MOVE或ACTION_UP時來到時,intercepted則為true表示攔截,ViewGroup會調用自己的OnTouchListener或onTouchEvent方法。
  • 當動态改變子view的dispatchTouchEven()方法傳回值時,比如在ACTION_DOWN時傳回true消費事件,ACTION_MOVE的時候傳回false,由于父ViewGroup的mFirstTouchTarget單連結清單裡儲存的該子view,隻有在它是cancelChild的時候才會移除,就算ACTION_MOVE的時候傳回false導緻子view的dispatchTouchEvent()方法傳回false,父ViewGroup的mFirstTouchTarget單連結清單還是會儲存該子view,所有後續的ACTION_MOVE和ACTION_UP事件還是會繼續發送給該子view。可以看出子view的dispatchTouchEvent()方法傳回值隻有在ACTION_DOWN事件時才能起到決定事件走向的作用。但是由于子view的dispatchTouchEvent()方法傳回false,而且也不會走父ViewGroup的OnTouchListener或onTouchEvent方法,是以最終到Activity的時,DecorView的dispatchTouchEvent()方法也會傳回false,這時候會執行Activity的onTouchEvent()方法。

回答疑問

  • 當按住一個在Linearlayout裡的button後滑動出這個button,為什麼這個button還能繼續接收處理觸摸事件

    因為Linearlayout的mFirstTouchTarget裡面儲存有該button,并且判斷它不是cancelChild,是以會一直轉發事件發給它,button内部的onTouchEvent()方法,ACTION_MOVE時會判斷是否滑出button邊界,如果是則會清除PRESSED狀态,并且重新整理背景,當ACTION_UP的時候判斷不是PRESSED狀态,是以OnClickListener的onclick()方法也不會觸發。

  • 觸摸事件是如何傳遞給Activity,才繼續進行Activity->Window->View的分發的
  • 事件分發遞歸調用的流程整理
  • 上層ViewGroup設定onTouchListener并且在onTouch方法傳回true,為什麼它的子view還是可以接收到觸摸事件

    因為如果子view的dispatchTouchEven()方法傳回true的話,則會先發送給子view消費,不會走到ViewGroup的onTouchListener和onTouchEvent()方法。隻有當點選ViewGroup中未包含子view的地方才會不經過子view直接自己消費。

  • 了解為什麼設定setEnable(false)後,onTouchEvent()事件還可以發生,onclick事件為什麼沒有

    因為setEnable(false)後,view的CLICKABLE或者LONG_CLICKABLE還是true的,是以還可以繼續消費事件,隻是接收後,什麼都不做直接傳回true,是以onclick事件不會觸發

  • 當touch事件繼續傳遞給一個坐标不在該view内的view對象時,它是怎麼處理的

    同第一個問題,ACTION_MOVE時會判斷是否滑出button邊界,如果是則會清除PRESSED狀态,并且重新整理背景,當ACTION_UP的時候判斷不是PRESSED狀态,是以OnClickListener的onclick()方法也不會觸發。

  • 當一個viewGroup有并列的子view時,是先把事件傳遞給哪個子view的

    preorderedList中的順序是按照addView或者XML布局檔案中的順序來的,後addView添加的子View,會因為Android的UI後重新整理機制顯示在上層,是以有覆寫的并列子view時,會先傳遞給上層view(已驗證源碼待分析)

  • MotionEvent裡的x,y坐标都是相對于誰的坐标值

    MotionEvent有四個方法getRawX(), event.getRawY(), getX(),getY()。awX和rawY是相對于螢幕的坐标,x和y是相對于目前控件的坐标。rawX和X 向右移動都是增大,向左移動都是減小,rawY和 Y 向下移動都是增大,向上移動都是減小。