从源码角度解析TouchEvent的传递机制

从源码角度解析TouchEvent的传递机制

转载请注明出处: 西木的博客

这几天在项目中用到了多层自定义view嵌套的情况,每一层的View都有自己点击、长按或者scroll事件要处理,所以经常需要去解决冲突的处理问题,那么这一期,我们就来讲讲Android中TouchEvent的传递,以及多点触控机制。

1. TouchEvent的传递

从源码角度解析TouchEvent的传递机制_第1张图片

Android程序在运行时,若用户点击某一个view,就会生成一个MotionEvent,然而这个event并不会立即被传递到用户点击的view的监听器中,而是先通过顶层ViewGroup一层一层向下传递的,中间层的ViewGroup,也就是目标 View的每一层祖先,都会先收到该event,而且可以决定是否拦截该event,让其不能进一步向下传递至目标event,最典型的就是ListView,我们在滚动listview中得元素时,这个时候产生的event, 它们的目标view应该是 被触摸到的那一行view,而不是整个listview,但是listview有权拦截这个event,并针对该event做出自己的变化。那么TouchEvent是以什么顺序进行传递,并决定是否拦截的呢?请听我慢慢道来。

1. DispatchTouchEvent

每当touchEvent第一次到达当前view或者viewGroup时,第一个被调用的函数便是DispatchTouchEvent,该函数,顾名思义,就是指如果有touch事件到达当前view,当前view应该以何种机制去处理这个view,是忽略呢,还是传递给子view呢,或者是直接调用当前view上的监听器呢?如果当前view是处于ViewHierarchy叶子层的view,不包含任何子view,那么就不应该传递给子view进行处理,我们来看看View类默认的dispatchTouchEvent是如何处理的:

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

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

我们可以看到,dispatchTouchEvent直接调用了 mOnTouchListener的onTouch进行处理,如果mOnTouchListener结果为false,则会调用onTouchEvent函数。这是在叶子层view分发事件的逻辑,只需简单调用该view上的监听函数而已。那如果当前view 还包括很多子view呢?我们再来看ViewGroup中dispatchTouchEvent的实现:

ViewGroup中dispatchTouchEvent的实现相对复杂,我们忽略掉非关键部分,并拆分成几部分进行介绍

1.Handle initial down event

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

这段代码其实就是初始化一个点击事件相关的属性,一个touch事件总是由Action_down事件开始,以aciton_up事件结束,那viewGroup每次检测到down事件,相当于一个touch事件的开始。

2. Check interception

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

这段代码,顾名思义就是检查是否要拦截(intercept)当前event,在第一次发生down事件时,我们总是执行检查,执行检查我们先要检查一个叫做disallowIntercept的标记是否存在,如果这个标记存在,就设intercepted = false, 即不执行拦截,这个标记的意思就是当前view的某个子view,请求其父view不要拦截touchEvent,大家肯定会想起有个函数就是执行这个功能的: requestDisallowInterceptTouchEvent, 那我们先来讲解这个函数:

2. requestDisallowInterceptTouchEvent

这个函数实现非常简单,我们先贴上源代码:

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

这段代码,首先设置了mGroupFlags标记,disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0),布尔型变量disallowIntercept代表是否拦截事件,但是大家肯定会想,如果我作为父view开启了,不拦截子view的事件,但是如果事件在祖先view上就被拦截了呢?这时候,连父view都收不到事件,更别说去拦截了,因此这个函数最后会递归调用每一层的祖先view,请求其不要拦截事件。

说完了requestDisallowInterceptTouchEvent,让我们回到祖先,继续研究dispatchTouchEvent,如果disallowIntercept为false,就会调用onInterceptTouchEvent尝试进行拦截,这个函数里面会做真正的拦截,决定对何种touchEvent在哪种情况下进行拦截。

3. onInterceptTouchEvent

关于onInterceptTouchEvent,目前只需要知道这几点:

  1. 返回值决定了拦截是否成功,事件是否会传给子view
  2. 只在viewGroup中有,view类中没有,因为单纯的叶子节点view没有子view,没有权利去拦截
  3. 默认返回false,表示不拦截

后面讨论具体的case时,我们再对onInterceptTouchEvent进行更加详细的用法了解。

继续看dispatchTouchEvent函数,接下来的一部分,将会检查cancel标记是否被设置,接着如果cancel和intercept都为false的话,就会尝试去找目标touchTarget,并做真正的dispatch。

对于touchTarget需要注意的是:

  1. MotionEvent中是没有存储view相关信息的,当事件到达当前view时,是view根据坐标和子view的绘制顺序决定传递到哪个子view的
  2. 对于action_down事件,代表一次新的tap事件发生,我们需要定位目标view,但是,后续的action_move和action_up都不需要重新寻找target,而是以上次的事件的target作为目标

dispatchTouchEvent最后会调用dispatchTransformedTouchEvent,将event转换后传递给子view。

4. dispatchTransformedTouchEvent

同样定位到源码:

//......省略一大波代码
if (child == null || child.hasIdentityMatrix()) {
    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;
}
//...省略一小批代码

如果child参数为null,则会调用super.dispatchTouchEvent(event),即调用View类中得dispatchTouchEvent,前面分析过,View类中的分发机制即直接调用当前view上的监听函数。如果child参数不为null,则会将event的坐标进行计算,转换为相对于child原点为参考的坐标值。再调用child的dispatchTouchEvent函数,至此就将event成功的传给了子view。

按照上述流程,一步一步向下查找,我们就能将event传递到用户真正发生点击时位于的view,有一个问题需要考虑的是:如果子view不想处理这个事件呢?该事件该何去何从呢?

3 事件返回

我们先来看一个例子,如下图所示,蓝色部分是一个RelativeLayout, 含有红色方块和橘色方块两个子view,当我们点击了重合部分时,事件传递的顺序是如何的呢?
从源码角度解析TouchEvent的传递机制_第2张图片

假设三个view事件监听器都返回false,即表示不处理该事件,但是在收到事件时打出log:

/Junli﹕ inner orange view ontouch
/Junli﹕ inner red view onTouch
/Junli﹕ wrapper onTouch

可以看到事件会最先传递给最上层的橘色view,然后交给红色view处理,最后蓝色父view自己处理,这个执行顺序看上去合情合理,代码中也可很明显的看出:

for (int i = childrenCount - 1; i >= 0; i--) {

    //子view是否能接受event,以及view是覆盖当前event的坐标
     if (!canViewReceivePointerEvents(child)
           || !isTransformedTouchPointInView(x, y, child, null)) {
        continue;
    }
    //...
    newTouchTarget = getTouchTarget(child);
    //... 传递给子view进行处理
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        //...
        break;
    }

}

//没有找到第一目标,则父view自己处理事件,child=null 调用dispatchTransform...
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.
    //  ....
}

到此,我们大致了上分析了事件被目标view忽略后,往回传递的流程。这一节的讲解就到这里,谢谢大家耐心的阅读。当然,对于事件的处理还有很多方面我们没有cover到,我会在下一期中继续讲解,小伙伴们,不见不散^-^

你可能感兴趣的:(android,TouchEvent)