Android

Anadorid的事件分发机制

2022-03-10  本文已影响0人  漆先生
image.png

一、点击事件的传递规则

点击事件的事件分发,其实就是MotionEvent事件的分发过程。当一个MotionEvent产生后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是分发过程。点击事件的分发过程是由三个很重要的方法来共同完成:diapatchTouchEvent、onInterceptTouchEvent和onTouchEvent

1.diapatchTouchEvent

用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent的影响,表示是否消耗当前事件。

2.onInterceptTouchEvent

在diapatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

3.onTouchEvent

在diapatchTouchEvent内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。

对于根ViewGroup,点击事件产生后,首先会传递给它,这时他的dispatchTouchEvent就会被调用,如果ViewGroup的onInterceptTouchEvent返回true,表示需要拦截事件,事件就会交给ViewGroup处理,调用他的OnTouchEvent方法。如果ViewGroup的onInterceptTouchEvent返回false,不拦截事件,子元素的dispatchTouchEvent就会被调用,如此反复,直到事件被最终处理。

设置了OnTouchListener,那么监听的onTouch会被回调,如果返回true,那么onTouchEvent方法将不会被调用。由此可见onTouchListener优先级比OnTouchEvent高。onTouchEvent中,如果设置了OnClickListener,onClick方法会被调用,其优先级最低。
点击事件产生后,遵循如下顺序:Activity->Window->View。View接收到事件后,就会按照事件分发机制去处理。
如果子View的OnTouchEvent返回false,那么它的父容器的OnTouchEvent会被调用,以此类推,最终会传递给Activity的OnTouchEvent方法。

4.一些结论

  1. 同一个事件序列,是手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束。这个事件序列以down事件开始,中间是数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能被一个View拦截消耗。
  3. 某个View一但拦截,那么这个事件序列都只能由它来处理(如果能够传递给他),并且它的onInterceptTouchEvent不会再被调用。
  4. 某个View开始处理事件,如果不消耗down事件(onTouchEvent返回false),那么同一事件序列不会在交给他处理,会传给父容器的OnTouchEvent
  5. 如果View不消耗down事件之外的其它事件,那么这个点击事件会消失,父元素的onTouchEvent并不会被调用,并且View能接受到后续的事件。
  6. ViewGroup不拦截任何事件,onInteceptTouchEvent默认返回false。
  7. View没有onInteceptTouchEvent,一但事件传递给它,直接调用其onTouchEvent方法。
  8. View的onTouchEvent默认消耗事件,返回true,设置clickable和longClickable、contextClickable同时为false且悬浮显示关闭,才会返回false。
  9. View的enable属性不影响onTouchEvent的默认返回值
  10. onClick的前提是,当前View是可点击的,并且它收到了down和up事件。
  11. 事件传递是由外向内的,事件总是先传递给父元素,然后父元素再分发给子View,通过requestDisallowIntercpetTouchEvent
    方法可以干预父元素的事件分发过程,但是down事件除外。

二、事件分发的源码分析

1.activity对点击事件的分发过程

当一个点击操作发生时,事件线传递给当前的activity,由当前的activity的dispatchTouchEvent进行事件分发,具体工作是由Activity内部的Window来完成的。window会把事件传递给decorView,当前界面顶层容器。

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

window是一个抽象类,superDispatchTouchEvent方法也是一个抽象方法。其唯一的实现是PhoneWindow。

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

PhoneWindow中mDecor就是顶级的DecorView。

2.顶级View对点击事件的分发过程

顶级View一般是个GroupView,其对点击事件的分发过程,主要是在diapatchTouchEvent方法中。

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();//重置状态
            }
            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 {
                intercepted = true;
            }

这两种情况下会判断是否要拦截当前事件:事件类型是ACTION_DOWN或者mFirstTouchTarget != null。当事件由子元素成功处理,mFirstTouchTarget 就会指向子元素,就不为null了。
所以当ViewGroup进行拦截时,mFirstTouchTarget != null不成立,当ACTION_MOVE或者ACTION_UP事件到来,将会导致ViewGroup的onIntercpetTouchEvent不会再被调用。
FLAG_DISALLOW_INTERCEPT这个标记为是通过requestDisallowInterceptTouchEvent方法来设置,设置后,ViewGroup无法拦截ACTION_DOWN之外的事件。
ViewGroup会在ACTION_DOWN事件到来时,重置状态,所以View调用requestDisAllowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。

ViewGroup不拦截事件的时候,事件会分发给子View进行处理。

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, 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 (!child.canReceivePointerEvents()
                                    || !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);
                        }

遍历所有子元素,再判断子元素是否能够接收到点击事件。点击事件是否落在子元素的区域内和子元素是否在播动画,只要同时满足这两个条件,那么事件就会传递给它来处理。
dispatchTransformedTouchEvent调用的就是子元素的dispatchTouchEvent方法。
如果子元素的dispatchTouchEvent返回true,这时就不需要考虑在子元素里边是怎么分发的,mFirstTouchTarget就会被赋值,同时跳出for循环。

3.View对点击事件的处理

View对点击事件的处理过程简单一些,这里的View不包括ViewGroup。

public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            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,如果onTouch方法返回true那么onTouchEvent方法不会被调用。OnTouchListener优先级高级onTouchEvent,这样做的好处是方便在外边处理点击事件。

 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
  if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        ...
                    }
                    break;
                    }
      ...
      reurn true;
}

只要View的CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE、TOOLTIP是开启的,就会消耗掉事件。
设置了setOnClickListener会自动设置View的CLICKABLE设为true,
设置了setLongClickListener则会自动将View的CONTEXT_CLICKABLE设为true,
设置了 setOnContextClickListener则会自动将View的CONTEXT_CLICKABLE设为true。
当ACTION_UP事件发生时,会触发performClick方法。perform会调用设置的OnClickListener。

上一篇 下一篇

猜你喜欢

热点阅读