View触摸事件分发 经过前面的两篇文章 ,我们终于从内核(触摸事件的真正来源)一路经过Native
层通过消息机制来到了需要接收的应用的主线程消息队列中然后被处理,首先调用的是应用根View
(DecorView
)的dispatchPointerEvent()
方法(继承自View
):
1 2 3 4 5 6 7 public final boolean dispatchPointerEvent (MotionEvent event) { if (event.isTouchEvent()) { return dispatchTouchEvent(event); } else { return dispatchGenericMotionEvent(event); } }
调用了ViewGroup
的dispatchTouchEvent()
方法(DecorView
继承自FrameLayout
):
dispatchTouchEvent(ViewGroup) 顾名思义,这个方法就是ViewGroup
的触摸事件分发方法,它重写了父类View
的该方法,View
也有自己的dispatchTouchEvent()
方法(后面再讲)。
这个方法非常长,我们拆开来分析,首先我们要明确一点,由于Android
在系统级别引入了辅助功能选项(AccessibilityFoucs
)来帮助有障碍的用户使用系统,所以如果一个事件带有TargetAccessibilityFocus
标志,说明这是一个特殊的辅助功能事件,需要进行特殊处理(虽然这种情况比较少见)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Override public boolean dispatchTouchEvent (MotionEvent ev) { if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false ); } boolean handled = false ; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } 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); } else { intercepted = false ; } } else { intercepted = true ; } if (intercepted || mFirstTouchTarget != null ) { ev.setTargetAccessibilityFocus(false ); }
这一段就是检测该view
是否应该被拦截,虽然没有看下面的代码,我们可以猜测如果intercepted
标志为true
,那么这个事件就会留在该view
被处理而不会再向其子view
分发。下面是ViewGroup
默认的处理方式:
onInterceptTouchEvent()
1 2 3 4 5 6 7 8 9 public boolean onInterceptTouchEvent (MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true ; } return false ; }
这个方法默认只是对一个特殊情况作了特殊的拦截处理。
dispatchTouchEvent(ViewGroup) 继续向下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 final boolean canceled = resetCancelNextUpFlag(this ) || actionMasked == MotionEvent.ACTION_CANCEL; final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0 ; TouchTarget newTouchTarget = null ; boolean alreadyDispatchedToNewTouchTarget = false ; if (!canceled && !intercepted) { 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(); final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0 ) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren;
这段主要为后面的查找目标view
作准备,我们先建立了preorderedList
列表,我们来看看这个列表的顺序是如何构建的:
构建待遍历的view数组 1 2 3 public ArrayList<View> buildTouchDispatchChildList () { return buildOrderedChildList(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ArrayList<View> buildOrderedChildList () { final int childrenCount = mChildrenCount; if (childrenCount <= 1 || !hasChildWithZ()) return null ; if (mPreSortedChildren == null ) { mPreSortedChildren = new ArrayList<>(childrenCount); } else { mPreSortedChildren.clear(); mPreSortedChildren.ensureCapacity(childrenCount); } final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = 0 ; i < childrenCount; i++) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View nextChild = mChildren[childIndex]; final float currentZ = nextChild.getZ(); int insertIndex = i; while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1 ).getZ() > currentZ) { insertIndex--; } mPreSortedChildren.add(insertIndex, nextChild); } return mPreSortedChildren; }
总的来说,就是如果有自定义绘制顺序,那么按自定义绘制顺序,否则按默认绘制顺序,然后如果view
定义了z
值属性,那么在屏幕最上层的view
应该先接收到触摸事件。
dispatchTouchEvent(ViewGroup) 回到分发方法,继续向下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 for (int i = childrenCount - 1 ; i >= 0 ; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (childWithAccessibilityFocus != child) { continue ; } childWithAccessibilityFocus = null ; i = childrenCount - 1 ; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null )) { ev.setTargetAccessibilityFocus(false ); continue ; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null ) { newTouchTarget.pointerIdBits |= idBitsToAssign; break ; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false , child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null ) { for (int j = 0 ; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break ; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true ; break ; } ev.setTargetAccessibilityFocus(false ); } if (preorderedList != null ) preorderedList.clear(); }
首先明确一点,这段代码整个都是在
1 2 3 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
这个条件下的,也就是说这是一个down
事件,标志着一个新触摸动作的开始(一个触摸动作一般是down
->move
->up
这样的顺序)。
我们在这段代码中找到了目标view
,然后进一步调用dispatchTransformedTouchEvent()
方法继续向下分发,如果该方法返回true
,那么说明下面的子view
处理了该事件,所以我们将该view
保存到touchTarget
链表中,然后保存了一些用于后续判断的事件信息。来看几个这段代码中调用的方法:
canViewReceivePointerEvents() 1 2 3 4 private static boolean canViewReceivePointerEvents (@NonNull View child) { return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null ; }
检查是否可见,或存在动画。
1 2 3 4 5 6 7 8 9 10 11 protected boolean isTransformedTouchPointInView (float x, float y, View child, PointF outLocalPoint) { final float [] point = getTempPoint(); point[0 ] = x; point[1 ] = y; transformPointToViewLocal(point, child); final boolean isInView = child.pointInView(point[0 ], point[1 ]); if (isInView && outLocalPoint != null ) { outLocalPoint.set(point[0 ], point[1 ]); } return isInView; }
1 2 3 4 5 6 7 8 public void transformPointToViewLocal (float [] point, View child) { point[0 ] += mScrollX - child.mLeft; point[1 ] += mScrollY - child.mTop; if (!child.hasIdentityMatrix()) { child.getInverseMatrix().mapPoints(point); } }
将点值从屏幕坐标系转换到view
的坐标系,然后检查是否在view
的区域内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 private boolean dispatchTransformedTouchEvent (MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {final boolean handled;final int oldAction = 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; } final int oldPointerIdBits = event.getPointerIdBits();final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;if (newPointerIdBits == 0 ) { return false ; } final MotionEvent transformedEvent;if (newPointerIdBits == oldPointerIdBits) { 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; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } 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()); } handled = child.dispatchTouchEvent(transformedEvent); } transformedEvent.recycle(); return handled;}
这个方法中对触摸事件设置好正确的偏移后向目标子view
进行分发,如果没有目标,则调用自身的父类,也就是view
的分发方法进行处理。
addTouchTarget() 1 2 3 4 5 6 private TouchTarget addTouchTarget (@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
这里mFirstTouchTartget
是链表头,新增的touchTarget
被插入了表头位置。
dispatchTouchEvent(ViewGroup) 再回到这个主要方法中:
1 2 3 4 5 6 7 8 9 10 11 12 if (newTouchTarget == null && mFirstTouchTarget != null ) { newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null ) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } }
注意11、12行的两个右括号分别对应退出的是
1 if (!canceled && !intercepted) {
与
1 2 3 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
这意味着我们已经处理完了没有被取消、当前ViewGroup
拦截,并且为初始触摸事件(Down
) 的情况的分发,但是要注意的是现在并没有退出函数,还要继续向下执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 if (mFirstTouchTarget == null ) { handled = dispatchTransformedTouchEvent(ev, canceled, null , TouchTarget.ALL_POINTER_IDS); } else { 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; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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;
dispatchTouchEvent(ViewGroup)小结 现在我们再从头梳理一遍这个比较长的方法过程:
关键点:
TouchTarget
链表保存了处理了初始触摸事件的子View
,注意只有一系列触摸动作的初始事件(Down
事件)才会找到对应的子View
并生成TouchTarget
的一个节点。后面的系列事件都会分发给TouchTarget
链表中保存的子View
,这也就意味着,如果一个子View
没有处理初始的Down
事件,那么它也就不会再接收到后面的move
up
等事件。
如果onInterceptTouchEvent()
返回true
,当前ViewGroup
拦截了该事件,那么该事件不会再向下面分发,并且会向TouchTarget
中保存的所有子View
发送cancel
事件提醒它们这一系列的事件已经因被拦截而取消了,同时还会移除分发记录,意味着后面的事件也不再会分发到子View
。
如果是辅助功能的事件,那么会优先分发给支持辅助功能的View
,如果不存在这样的view
,则进行一般的事件分发。
顺序(大致):
判断是否被拦截
如果未被拦截且为初始事件,找到可以处理事件的子View
(在点击范围内且可被点击),分发事件后如果该子View
处理了事件(dispatchTouchEvent()
方法返回true
)则存入TouchTarget
链表并停止子View
的遍历(后面的子View
就没有机会再收到事件),如果该子View
没有处理该事件,则继续遍历寻找
如果事件被拦截,向TouchTarget
中的子View
发送cancel
事件
将未被2、3情况处理的事件分发给TouchTarget
中的子View
,如果TouchTarget
为空,则交给ViewGroup
本身父View
的dispatchTouchEvent()
方法处理
dispatchTouchEvent(View) 现在我们知道,当一个触摸事件分发到一个非ViewGroup
的View
或者ViewGroup
不再向下分发该事件(没有处理事件的目标或者被本身拦截),那么View
类的dispatchTouchEvent()
将会被调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public boolean dispatchTouchEvent (MotionEvent event) { if (event.isTargetAccessibilityFocus()) { if (!isAccessibilityFocusedViewOrHost()) { return false ; } event.setTargetAccessibilityFocus(false ); } boolean result = false ; if (mInputEventConsistencyVerifier != null ) { mInputEventConsistencyVerifier.onTouchEvent(event, 0 ); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { stopNestedScroll(); } if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true ; } 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 ); } if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
我们可以看到,View
的onDispatchTouchEvent()
方法主要是先检查是否注册了onTouchListener
,如果注册了监听并且调用返回了true
消耗了该事件,那么说明该View
处理了该事件,也会收到后续的事件,如果没有注册监听或者没有消耗,就调用View
本身的onTouchEvent
方法,如果返回true
则消耗事件。
下面来看View
默认的onTouchEvent()
方法:
onTouchEvent() 这个方法我们也拆开来看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public boolean onTouchEvent (MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0 ) { setPressed(false ); } return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } if (mTouchDelegate != null ) { if (mTouchDelegate.onTouchEvent(event)) { return true ; } }
处理了view
被禁用和设置了触摸事件代理的情况。
1 2 3 4 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) {
注意下面的语句都是在该view
可被点击的情况下执行的,并且一旦该判断成立,那么最终一定会返回true
,也就是说,设置了可被点击的view
在默认情况下一定会消耗触摸事件。
下面对不同的触摸事件类型分别作出处理,为了分析方便,我调换了各case
的顺序:
case MotionEvent.ACTION_DOWN: 一个触摸动作的开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 mHasPerformedLongPress = false ; if (performButtonActionOnTouchDown(event)) { break ; } boolean isInScrollingContainer = isInScrollingContainer();if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null ) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { setPressed(true , x, y); checkForLongClick(0 , x, y); } break ;
1 2 3 4 5 6 7 8 9 protected boolean performButtonActionOnTouchDown (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; return true ; } return false ; }
只是处理了事件来源是鼠标的特殊情况。
CheckForTap() 它是一个Runnable
,用于延迟执行单击检测的任务:
1 2 3 4 5 6 7 8 9 10 11 private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run () { mPrivateFlags &= ~PFLAG_PREPRESSED; setPressed(true , x, y); checkForLongClick(ViewConfiguration.getTapTimeout(), x, y); } }
它被放到消息队列,在设置的超时之后被执行,如果这段时间它没有被移出队列,那么说明这就是一个单击事件,那么就显示触摸反馈并开始长按检测。
postDelayed() 1 2 3 4 5 6 7 8 9 public boolean postDelayed (Runnable action, long delayMillis) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null ) { return attachInfo.mHandler.postDelayed(action, delayMillis); } getRunQueue().postDelayed(action, delayMillis); return true ; }
往注册的Handler
的消息队列或者他自己实现的一个消息队列中发送需要被延时执行的消息,这块就不深入探究了,消息机制分析的文章已经讲得很清楚了。
checkForLongClick() 1 2 3 4 5 6 7 8 9 10 11 if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false ; if (mPendingCheckForLongPress == null ) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); }
同样是利用postDelayed()
方法来检测是否到达了检测时间,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; private float mX; private float mY; @Override public void run () { if (isPressed() && (mParent != null ) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true ; } } } public void setAnchor (float x, float y) { mX = x; mY = y; } public void rememberWindowAttachCount () { mOriginalWindowAttachCount = mWindowAttachCount; } }
如果run()
方法被执行,说明到达了设定的时间并且没有因为触摸点移动或者抬起而移除该Runnable
信息,为一个长按动作,执行performLongClick()
方法来触发长按回调:
1 2 3 4 5 6 7 8 public boolean performLongClick (float x, float y) { mLongClickX = x; mLongClickY = y; final boolean handled = performLongClick(); mLongClickX = Float.NaN; mLongClickY = Float.NaN; return handled; }
1 2 3 public boolean performLongClick () { return performLongClickInternal(mLongClickX, mLongClickY); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private boolean performLongClickInternal (float x, float y) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false ; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null ) { handled = li.mOnLongClickListener.onLongClick(View.this ); } if (!handled) { final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y); handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
第5-8行是与之前onTouchEvent()
方法中类似的关于是否注册了监听器的判断,如果注册了监听器,那么优先使用监听器的onLongClick()
方法来处理长近事件,如果没有监听器成功处理事件,那么会先判断长按是否有锚点,再根据锚点的存在性调用showContextMenu()
显示可能存在的上下文菜单。
13行判断如果当前方法成功消耗了长按事件,调用performHapticFeedback()
方法显示一个触觉的反馈。
case MotionEvent.ACTION_MOVE: 触摸点发生了移动
1 2 3 4 5 6 7 8 9 10 11 drawableHotspotChanged(x, y); if (!pointInView(x, y, mTouchSlop)) { removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0 ) { removeLongPressCallback(); setPressed(false ); } } break ;
第1行调用了drawableHotspotChanged()
通知可能存在的子View
或drawable
触摸点发生了移动。
第3行检测由于移动,触摸点是否移出了view
+slop
扩展出的范围,slop
的存在是为了保证在按下后轻微移出点击区域的情况下能正常判断点击:
1 2 3 4 public boolean pointInView (float localX, float localY, float slop) { return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) && localY < ((mBottom - mTop) + slop); }
如果移出了这个范围,首先第4行调用removeTapCall()
:
1 2 3 4 5 6 private void removeTapCallback () { if (mPendingCheckForTap != null ) { mPrivateFlags &= ~PFLAG_PREPRESSED; removeCallbacks(mPendingCheckForTap); } }
先取消了预按下状态的flag
,再调用removeCallbacks
:
1 2 3 4 5 6 7 8 9 10 11 12 public boolean removeCallbacks (Runnable action) { if (action != null ) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null ) { attachInfo.mHandler.removeCallbacks(action); attachInfo.mViewRootImpl.mChoreographer.removeCallbacks( Choreographer.CALLBACK_ANIMATION, action, null ); } getRunQueue().removeCallbacks(action); } return true ; }
从消息队列中移出我们检测单击事件的消息,这样,由于触摸点移动出了当前view
,如果在滚动容器中的情况下,长按的检测就不会进行(因mPendingCheckForTap
消息被移出消息队列)。
1 2 3 4 5 if ((mPrivateFlags & PFLAG_PRESSED) != 0 ) { removeLongPressCallback(); setPressed(false ); }
如果pressed
标志位为1,那么就取消消息队列中长按触发消息,同时去除pressed
标志位。
总结一下,只要触摸点移动出了当前view
,那么所有的点击、长按事件都不会触发,但是只要移动还在view
+slot
范围内,那么点击长按事件还是会被触发的。
case MotionEvent.ACTION_UP: 抬起手指
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0 ;if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { boolean focusTaken = false ; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { setPressed(true , x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { removeLongPressCallback(); if (!focusTaken) { if (mPerformClick == null ) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null ) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false ; break ;
可以看到up
时,才是单击事件真正触发的地方,如果这个view
可以获得焦点,那么会优先处理焦点获取,而不会触发点击事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean performClick () { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null ) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this ); result = true ; } else { result = false ; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
相似的方法,检测了是否有监听的存在并执行,最后给辅助功能选项发送一条消息。
case MotionEvent.ACTION_CANCEL: 取消这一系列触摸动作
1 2 3 4 5 6 7 8 case MotionEvent.ACTION_CANCEL:setPressed(false ); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false ; mHasPerformedLongPress = false ; mIgnoreNextUpEvent = false ; break ;
清除所有的状态。
小结 View
默认的onTouchEvent()
方法处理了一系列的触摸事件, 判断是否触发单击、长按等,并且提供了默认的按下、点击、长按的视觉反馈。