Android事件分發(fā)詳解

Android事件分發(fā)詳解

1.事件傳遞的流程是從外到內(nèi),即事件總是由父元素分發(fā)給子元素:Activity->ViewGroup-View,但是通過requestDisallowInterceptTouchEvent可以使子view要求父view不攔截事件

2.當一個事件發(fā)生時,首先由Activity接收,然后activity會交由他的window處理,window的處理方式即是調(diào)用頂級容器DecorView(即setContentView所設(shè)置的View的父容器,是一個FramLayout)的dispatchTouchEvent方法,事件就進入了一個ViewGroup,開始按照事件分發(fā)機制去分發(fā)事件,若DecorView不處理,則activity會調(diào)用自身的onTouchEvent()方法消費事件

3.三個最主要方法:

  • public boolean dispatchTouchEvent(MotionEvent ev)

    此方法是事件進入某個view和ViewGroup的入口,返回true表示在此層或者下層已經(jīng)消費了事件,返回false則表示此層即下層都沒處理

  • public boolean onTouchEvent(MotionEvent event)

此方法是view和ViewGroup自身事件的處理方法,返回true表示自己處理事件,返回false表示自己不處理

  • public boolean onInterceptTouchEvent(MotionEvent ev)

此方法是ViewGroup攔截事件的方法,返回true表示攔截事件不再向下層傳遞事件,返回false表示不攔截

4.整個ViewGroup的事件分發(fā)機制大致可以用下面的偽代碼來表示:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        if(onInterceptTouchEvent(ev)){
            handled = super.dispatchTouchEvent(ev);
        }else{
            if(child.dispatchTouchEvent(ev)){
                handled = true;
            }else{
                handled = super.dispatchTouchEvent(ev);
            }
        }
        return handled;
    }

ViewGroup的dispatchTouchEvent方法的功能可分為兩部分,即事件下發(fā)部分和自身事件處理部分,ViewGroup是繼承于View,所以它的自身事件就是view的dispatchTouchEvent方法().
由兩種情況來決定走什么功能:

1.是否攔截,攔截,則直接調(diào)用super.dispatchTouchEvent()方法處理身為一個view的特性處理自身事件。不攔截則調(diào)用child.dispatchTouchEvent()下發(fā)事件 2.所有被下發(fā)事件的子view的dispatchTouchEvent()在ACTION_DOWN就返回false,表示下面無view處理,則再調(diào)用super.dispatchTouchEvent()處理自身事件。這就完成了這一輪的事件分發(fā),其他層同理,是一個遞歸的過程

3、一個事件從手指按下屏幕的那一刻到離開屏幕,中間還可能有多次滑動,這個過程以一個ACTION_DOWN開始,中間可能有0及以上個ACTION_MOVE,然后以一個ACTION_UP結(jié)束。這是一個事件序列。

4.事件的分發(fā)是一層一層分發(fā)下來的,一旦某個ViewGrou成功攔截了事件序列中的某個事件,那么此事件序列之后的所有事件都會默認被攔截(只要后續(xù)事件能傳遞到它這里),不會再去調(diào)用onInterceptTouchEvent詢問是否攔截,當然也不會再向下分發(fā),

5.如果一個事件傳遞到某個View或ViewGroup,一旦在ACTION_DOWN返回了false,那么此事件序列后續(xù)的事件都不會傳遞到這里了,并且父View的onTouchEvent方法會被調(diào)用即事件會再次向上傳遞,若一旦開始消費某個事件,那么即便在后續(xù)事件選擇不消費了(比如此View的onTouchEvent方法在ACTION_DOWN返回true,但是ACTION_MOVE返回false),后續(xù)事件也會持續(xù)的傳到這里,并且父View的onTouchEvent方法不會被調(diào)用即事件不會再次向上傳遞,這些后續(xù)被返回false的事件最終會直接傳遞給Activity處理

6.View.dispatchTouchEvent()關(guān)鍵代碼,onTouchListener的優(yōu)先級要高于onTouchEvent(),onClickListener在onTouchEvent()里面調(diào)用。:

 if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

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

onTouchEvent方法里如果clickable和longClickable有一個為真,就會調(diào)用
performClick(),就是點擊事件:

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

7.Enable屬性不影響默認的系統(tǒng)控件的onTouchEvent的返回值,只要view的clickable或longClickAble有一個為true,默認的onTouchEvent方法便會返回true,否則返回false(例如,當TextView的onTouchEvent默認返回false,因為它默認就是不可點擊的,在設(shè)置了clickable為true后,它的onTouchEvent就返回true了)

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.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

8.以ViewGroup為例,當事件傳進來時,dispatchTouchEvent中的代碼:

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

這里的條件判斷的決定是否直接將攔截標志位設(shè)為true,不再調(diào)用onInterceptTouchEvent方法去判斷。滿足條件為這個事件是ACTION_DOWN事件,或者mFirstTouchTarget不為空。ACTION_DOWN是一個新事件序列的開始肯定不能直接將攔截標志位設(shè)為true,而是要經(jīng)過onInterceptTouchEvent去決定,mFirstTouchTarget是只要下面有子view消耗了事件就會賦值,所以不為空就是下面有子view處理了事件,而如果是后續(xù)事件但是mFirstTouchTarget為空說明子view在ACTION_DOWN事件就返回了false不消耗事件,那么后續(xù)的事件也就無需再傳遞給下面了,直接將攔截標志為設(shè)為true,而不再通過onInterceptTouchEvent去決定,這里需結(jié)合4和5仔細體會。

若進入了條件為真的的代碼塊,這個disallowIntercept一般是在子view中被用parent.requestDisallowInterceptTouchEvent后結(jié)果為true,就不再走onInterceptTouchEvent判斷了,直接使攔截標志位位true,使得本parentView不攔截事件。雖然事件的分發(fā)是從外到里,但是這是特殊的子view干預父view的事件分發(fā)的辦法,另外在MotionEvent.ACTION_DOWN事件到來時,disallowIntercept標志會在時被重置,mFirstTouchTarget會被清除,因為這是新事件序列的開始,所以這也更好理解為什么在MotionEvent.ACTION_DOWN時不能直接攔截了

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

如果不攔截,就會遍歷子view,通過是否在播放動畫和事件是否落在它的范圍內(nèi)來獲得合適的 View,如果存在就為mFirstTouchTarget賦值,注意這里能找到的合適子view可能不止一個,因此如果第一個合適的view不處理事件,那么會繼續(xù)為下面合適的繼續(xù)賦值,以此類推

        if (!canceled && !intercepted) {

            // If the event is targeting accessiiblity focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList<View> preorderedList = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    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 there is a view that has accessibility focus we want it
                        // to get the event first and if not handled we will perform a
                        // normal dispatch. We may do a double iteration but this is
                        // safer given the timeframe.
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }

                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            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;
                        }

                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

接著檢驗mFirstTouchTarget是否為空,為空的可能有兩種,一是沒有合適的view,二是view不處理事件:

if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

可以看到,不管是否為空都調(diào)用了dispatchTransformedTouchEvent()這個方法,看看它的關(guān)鍵代碼

        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        return handled;

看到這里就能明白了,關(guān)鍵在于第三個參數(shù),mFirstTouchTarget為空則將child賦值空,就會調(diào)用了父view的dispatchTouchEvent方法,事件再次向上傳遞,否則直接調(diào)用了子view的dispatchTouchEvent方法,事件進入下一輪

最后事件分發(fā)大致圖:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容