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

来源:互联网 发布:淘宝返利哪里取消? 编辑:程序博客网 时间:2024/05/20 19:46

当一个触摸事件发生的时候,事件首先传递给当前的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中有。

首先贴上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)) {            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.                cancelAndClearTouchTargets(ev);                resetTouchState();            }            // 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;            }            // 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) {                // 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 = buildOrderedChildList();                        final boolean customOrder = preorderedList == null                                && isChildrenDrawingOrderEnabled();                        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;                            }                            // 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();                    }                    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.                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;    }

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

    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 disallowInterceptTrue 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)。

0 0