View 事件体系之事件的分发

1,046 阅读5分钟

事件的分发流程

Activity->window->ViewGroup(decor View)

dispathTouchEvent(属于Activity)

决定是否Activity要自己消费事件,首先将事件交由依附ActivityWindow进行分发,如果View消费了事件,返回true,那么分发过程结束,如果所有的View都没有消费事件,那么AvtivityonTouchEvent()会被调用.

decorView 分发过程

顶级View(ViewGroup)对View的分发包括几个过程:
点击事件到达-> 调用dispatchTouchEvent()->如果interecptTouchEvent()返回true,消费事件->a 否则 ->b

a.如果有onTouchListener(),则 onTouch()会被触发,否则onTouchEvent()会被调用,如果在onTouchEvent中设置了onClickListener,onCLick会被调用.

b.事件会传递给事件链上的子View,子View接收到事件调用dispatchTouchEvent().

事件链是一开始ViewGroup就会遍历所有的子View来判断哪些View可以接受到事件,主要根据 子View是否在播动画点击坐标是否位于子View上.

decorView dispatch View

interecptTouchEvent

并非每次都会调用,只有当 事件是ACTION_DOWNmFirstTouchTarget != null(交由子View处理)FLAG_DISALLOW_INTERCEPT 没被设置 时调用 onInterecptTouchEvent.

因为ACTION_DOWN基本上都是一个新的事件,这个时候肯定没有交由子View来处理,所以mFirstTouchTarget == null,再加上前面就有代码对FLAG_DISALLOW_INTERCEPT进行初始化,所以如果是ACTION_DOWN,onInterecptTouchEvent一定会被调用,但是decorView(Viewgroup)默认都不会拦截事件.因此子View不能影响decorViewACTION_DOWN的处理.

如果 事件是ACTION_DOWNmFirstTouchTarget != null(交由当前View处理) 都不成立,即是收到ACTION_MOVE,ACTION_UP且已经标记不拦截事件,交由子View处理了就直接交由子View,不再拦截.
当消费事件返回true,不消费返回false.

public boolean dispatchTouchEvent(MotionEvent ev) {
   ...
   /**
   见 上一节  `interecptTouchEvent`
   */
        // 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;
        }
      ...
      /**
      分发之核心,遍历所有的子View来判断哪些View可以接受到事件,主要根据 子View是否在播动画 和 点击坐标是否位于子View上.
      */
                    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);
                        ...
                        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都没有消费事件,那么ViewGroup将会自己消费事件;
        否则继续处理后续事件(ACTION_UP,ACTION_MOVE)的分发,还有可能出现的取消点击(在屏幕上滑动离开目标View)
        不再判断,直接将点击事件传给目标View
        */        
        // Dispatch to touch targets.
        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;
            }
        }
...
}

View对点击事件的处理

以上都是点击事件怎么从Activity->Window->decor View
但是大多数情况下,处理点击事件的是View.

具体过程已经写了注释:

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处于不可DISABLE的状态,但是仍然会消费事件.
    前提是view是CLICKABLE 或者 LONG_CLICKABLE
    */
    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);
    }
    /**
    如果View有设置代理,会执行mTouchDelegate.onTouchEvent(event)
    */
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    /**
    如果View是CLICKABLE或者LONG_CLICKABLE,就消费事件.
    */
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            /**
            如果`ACTION_UP`,说明一次点击事件结束,`performClick()`方法被调用,它调用它内部的`click()`方法.
            */
            case MotionEvent.ACTION_UP:
             ...
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
            ...
                break;
            /**
            如果是`ACTION_DOWN`的情况,考虑是不是滑动操作.如果是就进行滑动操作的初始化;
            否则调用`setPressed()`来设置当前的按压状态->因为:
            事件分发到这里就以及确定了哪个`View`消费了事件,这个时候就要把这个消息往回传,告诉高等级那些大人物们.
            回传的起点自然就是当前所在的`view`的`onTouchEvent`->`dispatchSetPressed(pressed)`,
            让整个view都知道事件消费情况,并且返回`result`告诉viewGroup
            */
            //ViewGroup#dispatchTransformedTouchEvent()
             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;
                    }
            /**
            而ViewGroup#dispatchTouchEvent()收到返回值是这样的
            */
            //ViewGroup#dispatchTouchEvent()
             if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                         ...
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
            /**
            这是一个循环的一部分,这个循环遍历child(子View),用`dispatchTransformedTouchEvent()`进行分发;
            如果分发未果,没有消费事件,就遍历下一个子View;
            如果分发成功执行其内部操作,可以看到,已经分发成功,并获取到目标`View`.
            */
            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) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                  ...
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                        /**
                         * Sets the pressed state for this view and provides a touch coordinate for
                         * animation hinting.
                         *
                         * @param pressed Pass true to set the View's internal state to "pressed",
                         *            or false to reverts the View's internal state from a
                         *            previously set "pressed" state.
                         * @param x The x coordinate of the touch that caused the press
                         * @param y The y coordinate of the touch that caused the press
                         */
                    setPressed(true, x, y);
                    checkForLongClick(0);
                }
                break;
                /**
                撤销动作,算作没有消费事件
                */
            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
              ...
                break;
                /**
                移动
                */
            case MotionEvent.ACTION_MOVE:
              ...
                break;
        }
        return true;
    }
    return false;
}

事件分发-恶魔法则11条

1.同一序列事件是指从手指接触屏幕开始,到手指离开屏幕结束,中间产生的一系列以ACTION_DOWN开始,中间N个ACTION_MOVE,最后以ACTION_UP结束的一系列事件.

2.某个View一旦决定拦截事件(拦截了ACTION_DOWN),那么剩下的ACTION_MOVE & ACTION_UP都只能交由它处理,并且它的onInterceptTouchEvent()不再会被调用(其实如果是子View的话,decor View的也不会再被调用).

3.一个事件只能被一个View拦截消耗,因为后续不会再有拦截,也就没机会交给其它View处理.

4.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false)那么同一事件序列中的其它事件都不会再交由它处理, 会交由他们的父元素处理(Activity),既父元素的onTouchEvent会被调用.

5.如果View不消耗ACTION_DOWN以外的事件,这个点击事件会消失,父元素的onTouchEvent不会被调用,最后这些消失的点击事件会传给Activity处理.

6.ViewaGroup默认不拦截任何事件.

7.View没有onInterceptTouchEvent,事件传递给它后它的onTouchEvent就会被调用.

8.View的onTouchEvent默认消耗事件,除非它的CLICKABLELONG_CLICKABLE都为false.

9.View的ENABLE不影响事件的消费,只有CLICKABLE影响.

10.onClick会发生的前提是View是可点击的,并且收到了一系列事件(ACTION_DOWN->ACTION_UP),并且设置了监听(listener).

11.事件总是先传给父元素再分发给子View,可以通过requestDissallowInterceptTouchEvent来干预父元素除了ACTION_DOWN事件的分发.\