Android 触摸事件机制ViewGroup中触摸事件详解

概述

ViewGroup继承于View,所以说,ViewGroup中对触摸事件的处理,很多都继承于View。但是,ViewGroup又有自己对触摸事件的特定处理。本文重点介绍的是dispatchTouchEvent();理解ViewGroup的dispatchTouchEvent()接口是理解Android触摸事件传递机制的关机。

ViewGroup中触摸事件的概述

  1. ViewGroup继承于View,它中对触摸事件的处理,很多都继承于View。但是,ViewGroup又有自己对触摸事件的特定处理。
  2. ViewGroup重载了dispatchTouchEvent()接口。
  3. ViewGroup新增了onInterceptTouchEvent()接口。

ViewGroup中触摸事件的源码解析

    public boolean dispatchTouchEvent(MotionEvent ev) {
    // mInputEventConsistencyVerifier是调试用的,用理会
        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);
        }
        // 第1步: 是否要分发该触摸事件
        // onFilterTouchEventForSecurity()表示是否要分发该触摸事件?
        // 如果该View不是位于顶部,并且有设置属性使该View不在顶部时不响应触摸事件,则不分发该触摸事件,即返回false;
        // 否则,则对触摸事件进行分发,即返回true 
        // 注意:onFilterTouchEventForSecurity()用安全机制来过滤触摸事件,true为不过滤分发下去,false则销毁掉该
        // 事件。方法具体实现是去判断是否被其它窗口遮挡住了并且设置了被遮住就不能相应触摸事件,如此就要过滤掉该事件;
        boolean handled = false;// 事件处理的标记
        if (onFilterTouchEventForSecurity(ev)) {
        // action的值包括了触摸事件的类型和触摸事件的索引;
        // Android中在一条16位指令的高8位中储存触摸事件的索引,在低8位中储存触摸事件的类型;
        // 而actionMask仅仅包括事件类型;对于多点触控,我们需要获取到索引信息才能区分不同的触摸点,进行操作
        // action 用16位记录触摸点和事件类型:8-15位表示触摸点,0-7位表示事件类型;
        // MotionEvent.ACTION_MASK 0xff数值上等同于0x00ff两者&可以过滤触摸点,得到低8位的事件类型;
        // action和ACTION_POINTER_INDEX_MASK(0xff00)&可得到触摸点
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            // 第2步: 检测是否需要清空目标和状态
            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();
            }
            // 第3步:检查当前ViewGroup是否想要拦截触摸事件
            // 是的话,设置intercepted为true;否则intercepted为false。
            // Check for interception.
            final boolean intercepted;// 这个变量用于检查是否拦截这个TouchEvent
            // 第一个if中的判断限制了,必须是ACTION_DOWN事件,或者mFirstTouchTarget != null
            // 才会询问onInterceptTouchEvent()要不要拦截事件,否则ViewGroup就直接拦截了。
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 检查是否不允许拦截事件
                // 在第一个if的逻辑中,我们看到它先检查ViewGroup是否有FLAG_DISALLOW_INTERCEPT标志,
                // 如果有就直接不进行拦截,如果没有才会询问onInterceptTouchEvent()要不要拦截.这个
                // 标志在上一步中也出现了,只不过是清理掉它,所以ACTION_DOWN发生时,ViewGroup肯定是没这个标志的。
                // 只有在有子View处理触摸事件流的过程中,有子View调用requestDisallowInterceptTouchEvent(),
                // 可以给ViewGroup添加这个标志位。
                // **知识点**:如果子View在ACTION_DWON时处理了事件,那么后面可以通过
                // requestDisallowInterceptTouchEvent(true)来禁止父View拦截后续事件。

               // 一个纵向滑动的RecyclerView嵌套一个横向滑动的RecyclerView时之所以在触发横向滑
               // 动后,就再不能触发纵向滑动,就是因为RecyclerView在发生横滑时调用了
               // requestDisallowInterceptTouchEvent(true)禁止父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);
            }
            // 第4步:检查当前的触摸事件是否被取消
            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
            // 检查父View是否支持多点触控,即将多个TouchEvent分发给子View,
            // 通过setMotionEventSplittingEnabled()可以修改这个值。
            // FLAG_SPLIT_MOTION_EVENTS在3.0是默认为true的,即支持多点触控的分发。
            // Update list of touch targets for pointer down, if needed.

            // 第5:将触摸事件分发给"当前ViewGroup的子View和子ViewGroup"
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            // 如果触摸"没有被取消",同时也"没有被拦截"的话,则将触摸事件分发给它的子View和子ViewGroup
            // 如果当前ViewGroup的孩子有接受触摸事件的话,则将该孩子添加到mFirstTouchTarget链表中
            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
                // 检查TouchEvent是否可以触发View获取焦点,如果可以,查找本View中有没有获得焦点的子View,
                // 有就获取它,没有就为null.面代码中,还尝试获取了一下ViewGroup中当前获得焦点的View,
                // 这个在后面的判断中会用到。
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                 // 可以执行if里面的情况如下:1.Action_down事件 2.支持多点触控且是Action_pointer_down
                 // 事件,即新的事件流的Down事件 3.需要数遍等外设支持,暂时无意义
                 // 就是说同一个事件流,只有Down的时候才会去寻找谁要处理它,
                 // 如果找到了后面的事件直接让它处理,否则后面的事件会直接让父View处理

                 // 下面这个if判断当前的触摸事件类型是不是Down类型,如果是才会进入if开始真正的遍历
                 // 给子View分发事件。也就是说,一个事件流只有一开始的Down事件才回去遍历分发事件,后面的事件
                 // 将不再通过遍历分发,而是直接发到触摸目标队列的View中去。
                 // 在开始遍历前,还需要初始化一下多点触控的事件信息
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 获取触摸手指在多点触控中的排序
                    // 这个值可能因为有手指发生Down或Up而发生改变
                    // 对于ACTION_DOWN,actionIndex肯定是0
                    // 而getPointerId()是获取的该触摸事件的id,并将该id信息保存到idBitsToAssign中。
                    // 这个触摸事件的id是为多指触摸而添加的;对于单指触摸,getActionIndex()返回的肯定是0;
                    // 而对于多指触摸,第一个手指的id是0,第二个手指的id是1,第三个手指的id是2,...依次类推。
                    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.
                    // 将触摸目标链表中与触摸点id相同的TouchTarget移除
                    removePointersFromTouchTargets(idBitsToAssign);


                    // 获取该ViewGroup包含的View和ViewGroup的数目,然后递归遍历ViewGroup的孩子,
                    // 事件进行分发。
                    // 递归遍历ViewGroup的孩子:是指对于当前ViewGroup的所有孩子,都会逐个遍历,并分发触摸事件;
                    // 对于逐个遍历到的每一个孩子,若该孩子是ViewGroup类型的话,则会递归到调用该孩子的孩子

                    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


                        // 这一步中,获取了触摸事件的坐标,然后初始化了preorderedList和mChildren两个子View
                        // 集合。为什么需要两个呢?肯定是有所区别的。
                        // 通过buildTouchDispatchChildList()构建出来的子View集合有如下特点:
                        // 1. 如果ViewGroup的子View数量不多于一个,为null;
                        // 2. 如果ViewGroup的所有子View的z轴都为0,为null;
                        // 3. 子View的排序和mChildren一样,是按照View添加顺序从前往后排的,但是还会受到
                        //    子Viewz轴的影响。z轴较大的子View会往后排。
                        // mChildren上面说过,就是按照View添加顺序从前往后排的。所以,这两个子View集合的最大
                        // 区别就是preorderedList中z轴较大的子View会往后排。

                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        // 如果preorderedList为空,并且&后
                        final boolean customOrder = preorderedList == null
                                // 检查ViewGroup中的子视图是否是按照顺序绘制,其实就是不受z轴影响
                                && isChildrenDrawingOrderEnabled();    
                        final View[] children = mChildren;
                        // 开始遍历子View 从后往前遍历
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            // 如果preorderedList不为空,从preorderedList中取View
                            // 如果preorderedList为空,从mChildren中取View
                            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.
                            // 如果当前已经有View获得焦点了,找到它。后面的触摸事件会优先传给它。
                            // 应该主要影响后面触摸点的Down事件
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                // // 找到后i设为最后一个View
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            // 可以看到,对子View集合的遍历是倒序的。这就是为什么覆盖在上层的View总是能优先
                            // 获取到事件的原因。通过getAndVerifyPreorderedView()从preorderedList或者mChildren
                            // 中取到一个子View。然后如果ViewGroup中有子View获得了焦点,那么就会去找到这个获得焦点的子View。
                            // 注意,如果找到了会执行i = childrenCount - 1,这意味for中的逻辑执行完就结束了,如果中途有
                            // continue的操作,就直接终止本次循环                          


                            // 检查View是否显示或者播放动画以及TouchEvent点是否在View内
                            // 如果不满足会继续寻找满足的子View
                            //1. 如果上一步找到了获取焦点的View,并且触摸事件又没在焦点View中,退出循环。
                            //2. 如果没有任何的子View包含触摸点,也退出循环。
                            //3. 如果没有View获得焦点,就直到找到包含触摸点的View才会继续往下执行。
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }


                            //检查能够处理这个触点的View是否是已经处理之前的View
                            //即查找child是否存在于mFirstTouchTarget的单链表中。
                            //比如我在action_down的时候食指放在一个的视图上,后面我又放下了我的中指再这个视图上,触发
                            //ACTION_POINTER_DOWN,这样mFirstTouchTarget不为null,且这两个手指touch的视图都是同一个
                            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.
                                // mFirstTouchTarget不为null且找到了接受事件的视图,将触摸点Id复制给新的
                                // TouchTarget对象,跳出遍历
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            // 重置view的状态,能够重新分发事件
                            // 重置child的mPrivateFlags变量中的PFLAG_CANCEL_NEXT_UP_EVENT位
                            resetCancelNextUpFlag(child);
                            // 事件传给子View,看子View有没有消费,消费了执行if中逻辑,并结束循环。
                            // 就是说该View之后的子View将都不能接收到这个TouchEvent了

                            // 如果上面没有break,只有newTouchTarget为null,说明上面我们找的Child View
                            // 和之前的肯定不是同一个,而是是新增的, 比如mFirstTouchTarget为null,或者多点触
                            // 摸的时候,一个手指按在了这个当前视图上,另一个手指按在了另一个视图,
                            // 这时候我们就看此child是否分发该事件。
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                // 如果child能够接受该触摸事件,即child消费或者拦截了该触摸事件的话;
                                // 则调用addTouchTarget()将child添加到mFirstTouchTarget链表的表头,并返回表头对应
                                // 的TouchTarget
                                // 同时还设置alreadyDispatchedToNewTouchTarget为true。
                                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();
                                // 如果原来mFirstTouchTarget为touchTargetA,其中的child为childA,
                                // next为null,这里新child暂定为chlidB,那么addTouchTarget的做法就是先构建
                                // touchTargetB,然后将touchTargetA,添加到touchTargetB的next,并且更新
                                // mFirstTouchTarget为touchTargetB。即:
                                // mFirstTouchTarget=touchTargetB(child)——>touchTargetA
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                // 在有子View消费本次Down事件后,会执行alreadyDispatchedToNewTouchTarget = true,
                                // 标记一下已经有子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();
                    }
                    *再次强调!从第6步到这段逻辑只有DOWN类型的事件才能执行,其它类型的事件是不会走这的!
                    一开始再一次的尝试清除子View的PFLAG_CANCEL_NEXT_UP_EVENT标志。然后在给子View分发事件,
                    如果子View处理了,走if中逻辑,如果没处理,继续寻找下一个满足条件的View,然后看它处不处理。*
                    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;
    }

onFilterTouchEventForSecurity 方法

通过onFilterTouchEventForSecurity(ev)检查要不要分发本次事件,检查通过了才会进行分发,走if中的逻辑。否则就放弃对本次事件的处理。看代码注释,这里就是检查了两个条件,看这个事件对于该View来说是不是安全的。
第一个条件FILTER_TOUCHES_WHEN_OBSCURED可以通过在xml文件中的android:filterTouchesWhenObscured来设置,或者在Java中通过setFilterTouchesWhenObscured()来添加或移除。
DecorView默认是没有这个标志位的,而其他View基本上默认都是有的。Android这样设计是为了让我们可以自主的选择要不要过滤不安全事件。如果我们让自己的View不过滤这样的事件,那么在一个事件流进行中,如果突然弹出一个新窗口,我们的View仍然能接收到触摸事件。
第二个条件是在本次触摸事件分发到ViewGroup所在窗口时,判断窗口如果处于被其它窗口遮挡的状态的话,就会给这个MotionEvent加上这个标志位。
知识点:通过设置或清除FILTER_TOUCHES_WHEN_OBSCURED标志位,可以控制在事件流过程中,突然弹出窗口后,后续事件流是否还能继续处理。

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

cancelAndClearTouchTargets方法

先判断是不是ACTION_DWON事件,如果是的话需要进行一些重置,防止对新的事件流产生影响。

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

cancelAndClearTouchTargets(ev)流程不长,它就是清除上一次触摸事件流中能够接收事件的所有子View的PFLAG_CANCEL_NEXT_UP_EVENT标志,并且模拟了一个ACTION_CANCEL事件分发给它们。

一个View如果有PFLAG_CANCEL_NEXT_UP_EVENT标志,表示它跟ViewGroup解除了绑定,通常会在调用ViewGroup#detachViewFromParent(View)后被添加。一般不会有这个标记的。

给上一次接收事件流的子View发送模拟的ACTION_CANCEL事件,可以重置这些子View的触摸状态。比如取消它们的点击或者长按事件。

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

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

不难看出,每一个能够接收触摸事件流的子View都对应着一个TouchTarget。
后面CoorChice就称这个链表为触摸目标链表了。
mFirstTouchTarget是ViewGroup的成员变量,用来记录当前触摸目标链表的起始对象。
咱们接着看下一个方法。

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标志(这个标志后面在详细说)。到此,我们的ViewGroup及其子View们就准备好迎接新的触摸事件了!

TouchTarget

描述一个接受事件的View和它捕捉到的触摸点,暂称触摸目标,触摸点范围在0-31,也就是32位,最多支持32个触点,能通过1位来
表示当前触摸的点

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        //触摸点的id
        public int pointerIdBits;

        // The next target in the target list
        // 单链中指向的下一个节点的TouchTarget
        public TouchTarget next;

        private TouchTarget() {
        }


        // 构建TouchTarget
        public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
            if (child == null) {
                throw new IllegalArgumentException("child must be non-null");
            }

            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }

        // 回收处理
        public void recycle() {
            if (child == null) {
                throw new IllegalStateException("already recycled once");
            }

            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }

getTouchTarget


    //在mFirstTouchTarget单链中寻找目标TouchTarget 如果找到即返回,没有则反
    //回null
    private TouchTarget getTouchTarget(@NonNull View child) {
    // mFirstTouchTarget如果为null,那么直接返回null,如果能在mFirstTouchTarget单链中的
    // TouchTarget的child找到和传入的child相同的就返回此TouchTarget。
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

addTouchTarget

addTouchTarget 可认为是更新mFirstTouchTarget,举例:如果原来mFirstTouchTarget为touchTargetA,其中的 child为childA,next为null,即:
mFirstTouchTarget=touchTargetA(childA)——>null
这时候调用addTouchTarget,传入child假设为chlidB,那么addTouchTarget的做法就是先构建touchTargetB,然后将mFirstTouchTarget,添加到touchTargetB的next,并且更新mFirstTouchTarget为touchTargetB。即:
mFirstTouchTarget=touchTargetB(childB)—>touchTargetA(childA)—>null


    // 添加指定的子视图构成的TouchTarget,添加到触摸目标列表的开头。假定列表本来不包括此子视图的触摸目标
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        // 构建一个新的触摸目标
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        // 将原有的mFirstTouchTarget添加到此触摸目标其后
        target.next = mFirstTouchTarget;
        // 更新mFirstTouchTarget列表
        mFirstTouchTarget = target;
        // 返回此触摸目标
        return target;
    }

dispatchTransformedTouchEvent

开始先判断所指的是不是同一个触摸点,如果是的话再判断传入的child为不为空,或者传入的child的变换矩阵还是不是单位矩阵。如果满足再看传入的child为不为null,如果为空说明需要让ViewGroup去处理事件,反之将把事件分发给child处理

如果是把事件分发给child的话,接下来会计算事件的偏移量。因为child在ViewGroup中可能会发生位置变化,需要除去这些移动距离,以保证事件到达child的onTouchEvent()中时,能够正确的表示它在child中的相对坐标。就相当于事件也要跟着child的偏移而偏移。

可以看到,在进行事件传递时,会根据本次触摸事件构建出一个临时事件transformedEvent,然后用临时事件去分发。这样做的目的是为了防止事件传递过程中被更改。
所以,这个方法主要就是用来根据参数把一个事件分发到指定View的。

将事件交给坐标系中接收事件的View处理,过滤不必要的触点,如果需要重新组装这个事件,如果没有子View,那么这个事件将会把ViewGroup当做View来处理

说明:dispatchTransformedTouchEvent()会对触摸事件进行重新打包后再分发。
如果它的第三个参数child是null,则会将触摸消息分发给ViewGroup自己,只不过此时是将ViewGroup看作一个View,即调用View的dispatchTouchEvent()进行消息分发。而View的dispatchTouchEvent(),它会触摸事件分发给onTouch(), onTouchEvent()进行处理。 如果它的第三个参数child不是null,则会调用child.dispatchTouchEvent()进行消息分发。而如果这个child是ViewGroup对象的话,它则又会递归的将消息分发给它的孩子。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // 先记录原本的Action
        // 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.
        // 如果强制取消或是取消动作,则给子View或者当前ViewGroup作为View分发取消的的事件
        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的行为相当于没变,但是却把该ViewGroup事件取消掉
            event.setAction(oldAction);
            return handled;
        }
        // 这一段代码实现的功能就是:如果需要分发取消事件,那么就会分发取消事件。如果有目标子View则将取消事件分发给目标子View,
        // 如果没有就分发给ViewGroup自己,走的是View的dispatchTouchEvent(),和普通View一样。

        // Calculate the number of pointers to deliver.
        // 获取触摸事件的触摸点id
        /***
        *  getPointerIdBits是获取触摸点的数量然后循环把各触摸点的id左移1位相或,即
        *  把所有点的id信息保存到idBits中
        *  public final int getPointerIdBits() {
        *     int idBits = 0;
        *     final int pointerCount = nativeGetPointerCount(mNativePtr);
        *     for (int i = 0; i < pointerCount; i++) {
        *        idBits |= 1 << nativeGetPointerId(mNativePtr, i);
        *     }
        *   return idBits;
        * }
        */
        final int oldPointerIdBits = event.getPointerIdBits();
        // 两者&运算,判断desiredPointerIdBits 即此id触摸点是否已经存在其中
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        // 一些特殊情况下,可能会产生没有触摸点的事件,这种事件并不需要分发
        if (newPointerIdBits == 0) {
            // 表示不是
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        // 如果是同一个触摸点,进入if逻辑
        // 如果触摸点数量和信息没有发生改变,直接分发
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    //  如果事件不传给子View,自己在onTouch或onTouchEvent中处理
                    // (如果子view为空,那么当前的ViewGroup作为一个View来分发)
                    handled = super.dispatchTouchEvent(event);
                } else {
                    // 否则交给子View分发
                    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);
        }

        // Perform any necessary transformations and dispatch.
        // 如果触摸点信息不同或者不满足child == null || child.hasIdentityMatrix()条件
        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
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值