美文网首页
View之事件体系源码分析

View之事件体系源码分析

作者: 钦_79f7 | 来源:发表于2019-12-17 12:23 被阅读0次

事件分发总览

Activity -> Window -> DecorView (是一个ViewGroup) —> rootView (setContentView设置的)

源码分析

Activity

当一个点击操作发生时,事件首先传递给当前的Activity,由Activity进行事件分分发。

Activity#dispatchTouchEvent

    /**
     * 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

/**
 * 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.
 */
public abstract class Window {
    ...
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    ...
}

由Window 的注释可知,其仅有一个实现类 PhoneWindow,那么我们就直接在 PhoneWindow中看此方法的具体实现即可。

PhoneWindow#superDispatchTouchEvent

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

那么这个 mDecor 是啥呢?

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
public class DecorView extends FrameLayout {
    ...
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    ...
}

到了这里我们应该都熟悉了,这个 DecorView 是一个继承了 FrameLayout 的ViewGroup,也是我们在 Activity中直接 getDecorView 返回的View。

到了这里我们也看到了事件已经由 Window 分发到 View 了。由于这里DecorView是一个ViewGroup 这里我们先分析ViewGroup 的分发机制,姑且将 View 分为 ViewGroup 与 View (不包含ViewGroup的View)。

ViewGroup

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

这里可以看出,ViewGroup 是否拦截当前事件 取决于 事件类型为 ACTION_DOWN 或者 mFirstTouchTarget !=null。mFirstTouchTarget 在后面的源码我们可以看到逻辑,下面会有解释,这里直接看结论就是: 只有在 ViewGroup的子View成功处理 ACTION_DWON 事件(onTouchEvent 返回 true),mFirstTouchTarget才会被赋值。

  • ViewGroup 一旦拦截Down事件,那么后续的事件序列都会交由它处理,并且不会再次调用 onInterceptTouchEvent 来询问是否拦截

这里有个特殊情况,就是 FLAG_DISALLOW_INTERCEPT 标记位,这个标记位由requestDisallowInterceptTouchEvent来设置,一般是在子View中调用的。作用就是用来干预 ViewGroup的事件分发。一旦设置之后,ViewGroup将无法拦截除了DOWN事件之外的其他点击事件。因为当事件为ACTION_DWON时,ViewGroup会重置这个标记位。如下源码:

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

FLAG_DISALLOW_INTERCEPT 这个标记位会在resetTouchState中被重置。因此,每当面对 DOWN 事件时,ViewGroup都会调用 onInterceptTouchEvent 来询问是否拦截当前DOWN事件。

  • ViewGroup 中 onInterceptTouchEvent 不是每次事件都会被调用,只有 dispatchTouchEvent 才会被每次调用;
  • FLAG_DISALLOW_INTERCEPT 这个标记位,给我们一个处理滑动冲突的契机。

继续分析源码。

当ViewGroup 决定不拦截事件时,事件会向下分发到它的子View中进行处理。

    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        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 的所有子View,然后判断子View 是否处理点击事件。

子View是否能够处理点击事件,主要由两点来衡量:

  • 子元素是否在播放动画
  • 点击事件的坐标是都落在子元素的区域内

如果某个子元素满足这个两个条件,那么事件就会传递给它来处理。通过 dispatchTransformedTouchEvent 方法来调用 子元素的 dispatchTouchEvent 方法。而且通过上述分析,我们知道 这里传入的 child !=null。

    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    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);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        ...
    }

这里我们可以看到,当 child != null 时,直接调用子元素的 dispatchTouchEvent 方法。先不考虑此方法的内部逻辑,如果 dispatchTouchEvent 返回 true,那么 mFirstTouchEvent 就会被赋值同时跳出 for 循环,如下逻辑

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

如果子元素 dispatchTouchEvent 返回 false,那么就是继续遍历,继续分发事件。

这里我们着重关注 mFirstTouchTarget ,因为 mFirstTouchTarget 是否被赋值,将直接影响 ViewGroup 的对事件的拦截策略。

继续上一段,假如在遍历结束后,没有子元素能够处理这个事件,那么就会由ViewGroup 自己来处理,即调用自身的 onTouchEvent 方法。这里暂且先下这么个结论。其实这里提到的遍历结束,事件仍然没有被处理掉,包含了两种情况:

  • ViewGroup 没有子元素
  • 子元素处理了点击事件,但是在dispatchTouchEvent 中返回了 false,而这个false 一般也是由于子元素的 onTouchEvent 返回了 false。
    在这两种情况下,ViewGroup 中的 mFirstTouchTarget 都不会被赋值,那么在执行下面的逻辑时:
    // 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);
    }

mFirstTouchTarget == null 判断成立,因为第三个参数 child == null ,就会直接调用 ViewGroup 自身的onTouchEvent,如下:

            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            }

这里调用了父类也就是 View 的dispatchTouchEvent 方法,而View的dispatchTouchEvent 方法中调用 onTouchEvent 方法。

如此分析下来,在 ViewGroup 事件分发过程中的有两种情况 调用了 View 的事件逻辑:

  • 遍历子元素时,存在能够接收点击事件的子元素时
  • ViewGroup 自身处理点击事件

so,我们进入View 内部来看一下,事件分发的逻辑。

View

首先,我们直接就上 dispatchTouchEvent ,从上述分析中我们知道不管哪种情况,都是首先进入 View 的dispatchTouchEvent 的逻辑。

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...

        return result;
    }

这里可以看到,View 的处理逻辑要比 ViewGroup 的相对简单不少。因为 View 自身没有子元素,所以不需要处理向下传递事件的逻辑。因此,它只能自己处理事件。

这里,首先判断 onTouchListener,如果有设置,会先执行 onTouchListener 的逻辑,根据 onTouchListener 的返回再决定是否执行 onTouchEvent。所以:

  • onTouchListener 的优先级要比 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;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                            // 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();
                                }
                            }
                    break;
                    ...
            }
        }
        ...
        return true;
    }

分析源码,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个是true,那么就会消耗这个事件,即 onTouchEvent 返回 true,不管它是不是处于 DISABLED的状态。
然后就是 如果View设置了 OnClickListener ,那么 performClick 就会调用它的的 onClick 方法。

其实,从上面的源码可以看出,如果设置了 TouchDelagate,会优先执行 TouchDelegate 的逻辑,然后根据其返回值,来决定是否执行下面的逻辑。

相关文章

网友评论

      本文标题:View之事件体系源码分析

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