Android事件分发机制深度分析(三)

ViewGroup事件分发的源码解析

虽然ViewGroup是继承自View,但ViewGroup和View的事件分发的处理还是不一样的,所以这里分开来讲。
当点击事件到达ViewGroup时,会调用ViewGroup的dispatchTouchEvent方法。而dispatchTouchEvent方法中又会调用onInterceptTouchEvent方法,这时会出现下面两种情况:

如果该ViewGroup拦截该事件,则onInterceptTouchEvent方法返回true,该事件将由该ViewGroup处理,如果ViewGroup的mOnTouchListener被设置,则mOnTouchListener回调中的onTouch方法会被调用,否则onTouchEvent会被调用。也就是说,如果都提供的话,mOnTouchListener会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用。

如果该ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。到此为止,事件已经从顶级View传递给了下一层View, 接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。

可以看出ViewGroup的事件分发最先从dispatchTouchEvent()方法开始,同时这个方法也是ViewGroup事件分发过程中最主要的方法,方法比较长,我们分段进行解读。dispatchTouchEvent()方法开头便调用了onFilterTouchEventForSecurity()方法,这个方法主要是对触摸事件进行过滤,用于组件安全方面的考虑,比如View在一些特定的条件下需要屏蔽器触摸事件。接下来会监测点击事件如果是ACTION_DOWN时,则会清除所有以前的状态。

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

resetTouchState()方法中继续调用clearTouchTargets方法。其中我们主要关注对mGroupFlags中FLAG_DISALLOW_INTERCEPT标志的重置,以及设置mFirstTouchTarget为null,在后面分析时会用到这两个对象。

    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }

继续向下,如果当前的点击事件是ACTION_DOWN或者mFirstTouchTarget不为null时执行if代码块,点击事件是ACTION_DOWN这个条件好理解,那么mFirstTouchTarget什么时候不为null呢?这个从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处理时, mFirstTouchTarget会被赋值并指向子元素 ,也就是说当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null。 反 过 来 ,一旦事件由当前ViewGroup拦截时 ,mFirstTouchTarget != null就不成立。那么当 ACTION_MOVE 和 ACTION_UP 事件到来时,由于(actionMasked == MotionEvent. ACTIQN_DOWN || mFirstTouchTarget != null)这个条件为 false,执行else代码块:intercepted = true,这将导致ViewGroup的onlnterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给ViewGroup处理。

代码中还有一个比较关键的标志位FLAG_DISALLOW_INTERCEPT,可以看出当事件由子View处理时,该标志位才有意义。这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT—旦设置后,ViewGroup将不再拦截除了ACTION_DOWN以外的其他点击事件。为什么ACTION_DOWN点击事件无法被拦截呢?这是因为在dispatchTouchEvent()方法开头便针对ACTION_DOWN点击事件会清除所有以前的状态,其中就含有对FLAG_DISALLOW_INTERCEPT标志的重置。所以导致子View中设置的FLAG_DISALLOW_INTERCEPT标志位失效。总结一下:FLAG_DISALLOW_INTERCEPT这个标志的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件。这点很重要,尤其在处理滑动冲突时特别有用。

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

从上面的代码中还可以看出,onInterceptTouchEvent()方法并不是每次点击事件都会被调用,如果我们想提前处理所有的点
击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的ViewGroup。

接下来如果事件没有被取消或者被拦截的话,就会进行执行如下代码。代码虽然有些长,但我加了注释,应该都很好理解。

			//事件没有取消以及没有被拦截 才会执行以下代码块
            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.清理这个指针id之前所有的触摸目标,以防它们不同步。
                    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.首先找到一个可以接收事件的子节点列表。然后从最上层到最底层扫描子View。
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();//一般preorderedList不为null 所以customOrder一般为false
                        final View[] children = mChildren;
						//倒序遍历,从最上层的View开始
                        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; //将i重置,重新进行遍历
                            }

							//这个子视图不可以接收指针事件,这个子视图在转换后不包含指定的点
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

							//根据子View在单链表中找到对应的TouchTarget
                            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.
								//如果子View已经在它的范围内有了触摸指针。除了要处理的已经存在的触摸指针外,还要给它增加一个新触摸指针。
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
							//把事件分发给子View
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.子View想要在它的范围内接受触摸
                                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();
								//将已经消费了触摸事件的子View 以及触摸id标志位 添加到单链表的开头
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);//将指定子View的触摸指针Id添加到TouchTarget列表的开头
                                alreadyDispatchedToNewTouchTarget = true; //已经分配新的 TouchTarget 
                                break; //子View已经消费本次事件,退出遍历
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
							// 这个子View没有处理事件,因此清除标志并向其他子节点进行正常分发。
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();//将子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指向最近添加的目标。
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

在上面代码的第15行会对点击事件进行过滤,只有当ACTION_DOWN,ACTION_POINTER_DOWN,ACTION_HOVER_MOVE时才会执行if代码块。

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

以我们最常用的ACTION_DOWN点击事件来分析这段代码的用意。很明显,当发生ACTION_DOWN点击事件时才会进入到if代码块中,而代码块中主要做了如下事情:首先根据点击事件获取点击坐标,进而根据坐标找到一个可以接收事件的子节点列表,然后对这个子节点列表进行倒序遍历,即从最上层的子View开始,然后会遍历判断每个子View是否可以接收事件,以及点击坐标是否落在子View区域内,如果有子View均满足这两个条件,那么事件就会传递给它来处理,即调用dispatchTransformedTouchEvent()方法,而dispatchTransformedTouchEvent()方法的伪代码如下,由于已经找到满足条件的子View,所以传入的child不为null,最终执行子View的dispatchTouchEvent()方法,子View中的dispatchTouchEvent方法我们稍后会分析。至此ViewGroup成功将点击事件交由子View处理,从而完成了一轮事件分发。

      private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
              ...
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
          ...
           return handled;
      }

child.dispatchTouchEvent方法的返回值表示该事件有没有被消费,如果子View消费了这次事件,则会返回true,如果没有消费则会返回false。
假设当子View已经消费了事件,返回了true,那么dispatchTransformedTouchEvent()返回true。然后会执行下面代码:

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    // Child wants to receive touch within its bounds.子View想要在它的范围内接受触摸
    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();
    //将已经消费了触摸事件的子View 以及触摸id标志位 添加到单链表的开头
    newTouchTarget = addTouchTarget(child, idBitsToAssign);//将指定子View的触摸指针Id添加到TouchTarget列表的开头
    alreadyDispatchedToNewTouchTarget = true; //已经分配新的 TouchTarget 
    break; //子View已经消费本次事件,退出遍历
}

其中我们重点分析下addTouchTarget()方法,从下面的addTouchTarget方法的内部结构可以看出, mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么 ViewGroup就默认拦截接下来同—序列中所有的点击事件,这一点在前面已经做了分析。而这里mFirstTouchTarget被赋值,并指向最新添加的TouchTarget。

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

addTouchTarget()方法执行完成后会对newTouchTarget进行赋值,此时newTouchTarget与mFirstTouchTarget是相同的,都是指向最新添加的TouchTarget。
已经找到了子View来消费本次事件,所以用break跳出for循环,结束遍历。

而如果遍历所有的子View后事件都没有被合适地处理,这包含两种情况:第一种是ViewGroup没有子View,第二种是子View处理了点击事件,但是在dispatchTouchEvent中返回了 false, 这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件:

// Dispatch to touch targets.如果没有子View去消费这个触摸事件
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    //没有触摸目标所以当前是个普通的子View
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                            TouchTarget.ALL_POINTER_IDS);
}

这里dispatchTransformedTouchEvent()第三个参数child为null, 从前面的分析可以知道,它会调用super.dispatchTouchEvent(event),很显然,这里就转到了View的dispatchTouchEvent 方法,即点击事件开始交由View來处理。至于View中的处理流程稍后会分析。

如果已经有子View来处理事件了,即mFirstTouchTarget不为null,那么就会执行如下代码块,代码已经备注的很详细了,这里也就不在做详细的分析了。

// 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;
//遍历整个TouchTarget链表
while (target != null) {
    final TouchTarget next = target.next;
    //当ACTION_DOWN事件被子View消费时会满足该if条件
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
        handled = true;
    } else {
        //处理其它点击事件,如ACTION_MOVE,ACTION_UP等

        final boolean cancelChild = resetCancelNextUpFlag(target.child)
            || intercepted;//当前事件是否被拦截或者子View设置了清除弹起标志PFLAG_CANCEL_NEXT_UP_EVENT
        //把事件分发给子View
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                          target.child, target.pointerIdBits)) {
            handled = true;
        }

        //如果满足清除条件,则会将当前的TouchTarget从TouchTarget链表中移除。
        if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();
            target = next;
            continue;
        }
    }
    predecessor = target;
    target = next;
}

到这里ViewGroup的dispatchTouchEvent()方法已经分析的差不多了,由于只是对事件分发机制进行分析,所以忽略了代码中的一些细节部分。最后对dispatchTouchEvent方法总结成一张流程图以便理解和记忆。
在这里插入图片描述

Android事件分发机制深度分析(一)
Android事件分发机制深度分析(二)

Android事件分发机制深度分析(四)

发布了87 篇原创文章 · 获赞 28 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/MakerCloud/article/details/87835441