Android事件分發機制流程解讀

事件分發的順序

Activity->Window->DecorView->ViewGroup->View

事件的類型

ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL

通常一個事件序列是這樣的ACTION_DOWN 事件是一個事件的起點,然後伴隨着多個ACTION_DOWN事件,然後是ACTION_DOWN,中間可能會收到一個ACTION_DOWN事件

Activity的事件分發

//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 的 superDispatchTouchEvent 方法,而 Window 的實現類是 PhoneWindow,因此我們直接查看 PhoneWindow 的 superDispatchTouchEvent 方法

Window的事件分發

//PhoneWindow的superDispatchTouchEvent方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

發現是直接調用的 DecorView 的 superDispatchTouchEvent 方法,再進一步查看

//DecorView的superDispatchTouchEvent方法
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

原來這兒就調用了 ViewGroup 的 dispatchTouchEvent 方法,也就是說界面上的事件直接傳遞給了根佈局的 dispatchTouchEvent 方法

分析ViewGroup是如何分發事件的,來看下ViewGroup的dispatchTouchEvent方法

分析dispatchTouchEvent方法之前,先看一下dispatchTransformedTouchEvent方法,dispatchTouchEvent方法內部會多次調用到了dispatchTransformedTouchEvent方法

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);
        // 如果傳入的 child 不爲空,則調用 child 的 dispatchTouchEvent 方法,否則調用自身的 dispatchTouchEvent 方法
        if (child == null) {
            //相當於調用了View類的dispatchTouchEvent方法
            handled = super.dispatchTouchEvent(event);
        } else {
            //繼續分發事件到子View
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ......

    // 如果傳入的 child 不爲空,則調用 child 的 dispatchTouchEvent 方法,否則調用自身的 dispatchTouchEvent 方法
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ......
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    ......
    return handled;
}

可以看出 dispatchTransformedTouchEvent 方法主要做了兩件事

  • 如果傳入的事件是 ACTION_CANCEL,或者 cancel 參數爲 true,則直接分發 ACTION_CANCEL 事件
  • 分發過程中,如果 child 爲空,則調用當前 View 的 super.dispatchTouchEvent 方法,這是因爲 ViewGroup 的 dispatchTouchEvent 方法會被重寫,而此時調用 super 的方法也就是調用 View 的 dispatchTouchEvent 方法;如果 child 不爲空,則調用這個子 View 的 dispatchTouchEvent 方法。

下面分析dispatchTouchEvent的核心代碼部分

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        
        // 1. DOWN 事件進行初始化,清空 TouchTargets 和 TouchState  mFirstTouchTarget=null,disallowIntercept=false
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 2. 檢查是否攔截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // 是否強制不允許攔截,子 View 可以設置 parent 強制不允許攔截,默認爲 false
            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 {
            intercepted = true;
        }

        ......
        // 3. 如果沒有被攔截, 先處理 DOWN 事件,主要是賦值 TouchTarget
        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 != 0) {
                    ......
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        ......
                        // 找到 Visible 並且處於點擊範圍的子 View
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        ......
                        // 相當於調用子 View 的 dispatchTouchEvent 方法
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ......
                            // 賦值 TouchTarget,刷新標誌位 mFirstTouchTarget賦值了
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                        ......
                    }
                    ......
                }
                ......
            }
        }

        // 4. 是自己處理事件還是交由子 View 處理事件
        if (mFirstTouchTarget == null) {
            // 沒有子 View 消耗事件,則自己消耗,相當於調用 super.dispatchTouchEvent 方法
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // 如果是 DOWN 事件,則上面已經調用了子 View 的 dispatchTouchEvent 方法,則什麼都不用做
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 根據 intercepted 決定是否將事件強制改爲 CANCEL 事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    // 相當於調用子 View 的 dispatchTouchEvent 方法。如果 intercepted=true,此時會強制將 action 改爲 CANCEL;如果 intercepted=false,則
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 如果 intercepted=true,則將 mFirstTouchTarget 置爲 null
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        ......
    }
    ......
    return handled;
}

dispatchTouchEvent 方法主要由4個模塊組成的

  • DOWN 事件進行初始化,清空 TouchTargets 和 TouchState mFirstTouchTarget=null,disallowIntercept=false
  • 檢查是否攔截
  • 如果沒有被攔截, 先處理 DOWN 事件,主要是賦值 TouchTarget
  • 是自己處理事件還是交由子 View 處理事件

至此ViewGroup的dispatchTouchEvent說完了,總結下

down事件ViewGroup會清空mFirstTouchTarget並且重置disallowIntercept標記爲false,所以每次down事件來臨時,父容器第一步會調用onInterceptTouchEvent詢問自己是否攔截.如果down事件攔截了,那麼此次down事件會交給ViewGroup處理,此時mFirstTouchTarget=null的,所以下次move/up事件來臨時,父容器直接將intercepted=true,所以ViewGroup一旦將down事件攔截,那麼之後的事件都將由他自己處理,子View不會收到任何事件

當down事件ViewGroup沒有攔截時,此時ViewGroup會遍歷子View,尋找能處理這個事件的View,如果沒有找到,同樣這個事件依然被ViewGroup處理,後續事件都有他處理。當ViewGroup找到了處理down事件的子View,此時會將mFirstTouchTarget賦值,即下次move事件來臨時mFirstTouchTarget!=null的

當move事件來臨時,ViewGroup依然回調用onInterceptTouchEvent詢問自己是否攔截,如果攔截了,此時mFirstTouchTarget!=null,根據上面的分析,此次子View會收到一個cancel事件,並且ViewGroup會清空mFirstTouchTarget=null,之後的事件由於mFirstTouchTarget=null,所以事件都給有ViewGroup自身處理。

分析View是如何分發事件的 dispatchTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        //這裏判斷是否設置了mOnTouchListener,是否是ENABLED,onTouch的返回值
        //所以優先級的onTouch>onTouchEvent
        if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //這裏會調用onTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }

總結一下,首先如果設置了了mOnTouchListener,它的優先級是大於onTouchEvent的。當其onTouch方法返回true的時候onTouchEvent不會被調用

分析View是如何消費事件的 onTouchEvent(MotionEvent ev)

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

        //這裏判斷view是否是可以點擊的
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        //如果view的狀態是禁用的,只要它是可以點擊的就可以消費事件,只是不做任何的響應
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        //這裏如何設置了mTouchDelegate,會先調用mTouchDelegate.onTouchEvent(event)
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        //只要是可以點擊的就會進來,且最終一定會return true 消費事件
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                      ...
                        //這裏處理點擊事件onClick
                         performClickInternal();
                    break;

                case MotionEvent.ACTION_DOWN:
                    ....
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }
                    ....
                    if (isInScrollingContainer) {
                        ....
                        postDelayed(mPendingCheckForTap, 
                    } else {
                        ....
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ....
                case MotionEvent.ACTION_MOVE:
                    ....
                    break;
            }

            return true;
        }

        return false;
    }

總結一下,只要View是可以點擊的就可以消費事件,並且如果設置了mTouchDelegate,則會先執行mTouchDelegate的onTouchEvent方法,onLongClick的執行是在action_down中,最終走到
performLongClick方法中,如下

public boolean performLongClick() {
        return performLongClickInternal(mLongClickX, mLongClickY);
    }
    
 private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
            handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
        }
        if ((mViewFlags & TOOLTIP) == TOOLTIP) {
            if (!handled) {
                handled = showLongClickTooltip((int) x, (int) y);
            }
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

得到調用。onClick的執行是在action_up中,最終走到
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;
        }

        return result;
    }

,優先級onLongClick>onClick

事件衝突的解決思路

當一個事件來臨時,不只一個View可以處理此次事件,這時候父容器不知道將事件分發給誰處理,這時候就產生了事件衝突,通過以上分析,我們可以得出來兩個思路,第一個外部攔截法,通過改變ViewGroup的onIntercept方法的返回值。第二個通過更改disallowIntercept標記,干擾ViewGroup的分發流程

思路一:外部攔截法模版代碼
public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要事件) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        return intercepted;
    }

思路二:內部攔截法模版代碼

public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章