View 的事件分发 原理和源码分析

很多人都是学了忘,忘了学,感觉永远也记不住。View 事件分发到底应该怎么学?

其实很简单:

1、敲代码
2、学习原理
3、画流程图

View 事件分发 其实就4️部分组成

1、 dispatchTouchEvent(MotionEvent ev)

2、 onInterceptTouchEvent(MotionEvent ev)

3、 onTouchEvent(MotionEvent ev)

4、 MotionEvent

那么我们就详细的了解一下这四个的具体含义。


dispatchTouchEvent(MotionEvent ev)

从名字就可以看出这个方法主要用来事件的分发,每个 View 都有这个方法,如果消息能够传递到这个 View,那么这个方法一定能执行。返回 true 代表消耗这个事件,false 表示不消耗。这个结果受两个因素的影响。

1、当前 View 的 onTouchEvent() 的返回值。
2、下级 View dispatchTouchEvent() 方法的返回值

onInterceptTouchEvent(MotionEvent ev)

这个方法是在dispatchTouchEvent() 中调用,,事件拦截,这个只有在 ViewGroup 才有方法,View 是没有的。如果当前 View 拦截了这个事件,那么在一个事件序列中这个方法不会被再次调用了。直到这个事件序列结束。返回的结果表示是否消耗了这个事件。

onTouchEvent(MotionEvent ev)

这个方法是在dispatchTouchEvent() 中调用,用来处理点击事件,返回结果表示是否消耗了这个事件。如果不消耗,返回 false 那么在同一系列事件中,将不会再给这个事件派发,也就是再次收到事件。

在艺术探索一书中有这么一段伪代码

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

我们可以通过记忆这一段简单的伪代码,理解整个 View 的事件分发。


1、一个事件序列
事件序列
2、事件分发顺序
事件分发顺序
事件分发流程

通过伪代码和图示我们大概了解了 View 的事件分发,但是更具体的还需要源码去分析。

我们便从 Activity 到 View 一起来走一遍,看看具体代码是怎么实现的,里面有什么细节给我们解惑的。

1、Activity

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

我们结合上图和源码,可以看到很简单的几段代码,事件分发调用的是 Windowd 的superDispatchTouchEvent 分发。接着往下看。
我们知道 Window 是个抽象类,所以用实现类 PhoneWindow。

  mWindow = new PhoneWindow(this, window, activityConfigCallback);

PhoneWindow 又托管给 DecorView ,就是我们说的 RootView 或者 RootViewGroup。

    mDecor = (DecorView) preservedWindow.getDecorView();
          @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

其实就是 ViewGroup 的dispatchTouchEvent 方法。

   public boolean dispatchTouchEvent(MotionEvent ev) {
      ······
//从这里开始 默认 handled 为 false  表示不消耗
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 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.
                //  mFirstTouchTarget = null;重置mFirstTouchTarget 和 FLAG_DISALLOW_INTERCEPT
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 表示是否拦截
            final boolean intercepted;
        //当事件类型为 ACTION_DOWN 或者 mFirstTouchTarget 不为空的时候
//什么是 mFirstTouchTarget ?整体分析可知 当这个事件被子 view 消耗的时候,
//mFirstTouchTarget 会指向这个 view,也就是第一个消耗这个事件的 view。
//当 ViewGroup 不拦截的时候且子 view消耗了这个事件 那么mFirstTouchTarget !=null。
//那么 当事件类型为 MOVE 或者 UP 的时候,如果 mFirstTouchTarget !=null 那就说明
//有 view 消耗这个事件了,该系列的事件都默认交给它处理
//也就是事件的开始 和 事件有消耗后 都会走这个方法。当然也不是绝对的
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT这个参数是子 view 通过requestDisallowInterceptTouchEvent 方法,改变这个值 ,来控制 父 view 是否拦截
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//当需要拦截的时候 那就走自己的onInterceptTouchEvent 方法具体自己实现 ,不需要那就intercepted 为 false
//需要注意的是  对于 DOWN 子 view 通过 requestDisallowInterceptTouchEvent 方法是控制不了父 view 的拦截方法的,因为当有 DOWN 事件的时候 就会触发 之前的重置方法。
//所以只能针对 MOVE UP 等,可以让 父 view 不在拦截事件,可以使父 view 开始拦截事件
//正是因为 FLAG_DISALLOW_INTERCEPT ,如果我们父 view 开始拦截,并消耗了这个事件,那么 mFirstTouchTarget!=null 就不会成立,因为ViewGroup 的子 view 成功消耗这个事件后 才会mFirstTouchTarget!=null。所以以后的事件也就不会再走 onInterceptTouchEvent,直接返回  intercepted = true 。
                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;
            }

            // 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;
//当ViewGroup不拦截时,这里对 ViewGroup 的子 view 进行处理 
            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 = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍历子 view
                        for (int i = childrenCount - 1; i >= 0; i--) {
//这一堆的代码有点复杂 不过就一个目的 当前子 view 是都能接受点击事件
//如何判断呢? 1、是否在播放动画 2、点击的坐标是否在该子 view 的区域
                            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;
                            }
//这就是判断是否可以点击的方法之一    view 要显示,是否在播放动画
  /**
     * Returns true if a child view can receive pointer events.
     * @hide 
    * private static boolean canViewReceivePointerEvents(@NonNull View child) {
    *     return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
     *            || child.getAnimation() != null;
    * }
 */
//判断二  是否在子 view 的范围
/**
* protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }
*
*/
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
//mFirstTouchTarget 是一个链表形式 遍历这个链表 如果有这个 view 那么就直接赋值 newTouchTarget   
//注意 mFirstTouchTarget 是在View 的子 view中赋值的
// 说明点击确实在子 view 的区域 直接返回不用再继续遍历了
                            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);
//实际是 调用 子 view  的 dispatchTouchEvent 方法。依次递归下去
//如果 子 view dispatchTouchEvent 返回true 那么  newTouchTarget = addTouchTarget(child, idBitsToAssign);
// newTouchTarget 被赋值并跳出循环  其实是 mFirstTouchTarget链表添加数据
                                alreadyDispatchedToNewTouchTarget = true;  
                            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();
                    }
//当 newTouchTarget 还是null 那就 链表 最后一个 赋值给newTouchTarget
                    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.

            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
//当 mFirstTouchTarget 为空 其实也就是子 view 为空,调用 view 的 dispatchTouchEvent 方法 
//就是 viewGroup 如果需要消耗事件 其实调用的是 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;
                }
            }

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

我们来看下 ViewGroup 的分发。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
//当没有子 view 那么它就相当于 view 调用 view 的方法。
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
//如果有 那么就 递归这个方法了
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
//当没有子 view 那么它就相当于 view 调用 view 的方法。
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    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) {
//当没有子 view 那么它就相当于 view 调用 view 的方法。
            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;
    }

其实我们发现这一切都和 mFirstTouchTarget 有关系,当 viewGroup 需要自己消耗事件的时候,那么 mFirstTouchTarget 肯定是 null,也就是子 view 不消耗,或者自己拦截,都会造成 mFirstTouchTarget ==null,那么当调用 dispatchTransformedTouchEvent 方法是,就会触发 view 的 onTouchEvent 方法,viewGroup 自己去处理事件。

首先我们看看 View 的 dispatchTouchEvent() 方法(只保留了我们需要的源码)。

 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;
  //判断是否有监听事件  并且 mOnTouchListener不为空
            //view 的  enabled 为true 并且onTouch方法也返回true
          //那么直接返回 true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
//当 onTouch 被调动 且返回true 那么onTouchEvent不会调用了
            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 可以调用 onTouch() 且返回 true, onTouchEvent就不会再调用了,这就是一个优先级的问题了。

最后是 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;
//就算   不可用的情况下 也可能会消耗点击事件   只要 clickable 为true
        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;
        }
//如果 view 有代理会做这个方法 具体不说了 
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
 //只要控件的clickable 或者 long_clickable 或者 CONTEXT_CLICKABLE 有一个为 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
//这是 up  所以 长按事件回调移除
                            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.
// 我们设置点击事件 是在这儿 开始的~~看好了  我们发现是通过 post  把 run 里的调用 performClick() 方法。用 Runnable ,通过 post 形式执行 performClick,而不是直接调用 performClick , 因为这可以让这个 view 更新的视觉状态在这个方法执行之前。
//这个比如当这个 view 绘制 显示 完成之后 才可以 点击 之类的 等等 
                                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;
            }
//当 clickable 为 true 那么一定返回 true 不管执行了 点击事件啥 或者 其他 这个事件在这儿一定会消耗  
//就是返回 true 所以当设置 不可点击的时候且clickable 为false 才会不消耗这个事件
            return true;
        }

        return false;
    }

超级会员:mOnTouchListener
会员:onTouchEvent
普通用户:setOnClickListener

最后根据上面的源码分析后,我们整理一些结论,结合结论和源码分析,我们更好的理解 View的事件分发。(copy from 《艺术探索》 + 自己的理解)

1、同一个事件序列是从手指触摸屏幕的那一刻开始,直到手指离开屏幕的那一刻结束,在这个过程产生的一系列事件,是从 down 开始 经过 n 个 move 最后 up 结束。

2、正常情况下,一个事件只能被一个 View 拦截且消耗,原因参考 3。一旦一个 View 拦截了某事件,那么这个事件序列内的所有事物都会直接交给它处理,因此同一个事件序列中的事件不能由两个 view 同时处理,注意是同时,但是通过特殊手段可是,比如将一个View 的事件强行传递给其他view,这是无赖操作。

3、某个 View 一旦决定拦截,那么这个事件序列只能由他来出来,如果事件能传递给他,而且他的 onInterceptTouchEvent 不会再继续调用,因为 View 拦截那么mFirstTouchTarget 就不会在 子 View 中赋值,mFirstTouchTarget ==null 直接返回 true ,最后走 View 的 ouTouchEvent 方法。

4、某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件( onTouchEvent 返回 false),那么同一序列的而其他事件都不会交给他处理,而是将事件重新交给他的父元素处理,即 父 View 的 onTouchEvent 会调用 。源码上其实就是 比如子 View onTouchEvent 返回 false,因为 dispatchTouchEvent 是 递归的,所以返回 子 view 的dispatchTouchEvent 返回 false,最后 mFirstTouchTarget 还是null ,所以 父 view(ViewGroup) 调用 View 的 dispatchTouchEvent 进行处理,最后父 View调用onTouchEvent 方法,来处理这个事件。

5、如果 View 不消耗 ACTION_DOWN 以外的其他事件,那么这个点击事件就会消失,此时的父元素的 onTouchEvent 不会调用,而且当前 View 可以持续收到后续的事件,最终这些消失的事件会交给 Activity 处理。

6、ViewGroup 默认不拦截任何事件,即 onInterceptTouchEvent 返回 false。

7、View 没有 onInterceptTouchEvent。事件传递给它,ouTouchEvenr 就会调用。

8、View 的 ouTouchEvent 会默认消耗事件 返回true。除非他是不可点击 clickable 和longclickable 同时是 false。longclickable默认为false,button 默认 clickable 是true,textview 默认 false。

9、View 的enable 属性不影响 ouTouchEvent 的返回值,只要longclickable 或 clickable 有一个是 true ,那么ouTouchEvent 就返回 true。

10、onClick 会发生的前提是 view 可点击。并过去收到了down 和up。

11、事件传递是从外向内的,事件总是先传递给父view,然后再有父view分发给子view,通过 requestDisallowInterceptTouchEvent 方法,子类可以干预父 View 的事件分发过程。ACTION_DOWN 除外。

12、mOnTouchListener > onTouchEvent > setOnClickListener

参考 《Android开发艺术探索》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容