/** * 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. */ privatebooleandispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { finalboolean 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. finalintoldAction= 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; }
// Calculate the number of pointers to deliver. finalintoldPointerIdBits= event.getPointerIdBits(); finalintnewPointerIdBits= 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) { returnfalse; }
// 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 (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { finalfloatoffsetX= mScrollX - child.mLeft; finalfloatoffsetY= mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY);
// 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. finalintoldAction= 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; }
// 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 (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { finalfloatoffsetX= mScrollX - child.mLeft; finalfloatoffsetY= mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY);
/** * Resets all touch state in preparation for a new cycle. */ privatevoidresetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
clearTouchTargets里只是清空了mFirstTouchTarget链表而已,这里我们又看到了resetCancelNextUpFlag,看来我们后面必须搞清楚它。这里同时把禁止拦截的 flag 给关闭了,重置了嵌套滑动的状态。
// Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList();
显然,这个方法会获取根据需要(绘制顺序、Z 轴属性等)排列的子 View。接下来我们看看这个方法:
1 2 3 4 5 6 7 8 9 10 11
/** * Provide custom ordering of views in which the touch will be dispatched. * * This is called within a tight loop, so you are not allowed to allocate objects, including * the return array. Instead, you should return a pre-allocated list that will be cleared * after the dispatch is finished. * @hide */ public ArrayList<View> buildTouchDispatchChildList() { return buildOrderedChildList(); }
/** * Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children, * sorted first by Z, then by child drawing order (if applicable). This list must be cleared * after use to avoid leaking child Views. * * Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated * children. */ ArrayList<View> buildOrderedChildList() { finalintchildrenCount= mChildrenCount; if (childrenCount <= 1 || !hasChildWithZ()) returnnull; // ... }
上面的方法的作用就显而易见了,如果子 View 的数量不多于1或者没有一个子 View 有自定义的 Z 轴的话,就直接返回null,这里就有些奇怪了:即使启用了自定义绘制顺序,如果子 View 全部都用默认的 Z 值(0)的话,事件就仍然不会按照自定义的绘制顺序分发了吗?非也非也,事实上,这个buildOrderedChildList得到的排序是综合自定义绘制顺序和 Z 值的,从注释就看的出来,优先按照 Z 值排序,然后按照绘制顺序,那么如果所有 Z 值都为 0,我们来看看这个返回的null会在dispatchTouchEvent里造成什么影响:
1 2 3 4 5 6 7 8 9 10
final ArrayList<View> preorderedList = buildTouchDispatchChildList(); finalbooleancustomOrder= preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (inti= childrenCount - 1; i >= 0; i--) { finalintchildIndex= getAndVerifyPreorderedIndex( childrenCount, i, customOrder); finalViewchild= getAndVerifyPreorderedView( preorderedList, children, childIndex); // ...
我来给读者捋一下逻辑:如果子 View 不止 1 个,而且启用了自定义绘制顺序,而且有子 View 的 Z 值非 0,那么我们会得到综合了 Z 值和绘制顺序的preorderedList,此时上面的customOrder反而是否,为什么呢?因为此时我们已经拿到排好序的列表了,所以上面代码 L6 的childIndex就会等价于i,所以 L8 的child也会等价于preorderedList[i],换句话说,这就变成了依次遍历已经排好序的子 View 了,正是我们想要的结果,实在巧妙!而如果所有子 View 的 Z 值都是 0,那么customOrder也等价于是否启用了自定义绘制顺序,后面的遍历也会顺利地按照自定义绘制顺序获取子 View。
现在我们回到buildOrderedChildList方法,正如注释所说,这个方法准备好 ArrayList 之后,使用插入排序将 Z 值大但绘制顺序相对小的子 View 往前提:
if (mPreSortedChildren == null) { mPreSortedChildren = newArrayList<>(childrenCount); } else { // callers should clear, so clear shouldn't be necessary, but for safety... mPreSortedChildren.clear(); mPreSortedChildren.ensureCapacity(childrenCount); }
finalbooleancustomOrder= isChildrenDrawingOrderEnabled(); for (inti=0; i < childrenCount; i++) { // add next child (in child order) to end of list finalintchildIndex= getAndVerifyPreorderedIndex(childrenCount, i, customOrder); finalViewnextChild= mChildren[childIndex]; finalfloatcurrentZ= nextChild.getZ();
// insert ahead of any Views with greater Z intinsertIndex= i; while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) { insertIndex--; } mPreSortedChildren.add(insertIndex, nextChild); } return mPreSortedChildren;
canViewReceivePointerEvents
这个方法和下一个方法都是用于判断子 View 有没有机会消费触摸事件的,看看这个方法的源码:
1 2 3 4 5 6 7 8
/** * Returns true if a child view can receive pointer events. * @hide */ privatestaticbooleancanViewReceivePointerEvents(@NonNull View child) { return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null; }
/** * Returns true if a child view contains the specified point when transformed * into its coordinate space. * Child must not be null. * @hide */ protectedbooleanisTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) { finalfloat[] point = getTempPoint(); point[0] = x; point[1] = y; transformPointToViewLocal(point, child); finalbooleanisInView= child.pointInView(point[0], point[1]); if (isInView && outLocalPoint != null) { outLocalPoint.set(point[0], point[1]); } return isInView; }
终于到这货了!我们查阅代码,发现它在把一个 flag 复位,这个 flag 叫:PFLAG_CANCEL_NEXT_UP_EVENT,而这个 flag 是 View 里面定义的,直接看名字的话,这个 flag 表示是否要取消掉接下来的EVENT_UP事件,我们看看这个 flag 的注释:
1 2 3 4 5 6
/** * Indicates whether the view is temporarily detached. * * @hide */ staticfinalintPFLAG_CANCEL_NEXT_UP_EVENT=0x04000000;
它说的是这个 View 是否被临时从 View 树中摘掉了,我们接着看,搜索这个 flag 被用到的地方,一共三处,第一处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/** * Performs button-related actions during a touch down event. * * @param event The event. * @return True if the down was consumed. * * @hide */ protectedbooleanperformButtonActionOnTouchDown(MotionEvent event) { if (event.isFromSource(InputDevice.SOURCE_MOUSE) && (event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { showContextMenu(event.getX(), event.getY()); mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT; returntrue; } returnfalse; }
这就很尴尬了……说好的从 View 树摘掉呢???我猜测这个方法是后来才加上的,而前面对 flag 的注释是之前就写上的……从上面的方法看,当我们正在触摸这个 View 的时候,如果用鼠标(对,鼠标)对这个 View 按了右键,那我们得弹出上下文菜单,显然,这个时候松开手指也不能触发按钮的 click 事件啊!于是,我们看到上面把这个 flag 立了起来。
第二个地方:
1 2 3 4 5 6 7 8 9 10
/** * This is called when a container is going to temporarily detach a child, with * {@link ViewGroup#detachViewFromParent(View) ViewGroup.detachViewFromParent}. * It will either be followed by {@link #onFinishTemporaryDetach()} or * {@link #onDetachedFromWindow()} when the container is done. */ publicvoidonStartTemporaryDetach() { removeUnsetPressCallback(); mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT; }
/** * This is a framework-internal mirror of onDetachedFromWindow() that's called * after onDetachedFromWindow(). * * If you override this you *MUST* call super.onDetachedFromWindowInternal()! * The super method should be called at the end of the overridden method to ensure * subclasses are destroyed first * * @hide */ @CallSuper protectedvoidonDetachedFromWindowInternal() { mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT; // ... }
这里我的理解是,移除之后重置一下状态,如果有读者知道其目的,可以在评论区交流。
到这里我们就知道了,这个 flag 在某些情况下会被立起来,目的是取消后续的抬起事件,那么我们在resetTouchState里为什么要复位也可以理解了:开始新一波触摸事件之后,之前如果没有复位的话当然就要复位了,否则这次事件也会被这个 flag 影响的!既然如此,我们顺便看看这个 flag 怎么影响我们的事件分发,回到 ViewGroup,两处用到了resetCancelNextUpFlag的返回值,首先是dispatchTouchEvent的这里:
1 2 3
// Check for cancelation. finalbooleancanceled= resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
没错,如果我们把这个 flag 立起来了,那么我们会得到canceled == true,也就是说,如果发生了前面所说的,按下按钮的时候又点击鼠标右键,那么我们在这里就会判定这个 ViewGroup 需要取消事件,最终我们会分发一个EVENT_CANCEL给这个 ViewGroup。其实,与此同时我们的这个 flag 也会被复位,因此代码里多处调用,只是为了保证意外吧。第二个地方也在dispatchTouchEvent里,不过这次轮到了子 View:
/** * 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. */ publicbooleandispatchTouchEvent(MotionEvent event) { booleanresult=false;
finalintactionMasked= event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); }
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfoli= mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; }
if (!result && onTouchEvent(event)) { result = true; }
// 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; }
我们首先看到,如果是新的触摸按下事件,我们会停下嵌套滑动:
1 2 3 4 5
finalintactionMasked= event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); }
如果我们是拖动滚动条(这些应该是另外一套处理机制了),我们也认为我们已经消费了事件:
1 2 3
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; }
接下来,在 View 启用(enabled)的时候,如果有触摸监听器,我们会触发对应的回调:
1 2 3 4 5 6
ListenerInfoli= mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; }
// 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(); }