理解View的事件体系

        通俗的理解:“当用户在屏幕上完成一次点击、滑动等等触摸行为时,都会伴随着一次View事件的触发,所以这些行为可以被认为是View的事件”。

那么这些事件是怎么被区分、标记、传递的呢?这就不得不引入Android View体系中一个重要的概念 - MotionEvent

View 事件体系中几个重要的概念

MotionEvent

        MotionEvent 是由ViewRootImpl包装生成的,用于在整个事件流程中传递信息。MotionEvent会将本次触摸事件所产生的信息包装起来,其中最重要的信息便是本次触摸事件的类型。主要的MotionEvent事件类型如下表所示。

表 2 - 1 常用触摸事件类型

类型备注
ACTION_DOWN0当用户发生“按下”这一行为时触发。
ACTION_MOVE2当用户“按下”后产生有效滑动距离后触发。
ACTION_UP1当用户“抬起手指”时触发。
ACTION_CANCEL3当发生异常事件时触发,事件被中断。

注: 以上并非列举全部类型,仅为常用类型。

TouchSlop

        TouchSlop 是View 中标识的最小有效滑动距离,如果实际的滑动距离小于该单位,则本次滑动是无效的。

TouchTarget

        TouchTarget可以看作是一个映射关系,当某个事件被某个view消耗后,那么该事件序列中的后续所有事件都可以通过TouchTarget “直接” 到达接收事件的view,而无需重新通过遍历子view的方式进行分发,减少开销。

表 2 - 2 TouchTarget 主要属性

属性备注
childviewgroup中被触摸的子veiw
pointerIdBits被target捕获的所有触点的id 组合掩码
next指向下一个 target

事件分发 

        不知道大家有没有好奇过,为什么用户在点击屏幕后,对应被点击的组件能做出反应呢?为什么不是包含该组件的外层组件响应呢?这个就要归功于Android中十分重要的一个机制--事件分发机制。

图 2 -1 view层级示例

          那事件是如何分发的呢?按照图2 - 1 所示的结构来简单分析一下,假设被触发的事件是Down事件,当我们点击btn_A时,Down事件首先被触发,并且传递到view层(指DecorView的下一层:contentView)后,Group_A会第一个接到该事件,然后传递给Group_B,最后由Group_B传递给btn_A,也就是:

图 2 - 2 事件传递示例

        那么整个事件分发机制具体是怎么运行的呢?前文有提到过MotionEvent是由ViewRootImpl包装生成的,所以笔者理解这里就是整个事件分发的源头,接下来我们看下详细一些的流程图。

图 2 - 3 事件整体分发运行流程

        从图2-3可以看出,MotionEvent从ViewRootImpl传递到stage,再从stage传递到了顶级view (DecorView)中,在DecorView中会判断是否有Activity、Dialog等实现了window的回调(大多数情况下,会由activity实现该回调,所以示意流程图中写明了activity,其余情况暂不分析),如果有,则调用并将MotionEvent传递过去;若无,则直接调用父类(ViewGroup)的分发方法进行view层级的分发流程;但是从整体来看,最终都会回归到ViewGroup的分发方法。

        “笔者认为:为什么会有从Activity -> window -> viewgroup 这条链路,主要是为了当所有view均不处理该事件时,可以由Activity进行兜底处理,避免事件流失,这一点可以从源码中看出。”

        事件分发的整体流程的前半部分,不再进行详细分析了,后续主要分析 图 2-3 中的绿色部分,也就是viewGroup的dispatchTouchEvent方法。

ViewGroup的dispatchTouchEvent

ViewGroup中 dispatchTouchEvent方法的分发逻辑流程图

图 2 - 4 ViewGroup dispatchTouchEvent方法流程图

        图 2 - 4 是一个比较复杂的流程图,但是图中的每一步都是必不可少的,为了便于理解,可以将整个流程分为四部分:拦截事件、 遍历子view传递MotionEvent、自己消耗事件和遍历TouchTargets传递MotionEvent。

        为什么会有两个地方去传递MotionEvent?这是因为当Down事件通过遍历子View被一个具体的View消耗后,TouchTarget会记录当前ViewGroup中消耗事件的子View,当后续事件过来时,会通过TouchTarget一层一层的传递给该具体的View,从而减少遍历子View带来的开销,所以理论上两条传递的链路只会走一条。那么这个机制就决定了 当一个View接收并处理了某个事件,那么该事件序列。后续的所有事件在正常情况下均会由该View继续处理。

拦截事件

        该流程中最重要的一个方法就是 onInterceptTouchEvent,但是要走到该方法需要满足一定的条件: (事件为Down事件 或者 TouchTarget不为空) 且 子View允许viewGroup拦截,从这里我们也可以知道,onInterceptTouchEvent方法不是每次都会走到的,所以如果需要添加必做的操作时,只能在dispatchTouchEvent方法中处理。并且onInterceptTouchEvent方法在大部分的ViewGroup中都是默认不拦截的。在开发过程中如果需要拦截,则需要重写ViewGroup的onInterceptTouchEvent方法。

if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // 防止事件在拦截过程中被改变了,进行复原。
    } else {
        intercepted = false;
    }
} else {
    // 如果当前事件不是Down事件 且 当前无TouchTarget,则表示该view继续拦截该事件。
    intercepted = true;
}

        从上述代码中,我们可以看到,不管ViewGroup是否拦截,都不会立即对该事件进行处理,而是将结果赋值给intercepted变量,这个变量将会决定是否进入 “遍历子view传递”流程。

遍历子View传递

        进入到该流程,除了上述的未被拦截条件外,还需要该事件非Cancel事件,并且事件类型需要是ACTION_DOWN、ACTION_POINTER_DOWN和ACTION_HOVER_MOVE中的一种。我们来看下这块的代码实现:

 if (!canceled && !intercepted) {
    ...
    if (actionMasked == MotionEvent.ACTION_DOWN
         || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
         || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        ...
    }
}

          为什么会有第二个if判断? 这里需要结合整体来看才能明白,这里先提前讲一下,主要是因为,当Down事件被子View接收并处理后,后续的Move等事件到来时,不应该再走子View遍历传递这条链路,而是走TouchTarget传递,但是由于事件不是取消事件并且ViewGroup不拦截,那么还是会进入到遍历子View的流程中,所以这里做了一层拦截,使得后续事件能够通过TouchTarget进行传递。

        接下来我们看下遍历子View传递这个过程的具体实现:

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

       ...
        // 如果子View无法接收触摸事件  或者  点击的xy不在子View的范围内,则跳过该子View

      if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
            continue;
      }

     ...

     // 将事件传递给子View,如果子View处理了,dispatchTransformedTouchEvent方法会返回true。
     if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

         ...
         
        // 新创建target,并初始化 
         newTouchTarget = addTouchTarget(child, idBitsToAssign);
        // 标记事件已经分发给子View并处理,后续会用到
         alreadyDispatchedToNewTouchTarget = true;
         break;
    }
}
自身消耗
if (mFirstTouchTarget == null) {
    // 需要注意,这里child传了null  表示ViewGroup自身处理,不需要传递到子View
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else { 
    ...
}

        如果在前面的拦截中ViewGroup拦截了该事件或者子View未处理,那么ViewGroup自身就有可能会处理该事件,为什么说有可能?这里需要分情况分析:

1、 ViewGroup拦截了Down事件:这个时候由于拦截后不再分发给子View,所以mFirstTouchTarget == null,这个时候会直接进入到自己处理的流程。

2、 ViewGroup拦截了Down事件的后续事件,并且Down事件被子View处理了,那么这时mFirstTouchTarget != null,所以这时会进入到else分支,ViewGroup不会处理该事件。但是ViewGroup会处理该事件序列中的后续的事件

3、 ViewGroup未拦截任何事件,但是没有子View处理该事件,那么会走自己处理的流程。

遍历TouchTargets传递

        前面说过,当Down事件被子View消耗后,会生成一个绑定子View和id的TouchTarget,这个时候后续的事件都会通过TouchTarget的引用传递到下一层。我们来看下具体实现:

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;
                // 如果在上述的遍历子View传递过程中,事件已经被消耗,这里会直接返回handled = true
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 查看本次事件是否是取消事件 (包含事件被ViewGroup拦截的情况)
                    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                    // 分发给target中的View
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // 如果是拦截造成的cancel,会将mFirstTouchTarget置为null,如果是单个child被取消,则从mFirstTouchTarget中移除对应节点
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
		}

        这里可以印证上节自身消耗中的第二点,ViewGroup虽然拦截了Down事件后续的某个事件,但是拦截的第一个事件ViewGroup是不会去处理的,而是将其转换为cancel事件传递给子View,然后将mFirstTouchTarget中的touchtarget清除,那么后续的事件序列,将会交由ViewGroup处理。

        认真看了会发现,整个dispatchTouchEvent的出口方法都是dispatchTransformedTouchEvent,那么这个方法的作用是啥呢?它的最大的作用就是将事件传递到下一层(也就是调用子View的dispatchTouchEvent方法),其次就是通过传入的child参数,决定该事件是传递给子View还是自身消耗,以及在ViewGroup拦截事件时,将事件转换为cancel事件并传递给子View也是在该方法中进行的,具体就不对该方法进行分析了,感兴趣可以自行查看源码。

至此,ViewGroup中的dispatchTouchEvent方法基本都分析完成了,咱们继续进入到View的dispatchTouchEvent方法分析,了解View对事件的处理流程是怎么样的。

View的dispatchTouchEvent

        View的dispatchTouchEvent方法不会再拦截了(本身View也不需要拦截了,已经是叶子节点了。),那么在dispatchTouchEvent中会做些什么呢?

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

               ...

        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;
               // 首先如果View实现了onTouchListener接口,那么会触发并调用onTouch方法。
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                // 注意,如果这里处理成功了,会给结果赋值true。
                result = true;
            }
            // 如果上面未处理,才会进入 onTouchEvent方法

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

       ...

        return result;
    }

        从上面可以看出,对一个View设置 onTouchListener 的执行优先级高于 onTouchEvent(onClickListener 和onLongClickListener),并且执行了onTouchListener就不会执行后者

回到最开始,事件分发是如何从Group_A 到btn_A的呢?看图!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值