美文网首页面试专题Mobile
Android 事件分发与拦截机制详解

Android 事件分发与拦截机制详解

作者: Little丶Jerry | 来源:发表于2018-02-17 21:03 被阅读0次

    一、什么是事件?

    在我们通过屏幕与手机交互的时候,每一次手指的按下、移动、抬起就是一个个事件。按照面向对象的思想,这些一个个事件都被封装成了 MotionEvent 对象。

    二、什么是事件序列?

    同一个事件序列是指:从手指触摸屏幕的那一刻开始,到手指离开屏幕那一刻结束,在这个过程中产生的一系列事件。这一系列事件以 ACTION_DOWN 事件开始,以 ACTION_UP 事件结束,中间可能还有一些 ACTION_MOVE 事件。

    千万别把一个事件序列当成一个事件,不然会对理解整个事件分发机制感到迷惑。比如事件处理是以事件为单位,而不是以事件序列为单位。即有可能只处理 ACTION_DOWN 事件,而不处理后面的 ACTION_MOVE、ACTION_UP 事件。

    三、事件的传递顺序是怎样的?

    事件传递的方向是由外向内的,即事件总是先传递给父视图,然后由父视图分发给子视图。

    四、三个重要的方法

    其实要理解事件分发机制,不外乎掌握以下三个方法。这三个方法有一个共同点,就是它们是否执行自己的功能(分发、拦截、消耗)完全由自己的返回值来确定,返回 true 就表示自己完成了自己的功能(分发、拦截、消耗)。

    • public boolean dispatchTouchEvent (MotionEvent event)
      在 View 类以及 ViewGroup 类中均有定义。
      注意:ViewGroup 类继承 View 类,因此该方法在 ViewGroup 类中是重写。

    作用:
    用来进行事件的分发,传递触摸屏幕(Touch)事件至目标 View,除非当前视图就是目标 View。

    参数:
    event,被分发的动作事件。Touch 事件的相关细节(发生触摸的位置、时间等)被封装成 MotionEvent (动作事件对象)。

    返回值:
    返回 true 表示该事件将在当前 view 中处理,消耗了该事件,因此不再往下传递事件;返回 false 则表示继续向子视图传递该事件。默认返回 false。

    如果事件能够传递到当前视图,此方法一定会被调用。因为该方法在 View 类和 ViewGroup 类中均有定义,所以逻辑表达上会有不同的意思。

    ViewGroup 类 dispatchTouchEvent() 的返回值受 ViewGroup 视图 onTouchEvent() 方法和子视图 dispatchTouchEvent() 方法返回值的影响。

    View 类中 dispatchTouchEvent() 源码:

        public boolean dispatchTouchEvent(MotionEvent event) {
            // If the event should be handled by accessibility focus first.
            if (event.isTargetAccessibilityFocus()) {
                // We don't have focus or no virtual descendant has it, do not handle the event.
                if (!isAccessibilityFocusedViewOrHost()) {
                    return false;
                }
                // We have focus and got the event, then use normal event dispatch.
                event.setTargetAccessibilityFocus(false);
            }
    
            boolean result = false;
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(event, 0);
            }
    
            final int actionMasked = event.getActionMasked();
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Defensive cleanup for new gesture
                stopNestedScroll();
            }
    
            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;
                }
            }
    
            if (!result && mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
            }
    
            // Clean up after nested scrolls if this is the end of a gesture;
            // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
            // of the gesture.
            if (actionMasked == MotionEvent.ACTION_UP ||
                    actionMasked == MotionEvent.ACTION_CANCEL ||
                    (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
                stopNestedScroll();
            }
    
            return result;
        }
    

    可以看到,在 View 中的 dispatchTouchEvent() 方法内有一个判断,里面有三个条件,如果三个条件都满足,就返回 true,否则返回 onTouchEvent() 方法执行的结果。

    • 第一个条件是判断 mOnTouchListener 变量是否为空,这个变量是在 View 中的 setOnTouchListener() 方法中赋值的,也就是说只要我们给控件注册了 onTouch() 事件,mOnTouchListener 就一定被赋值。

    • 第二个条件是判断控件是否 enable。

    • 第三个条件是 onTouch() 方法的返回值。如果我们在 onTouch() 方法中返回 true,基本上,dispatchTouchEvent() 就会返回 true,不再继续传递事件,也不会执行 onTouchEvent() 方法。如果我们在 onTouch() 方法里返回 false,则会执行 onTouchEvent() 方法。

    因此,如果 onTouch() 返回 true,onClick() 就不会再执行。
    注意:这里引申出三个问题。

    1. 什么时候调用 onClick() 方法?

    View 类中 onTouchEvent() 部分代码:

        case MotionEvent.ACTION_UP:
             ......
            //处理 click 事件
            if (!focusTaken) {
    
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
            }
        }
              .....
        break;
    

    当手指抬起,ACTION_UP 事件发生时,会触发 performClick() 方法,performClick() 方法又会调用 onClick() 方法。对于 performClick() 方法,只要 mOnClickListener 不为空,就会去调用它的 onClick() 方法。又因为 onTouch() 方法默认返回 false,因此,通常执行完 onTouch() 方法后,还会执行 onClick() 方法就是这个原因。

    onClick() 方法会调用的前提是:当前 View 是可点击的,并且它收到了 ACTION_DOWNACTION_UP 的事件。

    2. 什么时候调用 onLongClick() 方法?

    View 类中 onTouchEvent() 部分代码:

        case MotionEvent.ACTION_DOWN:
            
            // For views inside a scrolling container, delay the pressed feedback for
            // a short period in case this is a scroll.
            if (isInScrollingContainer) {
              ......
            } else {
                // Not inside a scrolling container, so show the feedback right away
                setPressed(true, x, y);
                checkForLongClick(0, x, y);
            }
         break;
    

    当手指按下并持续一段时间,ACTION_DOWN 事件发生时,会触发 checkForLongClick() 方法,如果该 View 设置了 onLongClickListener,那么 checkForLongClick() 方法又会调用 onLongClick() 方法。

    3. onTouch() 和 onTouchEvent() 有什么区别,两者又该如何使用?

    这两个方法都是在 View 类的 dispatchTouchEvent() 中调用的。

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

    从源码中可以看出,onTouch() 先于 onTouchEvent() 执行。如果在 onTouch() 方法中通过返回 true 将事件消费掉,则 onTouchEvent() 将不会再执行。

    另外需要注意的是,onTouch() 能够执行需要两个前提条件:

    • mOnTouchListener 的值不能为空;

    • 当前处理事件的 View 必须是 enable 的;

    所以,如果我们有一个控件是 disable 的,那么给它设置的 onTouch() 方法将永远得不到执行。对于这一类控件,如果我们想要监听它的 Touch 事件,就必须在该控件中重写 onTouchEvent() 方法来实现。

    一个不可用(disable)的 View 仍然可以消耗事件,只是不做任何响应并返回 false。View 的 enable 属性不影响 onTouchEvent() 的默认返回值。如果一个 View 是 disable 状态,只要它的 clickable 和 longClickable 属性有一个为 true,那么它的 onTouchEvent() 就返回 true。

    看完 View 类中的 dispatchTouchEvent() 代码,下面来看看
    ViewGroup 类中 dispatchTouchEvent() 代码:

    @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
            }
    
            // If the event targets the accessibility focused view and this is it, start
            // normal event dispatch. Maybe a descendant is what will handle the click.
            if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
                ev.setTargetAccessibilityFocus(false);
            }
            // handled 是用作返回值的变量,表示事件是否被分发,默认是 false
            boolean handled = false;
            if (onFilterTouchEventForSecurity(ev)) {
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
    
                // Handle an initial down.
                // 处理一个新的 ACTION_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();
                }
              
                // Check for interception.
                // intercepted 是用来记录是否被拦截的结果
                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.
                    // mFirstTouchTarget 为空,同时事件为非
                    // ACTION_DOWN 事件,那么在这里进行拦截
                    // mFirstTouchTarget 是触摸目标列表的第一个触摸目标
                    // (First touch target in the linked list of touch targets.)
                    intercepted = true;
                }
    
                // If intercepted, start normal event dispatch. Also if there is already
                // a view that is handling the gesture, do normal event dispatch.
                if (intercepted || mFirstTouchTarget != null) {
                    ev.setTargetAccessibilityFocus(false);
                }
    
                // Check for cancelation.
                final boolean canceled = resetCancelNextUpFlag(this)
                        || actionMasked == MotionEvent.ACTION_CANCEL;
    
                // Update list of touch targets for pointer down, if needed.
                final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
                TouchTarget newTouchTarget = null;
                boolean alreadyDispatchedToNewTouchTarget = false;
    
                // 如果事件没有取消或被拦截
                if (!canceled && !intercepted) {
                    ......
                    // 这里开始对事件类型进行区分,如果是
                    // ACTION_DOWN,就是一个新的事件序列开始
                    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);
    
                        // mChildrenCount 记录子 View 个数
                        final int childrenCount = mChildrenCount;
                        if (newTouchTarget == null && childrenCount != 0) {
                            // 获取到点击的坐标,用来从所有子 View 中筛选出目标 View
                            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.
                            // 接下来从前往后检索能够收到事件的子 View
                            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 (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
                                // 找到了合适的子View,这里将子View封装为一个target
                                // 要是newTouchTarget不为空就跳出循环
                                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;
                                }
                                
                                // 就算返回结果为空也没关系,在这里继续递归调用
                                // 子View的dispatchTransformedTouchEvent()
                                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();
                        }
                        
                        // 没有找到可接受事件的View
                        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;
                        }
                    }
                }
    
                // Dispatch to touch targets.
                // 接下来是对于非 ACTION_DOWN 事件的分发了,这里有两种情况
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    // 1. 如果 mFirstTouchTarget 为 null,调用
                    // dispatchTransformedTouchEvent() 且传一个 null 的 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.
                    // 2. 之前有 View 接受了 ACTION_DOWN 事件,那么 
                    // 这个 View 也将接受其余的事件
                    TouchTarget predecessor = null;
                    TouchTarget target = mFirstTouchTarget;
                    while (target != null) {
                        final TouchTarget next = target.next;
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            // alreadyDispatchedToNewTouchTarget这个变量在之前
                            // View接受ACTION_DOWN事件时设置为true
                            // 同时这个mFirstTouchTarget也就是之前那个
                            // View封装好的target
                            // 那么返回值handled就为设置为true,表示已经分发
                            handled = true;
                        } else {
                            // 如果mFirstTouchTarget不为空,对于非ACTION_DOWN事件,
                            // 递归调用dispatchTransformedTouchEvent() 
                            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;
                    }
                }
    
                // Update list of touch targets for pointer up or cancel, if needed.
                // 根据需要,更新触摸目标列表。
                if (canceled
                        || actionMasked == MotionEvent.ACTION_UP
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    resetTouchState();
                } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                    final int actionIndex = ev.getActionIndex();
                    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                    removePointersFromTouchTargets(idBitsToRemove);
                }
            }
    
            if (!handled && mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
            }
            return handled;
        }
    

    可以看到,上面方法中多次调用了
    dispatchTransformedTouchEvent() 方法,我们来看一下它的源码:

        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
    
            // 处理 ACTION_CANCEL 事件
            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;
            }
             ......
            final MotionEvent transformedEvent;
            if (newPointerIdBits == oldPointerIdBits) {
                if (child == null || child.hasIdentityMatrix()) {
                    if (child == null) {
                        // 如果传来的参数child为空的话,则调用ViewGroup的dispatchTouchEvent()
                        handled = super.dispatchTouchEvent(event);
                    } else {
                        final float offsetX = mScrollX - child.mLeft;
                        final float offsetY = mScrollY - child.mTop;
                        event.offsetLocation(offsetX, offsetY);
                        // 不为空,则调用child的dispatchTouchEvent()
                        handled = child.dispatchTouchEvent(event);
    
                        event.offsetLocation(-offsetX, -offsetY);
                    }
                    return handled;
                }
                transformedEvent = MotionEvent.obtain(event);
            } else {
                transformedEvent = event.split(newPointerIdBits);
            }
    
            // Perform any necessary transformations and dispatch.
            if (child == null) {
                handled = super.dispatchTouchEvent(transformedEvent);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                transformedEvent.offsetLocation(offsetX, offsetY);
                if (! child.hasIdentityMatrix()) {
                    transformedEvent.transform(child.getInverseMatrix());
                }
    
                handled = child.dispatchTouchEvent(transformedEvent);
            }
    
            // Done.
            transformedEvent.recycle();
            return handled;
        }
    

    它的作用是:将事件传给为指定的子视图。如果子视图为 null,则事件将交回给此视图组。

    • public boolean onInterceptTouchEvent(MotionEvent ev)
      仅在 ViewGroup 类中有定义,View 类中没有该方法。
      注意:ViewGroup 类继承 View 类。

    作用:
    用于拦截所有触摸屏幕动作事件。

    参数:
    正在被分发到下层的动作事件。

    返回值:
    返回 true 表示拦截了事件,即窃取子视图的动作事件并通过 onTouchEvent() 方法,将它们分发给该 ViewGroup。而目标视图则会收到一个 ACTION_CANCEL 的事件,并且不会有更多的信息发送给它。

    • ViewGroup 默认不拦截事件,即 onInterceptTouchEvent() 方法默认返回 false。

    • 如果当前 View 拦截了某个动作事件,那么在同一个事件序列当中,此方法不会被再次调用。

    ViewGroup 中 onInterceptTouchEvent() 源码:

        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                    && ev.getAction() == MotionEvent.ACTION_DOWN
                    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
                return true;
            }
            return false;
        }
    
    • public boolean onTouchEvent (MotionEvent event)
      仅在 View 类中有定义,ViewGroup 类中无重写。
      注意:ViewGroup 类继承 View 类。

    作用:
    处理屏幕动作事件。

    参数:
    event,动作事件。

    返回值:
    返回 true 表示该事件已经被处理,否则返回 false

    • View 的 onTouchEvent() 方法默认返回 true,即默认消耗事件,除非它是不可点击的(clickable 和 longClickable 同时为 false)。

    • 如果不处理事件(即返回 false),则当前 View 无法再次接收同一个事件序列中的其他事件。

    View 类中 onTouchEvent() 源码:

    
        public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            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;
            }
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    
            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) {
                            // 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) {
                                // The button is being released before we actually
                                // showed it as pressed.  Make it show the pressed
                                // state now (before scheduling the click) to ensure
                                // the user sees it.
                                setPressed(true, x, y);
                            }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                // 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();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        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) {
                            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(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    
            return false;
        }
    

    三个方法间的联系

    三个方法间的联系可以用如下代码形象地表示:

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

    通过上面的伪代码,可以看出,事件的分发机制大致如下:

    对于一个父 ViewGroup 来说,屏幕触摸事件产生后,该事件首先会传递给它,这时它的 dispatchTouchEvent() 方法就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent() 方法返回 true 就表示它要拦截当前事件,接着事件就会交给该 ViewGroup 处理,即执行 onTouchEvent() 方法。如果 onInterceptTouchEvent()
    方法返回 false 就表示它不拦截该事件,这时事件就会继续传递给它的子视图,接着子视图的 dispatchTouchEvent() 就会被调用,如此反复,直到事件被处理。

    五、总结:

    1. ViewGroup 的 dispatchTouchEvent() 是真正在执行 “分发” 工作,而 View 的 dispatchTouchEvent() 方法,并不执行分发工作,或者说它分发的对象就是自己,决定是否把 Touch 事件交给自己处理,而处理的方法,就是 onTouchEvent()。

    2. 当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent() 方法来询问自己是否要拦截该事件。

    3. 正常情况下,一个事件序列只能被一个 ViewGroup 拦截并消耗(处理)。

    4. 某个 ViewGroup 一旦决定拦截,那么该事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent() 方法不会再被调用。

    5. View 没有 onInterceptTouchEvent() 方法,一旦有事件传递给它,那么它的 onTouchEvent() 方法就会被调用。

    6. 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件,即 onTouchEvent() 返回了 false,那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理,即父视图的 onTouchEvent() 会被调用。

    7. 如果 View 不消耗 ACTON_DOWN 以外的其他事件,那么这个点击事件会消失,此时父视图的 onTouchEvent() 并不会被调用,并且当前 View 可以持续收到后续的事件。最终,这些消失的屏幕触摸事件会传递给 Activity 处理。

    相关文章

      网友评论

        本文标题:Android 事件分发与拦截机制详解

        本文链接:https://www.haomeiwen.com/subject/yegdtftx.html