View的事件分发机制(Android开发艺术探索学习笔记)

从Activity到DecorView

当一个触摸事件发生的时候,事件首先传递给当前的Activity,由Activity的dispatchTouchEvent(MotionEvent ev)进行分发。代码如下:

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

从代码中可以看出,Activity会把事件交给自己对应的Window处理。从Window类顶部的注释中可以看出,Window是一个抽象类,Window的唯一具体实现类是PhoneWindow,如下:

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */

PhoneWindow重写了superDispatchTouchEvent(ev),如下:

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

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

可以看到Window其实又把事件交给DecorView处理,DecorView就是Window中的顶级容器,对应setContentView中View的父容器。

如果Window成功分发,Activity中的dispatchTouchEvent(MotionEvent ev)就返回true;如果没分发成功,就调用Activity自己的onTouchEvent(ev)。

如果事件传递给了DecorView,就会到ViewGroup的层面进行事件分发。

ViewGroup的事件分发涉及到的主要的三个方法:

1.public boolean dispatchTouchEvent(MotionEvent ev)
这个方法View和ViewGroup中都有。

2.public boolean onInterceptTouchEvent(MotionEvent ev)
这个方法只有ViewGroup中有,默认不做其他处理,直接返回false。

3.public boolean onTouchEvent(MotionEvent event),这个方法只有View中有。

ViewGroup的dispatchTouchEvent()

首先贴上API23中ViewGroup中dispatchTouchEvent()的源码:

    /**
     * {@inheritDoc}
     */
    @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);
        }

        boolean handled = false;
        // 安全性检查,要不要分发本次事件
        if (onFilterTouchEventForSecurity(ev)) {
            // 获取MotionEvent的事件类型
            final int action = ev.getAction();
            // actionMasked能够区分出多点触控事件
            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.
                // 清理上一次接收触摸事件的View的状态
                cancelAndClearTouchTargets(ev);
                // 重置ViewGroup的触摸相关状态
                resetTouchState();
            }

            // Check for interception.
            // 这个变量用于检查是否拦截这个TouchEvent
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 如果触摸事件是ACTION_DOWN或者之前有子View处理了该事件流
                // 检查是否不允许拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 如果没有不允许拦截,就是允许拦截。
                    // 调用onInterceptTouchEvent看ViewGroup是否拦截事件
                    intercepted = onInterceptTouchEvent(ev);
                    // 在这里防止Event中途被篡改
                    // 所以要篡改TouchEvent的Action不要在这之前改
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    // 不允许拦截就直接设置为false
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                // 如果没有子View,并且也不是ACTION_DOWN事件,直接设置为拦截
                // 这样后面就自己处理事件
                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.
            // 检查父View是否支持多点触控,即将多个TouchEvent分发给子View,
            // 通过setMotionEventSplittingEnabled()可以修改这个值。
            // FLAG_SPLIT_MOTION_EVENTS在3.0是默认为true的,即支持多点触控的分发。
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            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.
                // 检查TouchEvent是否可以触发View获取焦点,如果可以,查找本View中有没有获得焦点的子View,
                // 有就获取它,没有就为null
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                // 可以执行if里面的情况:
                // 1. ACTION_DOWN事件
                // 2. 支持多点触控且是ACTION_POINTER_DOWN事件,即新的事件流的DOWN事件
                // 3. 需要鼠标等外设支持,暂时无意义
                // 就是说同一个事件流,只有Down的时候才会去寻找谁要处理它,
                // 如果找到了后面的事件直接让它处理,否则后面的事件会直接让父View处理
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 获取当前触摸手指在多点触控中的排序
                    // 这个值可能因为有手指发生Down或Up而发生改变
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    // 标识当前是那一个点的触摸事件
                    // 此时获取到手指的Id,这个值在Down到Up这个过程中是不会改变的
                    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);
                    // 子View总数
                    final int childrenCount = mChildrenCount;
                    // 第一个点的Down事件newTouchTarget肯定为null,后面的点的Down事件就可能有值了
                    // 所以只有第一个点的Down事件时走if中逻辑
                    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.
                        // 将所有子View放到集合中,按照添加顺序排序,但是受到Z轴影响
                        // 只有子View数量大于1,并且其中至少有一个子View的Z轴不为0,它才不为null
                        // 7.0中,View的elevation默认为0
                        final ArrayList<View> preorderedList = buildOrderedChildList();
                        //检查ViewGroup中的子视图是否是按照顺序绘制,其实就是不受z轴影响
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 开始遍历子View啦,从后往前遍历
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            // 如果preorderedList不为空,从preorderedList中取View
                            // 如果preorderedList为空,从mChildren中取View
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(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.
                            // 如果当前已经有View获得焦点了,找到它。后面的触摸事件会优先传给它。
                            // 应该主要影响后面触摸点的Down事件
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                // 找到后i设为最后一个View
                                i = childrenCount - 1;
                            }
                            // 可以看到,对子View集合的遍历是倒序的。这就是为什么覆盖在上层的View总是能优先获取到事件的原因。

                            // 检查View是否显示或者播放动画以及TouchEvent点是否在View内
                            // 如果不满足会继续寻找满足的子View
                            if (!canViewReceivePointerEvents(child)
                                    || !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.
                                // 如果子View已经接收过触摸事件,就给它添加一个新的触摸点
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            // 再次重置View
                            resetCancelNextUpFlag(child);
                            // 将事件传给子View,看子View有没有消费,消费了执行if中逻辑,并结束循环。
                            // 就是说该View之后的子View将都不能接收到这个TouchEvent了
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                // 记录这次TouchEvent按下的时间
                                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]) {
                                            // 记录触摸的View的位置
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    // 记录触摸的View的位置
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                // 创建一个TouchTarget,加入触摸链
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                // 标记已经把事件分发给了newTouchTarget,退出子View遍历
                                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在不是Down事件,或者没有找到处理事件的View时是null
                    // mFirstTouchTarget在Down事件时,如果找到了处理的View就不为null
                    // if中的代码只有在多点触控中才能执行。
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        // 让最近添加的一个View继续处理
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // 父View自己处理事件
                // 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;
                    // 只有down时,并且有View处理了事件才会走if中逻辑
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        // 表示触摸事件已经被子View处理,并且找到了子View,设置处理标记为true
                        handled = true;
                    } else {
                        // 父View拦截,或者child原本不可接收TouchEvent的状态,为true
                        // 如果事件被父View拦截了,或者child原本被打上了暂时不可接收TouchEvent的标记PFLAG_CANCEL_NEXT_UP_EVENT
                        // 给它发送取消事件
                        // 如果父View没有拦截,并且子View原本没有PFLAG_CANCEL_NEXT_UP_EVENT
                        // 给它分发事件
                        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;
    }

根据以上代码和注释,可以得到如下的伪代码:

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

即当一个ViewGroup接收到一个触摸事件,首先判断自己是否拦截。如果自己拦截下来了,就继续判断自己是否处理了触摸事件;如果自己没拦截下来,就把事件分发给子控件,子控件也继续进行同样的操作,不断递归

dispatchTouchEvent(MotionEvent ev)中有一段代码是这样的:

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

结合后面的内容能够知道mFirstTouchTarget一开始为null,如果触摸事件由子元素成功处理时,mFirstTouchTarget就指向目标子元素

这样的话,以上代码就很清楚了。如果是触摸事件不是ACTION_DOWN,而且触摸事件之前被自己拦截下来没交给子元素处理,自己的onInterceptTouchEvent(MotionEvent ev)就不会被再次调用,默认返回true

反之,触摸事件是ACTION_DOWN或者之前交给子元素处理了,自己的onInterceptTouchEvent(MotionEvent ev)就有可能会被调用

为什么是有可能呢?因为disallowIntercept这个值是可以被requestDisallowInterceptTouchEvent(boolean disallowIntercept)改变的。以下是requestDisallowInterceptTouchEvent(boolean disallowIntercept)的方法说明:

Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.
Parameters disallowIntercept
True if the child does not want the parent to intercept touch events.

也就是子元素要求父元素对触摸事件拦不拦截进行设置。但是也有特殊情况:

           // 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.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

以上代码说明每当ACTION_DOWN事件来临时,状态会被重置。所以得在子元素的dispatchTouchEvent(MotionEvent ev)中使用requestDisallowInterceptTouchEvent(boolean disallowIntercept)才能有效。

一般在子元素中处理滑动冲突就可以这么解决,先把事件传递给子元素,拦不拦截由子元素给父元素传达的命令决定。

以上分析可以得到如下结论:
onInterceptTouchEvent(MotionEvent ev)不是每次都会被调用,如果我们想提前处理所有触摸事件,要选择dispatchTouchEvent(MotionEvent ev),当然前提是触摸事件被传递到了当前的ViewGroup

然后继续看

                        final View[] children = mChildren;
                        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);

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

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

以上可以看出ViewGroup对内部的子元素进行遍历,看子元素是否符合事件传递的要求,如果符合就调用子元素的dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits),点进去看发现最终就是调用了子元素的dispatchTouchEvent(MotionEvent ev),如下:

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

倘若子元素dispatchTouchEvent(MotionEvent ev)返回true,那么通过addTouchTarget(View child, int pointerIdBits)给之前提到的mFirstTouchTarget进行赋值,并且跳出循环。

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

但如果最后没有找到符合要求的子元素

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

进入dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        }

就调用View中的dispatchTouchEvent(MotionEvent ev),如下:

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        ……
        boolean result = false;
        ……
        if (onFilterTouchEventForSecurity(event)) {
            //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;
    }

以上可以看出View处理触摸事件时,首先会判断有没有设置OnTouchListenner,如果OnTouchListenner中的onTouch方法返回true,那么onTouchEvent(MotionEvent event)就不会被调用,所以onTouch的优先级高于onTouchEvent(MotionEvent event),这样做的好处是方便在外界进行触摸事件处理

下面分析onTouchEvent(MotionEvent event):

        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照样会消耗触摸事件,尽管它看起来不可用。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    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;
                    ……
            return true;
        }

从以上可以看出,只要View的CLICKABLE,LONG_CLICKABLE和CONTEXT_CLICKABLE中只要有一个为true,onTouchEvent(MotionEvent event)就返回true,无论是不是否DISABLE

还有一点就是接收到ACTION_UP事件时,会调用performClick(),performClick()中会判断是否有OnClickListener,如果有会调用它的onClick()

    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    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;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

再来看setOnClickListener(@Nullable OnClickListener l):

    /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

可以看出,即使View本来是unclickable的,但是设置了OnClickListener ,也会变得clickable,例如TextView。同样的方法还有类似的setOnLongClickListener(@Nullable OnLongClickListener l)。

onFilterTouchEventForSecurity()

dispatchTouchEvent()最前面有一段代码是这样的:

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

其中onFilterTouchEventForSecurity()

    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        // 先检查View有没有设置被遮挡时不处理触摸事件的flag
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                // 再检查受到该事件的窗口是否被其它窗口遮挡
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

这里就是检查了两个条件,看这个事件对于该View来说是不是安全的。

第一个条件 FILTER_TOUCHES_WHEN_OBSCURED可以通过在xml文件中的android:filterTouchesWhenObscured来设置,或者在Java中通过setFilterTouchesWhenObscured()来添加或移除。DecorView默认是没有这个标志位的,而其他View基本上默认都是有的。Android这样设计是为了让我们可以自主的选择要不要过滤不安全事件。如果我们让自己的View不过滤这样的事件,那么在一个事件流进行中,如果突然弹出一个新窗口,我们的View仍然能接收到触摸事件。

第二个条件是在本次触摸事件分发到ViewGroup所在窗口时,判断窗口如果处于被其它窗口遮挡的状态的话,就会给这个MotionEvent加上这个标志位。

cancelAndClearTouchTargets()

ACTION_DOWN的时候会被调用, 清理上一次接收触摸事件的View的状态

private void cancelAndClearTouchTargets(MotionEvent event) {
    // 如果触摸事件目标队列不为空才执行后面的逻辑
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        if (event == null) {
            final long now = SystemClock.uptimeMillis();
            // 自己创建一个ACTION_CANCEL事件
            event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            // 设置事件源类型为触摸屏幕
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            // 标记一下,这是一个合成事件
            syntheticEvent = true;
        }
        // TouchTarget是一个链表结构,保存了事件传递的一系列目标子View
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            // 检查View是否设置了暂时不在接收事件的标志位,如果有清除该标志位
            // 这样该View就能够接收下一次事件了。
            resetCancelNextUpFlag(target.child);
            // 将这个取消事件传给子View
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        // 清空触摸事件目标队列
        clearTouchTargets();
        if (syntheticEvent) {
            // 如果是合成事件,需要回收它
            event.recycle();
        }
    }
}

TouchTarget

这里先讲一下能够接收触摸事件流的子View怎么被记录的。其实就是使用一个TouchTarget去记录,它是一个单链表结构,并且有复用机制,设计的比较巧妙。下面是TouchTarget中的与我们关连最大的两个成员:

// 用来保存能够处理触摸事件的View
public View child;
// 指向下一个TouchTarget
public TouchTarget next;

不难看出,每一个能够接收触摸事件流的子View都对应着一个TouchTarget,一个触摸链表。

resetTouchState()

private void resetTouchState() {
    // 再清除一次事件传递链中的View
    clearTouchTargets();
    // 再次清除View中不接收TouchEvent的标志
    resetCancelNextUpFlag(this);
    // 设置为允许拦截事件
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

这个方法后中就是把触摸链表清空了,然后清除ViewGroup的PFLAG_CANCEL_NEXT_UP_EVENT标志和FLAG_DISALLOW_INTERCEPT标志。

dispatchTransformedTouchEvent()

cancelAndClearTouchTargets()方法中也出现过,当时是用它来给子View分发取消事件。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    // 先记录原本的Action
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        // 可能过来的事件没有ACTION_CANCEL,如果希望取消的话,那么为事件添加取消标志。
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            // 如果没有子View了,调用View中的dispatchTouchEvent
            // 进而调用View的onTouch或者onTouchEvent方法,触发ACTION_CANCEL逻辑
            handled = super.dispatchTouchEvent(event);
        } else {
            // 如果有子View,将这个取消事件传递给子View
            handled = child.dispatchTouchEvent(event);
        }
        // 在设置回原本的Action
        // 此时TouchEvent的行为相当于没变
        event.setAction(oldAction);
        return handled;
    }

    // 获取触摸事件的触摸点id
    final int oldPointerIdBits = event.getPointerIdBits();
    // 看和期望的触摸点是不是一个
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    if (newPointerIdBits == 0) {
        // 表示不是
        return false;
    }
    final MotionEvent transformedEvent;
    // 如果是同一个触摸点,进入if逻辑
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                // 如果不给子传事件,自己在onTouch或onTouchEvent中处理
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                // 根据滚动值计算触摸事件的偏移位置
                event.offsetLocation(offsetX, offsetY);
                // 让子View处理事件
                handled = child.dispatchTouchEvent(event);
                // 恢复TouchEvent坐标到原来位置,避免影响后面的流程
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    ...
    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());
        }
        // 让子View处理事件
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    // Done.
    transformedEvent.recycle();
    return handled;

参考:
你还在被触摸事件困扰吗?看看这篇吧

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值