Android 触摸反馈机制源码解析
前言
“定义你的术语……,否则我们将永远无法相互理解。”这是来自作家伏尔泰的忠告。Android 中有关输入事件的表述中,常见以下术语:触摸事件、轻触手势、输入事件、运动事件、触觉反馈、手势处理、触摸反馈等,这就不可避免带来理解上的混乱。在本篇博客的语境下,只使用触摸事件、触摸反馈两个术语,且它们有如下定义:
触摸事件:指 MotionEvent 对象,中文通常称为“运动事件”或“触摸事件”。它表示用户与屏幕交互时产生的事件,例如按下、移动、抬起等操作。
触摸反馈:或叫触摸反馈机制,指从用户手指触摸屏幕开始到最后一根手指离开屏幕这个周期内对用户交互行为的响应,涉及事件的分发、拦截和处理。(*这不是最准确的定义,但这是我能想到的最准确的定义)
开发环境
Android Studio Arctic Fox|2020.3.1 Android 33事件分发和拦截
Android 的触摸反馈机制涉及事件的分发、拦截和处理,核心由三个方法构成:dispatchTouchEvent、onInterceptTouchEvent(仅 ViewGroup)和onTouchEvent。
| 方法 | 所属对象 | 作用 |
|---|---|---|
| dispatchTouchEvent | Activity/ViewGroup/View | 事件分发入口,决定是否向下传递或自身处理。 |
| onInterceptTouchEvent | ViewGroup | 仅 ViewGroup 拥有,返回 true 拦截事件,转交自身 onTouchEvent 处理。 |
| onTouchEvent | Activity/ViewGroup/View | 最终处理事件,返回 true表 示消费事件,否则向上传递。 |
Android 的视图结构是树形层级结构,事件传递遵循以下流程:
- 向下分发:Activity → Window → DecorView → ViewGroup → View
- 向上回溯:如果事件未被消费,则逐层回传到Activity。
其核心思想是“事件优先由子视图处理,如果子视图不处理,则向上回传到父视图,最终由 Activity 处理”

事件处理
触摸事件(MotionEvent)作为用户与移动设备交互的核心载体,其处理机制直接影响着用户体验的流畅度与精确性。从简单的单击操作到复杂的多指手势,从视觉反馈到物理交互,构建完善的触摸响应体系需要系统化思考以下关键问题:
- 如何基于 action_down、action_move 和 action_up 实现点击和长按操作?
- 如何处理多点触控手势?
- 如何扩大 View 的触摸响应范围?
- 如何添加 Tooltip 提示和 3D Touch(重压)事件?
- 如何避免与侧边全局手势区域的冲突?
- 如何处理内嵌在滚动列表时的理滑动冲突?
- 如何使 Ripple(水波纹)生效?
- 倘若事件来自鼠标、VR 设备、遥控器或语音输入,又当如何处理?
Android 触摸反馈框架层的设计者如同深谙武学至理的武林高手,纵使四面八方暗器袭来,一柄长剑即可防的水泄不通。View 的源码便是一部武学典籍,隐藏着构建优雅交互系统的无上心决。通过源码逆向推演设计者原始意图的修炼,终将内化为开发者应对复杂场景的直觉与底气。
View 的核心方法
class View { /** * 将触摸屏的运动事件传递给目标视图,或者如果当前视图是目标,则传递给当前视图。 * * @param event 要分发的运动事件。 * @return 如果事件被视图处理,则返回 true,否则返回 false。 */ 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; // 如果处理了滚动条拖动,则标记事件为已处理。 } // 检查是否设置了触摸监听器,并调用其 onTouch 方法。 ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; // 如果监听器处理了事件,则标记事件为已处理。 } // 如果事件未被处理,则调用当前视图的 onTouchEvent 方法。 if (!result && onTouchEvent(event)) { result = true; // 如果 onTouchEvent 处理了事件,则标记事件为已处理。 } } // 如果事件未被处理,并且启用了输入事件一致性验证器,则记录未处理的事件。 if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // 如果事件是手势结束(如抬起或取消),或者尝试了 ACTION_DOWN 但未处理后续手势,则清理嵌套滚动状态。 if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; // 返回事件处理结果 } /** * 处理触摸屏的 MotionEvent 事件。 * <p> * 如果要通过此方法检测点击动作,建议通过实现并调用{@link #performClick()}来执行操作, * 这将确保系统行为的一致性,包括: * <ul> * <li>遵循点击音效偏好设置</li> * <li>分发 OnClickListener 回调</li> * <li>在启用无障碍功能时处理 {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK}</li> * </ul> * * @param event 触摸事件对象 * @return 如果事件被处理返回 true,否则返回 false */ public boolean onTouchEvent(MotionEvent event) { // 获取触摸事件的坐标 final float x = event.getX(); final float y = event.getY(); // 获取视图的标志位,用于判断视图的属性(如是否可点击、是否启用等) final int viewFlags = mViewFlags; // 获取触摸事件的动作类型(如按下、抬起、移动等) final int action = event.getAction(); // 判断视图是否可点击(包括普通点击、长按点击和上下文点击) final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 如果视图被禁用且不允许在禁用状态下点击,则直接返回 if ((viewFlags & ENABLED_MASK) == DISABLED && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) { // 如果是抬起事件且视图处于按下状态,取消按下状态 if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // 清除手指按下标志 mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // 禁用的可点击视图仍然会消耗触摸事件,只是不会响应 return clickable; } // 如果设置了触摸代理(TouchDelegate),先让代理处理触摸事件 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; // 如果代理处理了事件,直接返回 true } } // 如果视图可点击或有工具提示功能,则处理触摸事件 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: // 处理抬起事件 // 清除手指按下标志 mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { // 如果有工具提示功能,处理工具提示的抬起事件 handleTooltipUp(); } if (!clickable) { // 如果不可点击,移除所有相关回调 removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } // 判断是否处于预按下或按下状态 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) { // 使用 Runnable 延迟执行点击操作,以便先更新视图状态 if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClickInternal(); } } } // 延迟取消按下状态 if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // 如果延迟失败,立即取消按下状态 mUnsetPressedState.run(); } // 移除点击检查回调 removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: // 处理按下事件 // 如果是触摸屏事件,设置手指按下标志 if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { // 如果不可点击,检查是否是长按操作 checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); break; } // 如果按下时执行了按钮操作,则直接返回 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( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } break; case MotionEvent.ACTION_CANCEL: // 处理取消事件 if (clickable) { // 如果可点击,取消按下状态 setPressed(false); } // 移除所有相关回调 removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: // 处理移动事件 if (clickable) { // 更新热点区域的位置 drawableHotspotChanged(x, y); } // 获取事件的分类(如是否是模糊手势或深按手势) final int motionClassification = event.getClassification(); final boolean ambiguousGesture = motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; int touchSlop = mTouchSlop; // 获取触摸斜率(用于判断是否移动超出范围) // 如果是模糊手势且有长按回调,则扩展长按的延迟时间 if (ambiguousGesture && hasPendingLongPressCallback()) { if (!pointInView(x, y, touchSlop)) { // 如果移动超出视图范围,取消长按回调并重新设置延迟 removeLongPressCallback(); long delay = (long) (ViewConfiguration.getLongPressTimeout() * mAmbiguousGestureMultiplier); // 减去已经过去的时间 delay -= event.getEventTime() - event.getDownTime(); checkForLongClick( delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } touchSlop *= mAmbiguousGestureMultiplier; // 扩展触摸斜率 } // 如果移动超出视图范围,取消按下状态和相关回调 if (!pointInView(x, y, touchSlop)) { removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } // 如果是深按手势且有长按回调,则立即处理长按操作 final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS; if (deepPress && hasPendingLongPressCallback()) { removeLongPressCallback(); checkForLongClick( 0 /* 立即发送 */, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); } break; } // 如果视图可点击或有工具提示功能,返回 true 表示消耗事件 return true; } // 如果视图不可点击且无工具提示功能,返回 false 表示不消耗事件 return false; }}ViewGroup 的核心方法
class ViewGroup { @Override public boolean dispatchTouchEvent(MotionEvent ev) { // 输入事件一致性验证器,用于调试和验证事件流正确性 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // 处理无障碍焦点相关逻辑:如果事件目标是当前获得无障碍焦点的视图,则重置标记 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; // 处理初始的按下事件(ACTION_DOWN) if (actionMasked == MotionEvent.ACTION_DOWN) { // 清除所有之前的触摸目标和状态,开始新的手势 cancelAndClearTouchTargets(ev); resetTouchState(); } // 检查是否需要拦截事件 final boolean intercepted; // 仅在DOWN事件或已有处理目标时判断拦截 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); // 调用拦截方法 ev.setAction(action); // 恢复可能被修改的action } else { intercepted = false; // 子View通过requestDisallowIntercept禁止拦截 } } else { // 非DOWN事件且无处理目标时,直接拦截 intercepted = true; } // 如果拦截或已有处理目标,清除无障碍焦点标记 if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // 检查是否取消事件(ACTION_CANCEL 或标记被重置) final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 处理多点触控相关逻辑 final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE; // 判断是否分割事件(不同指针ID分发给不同子View) final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0 && !isMouseEvent; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; // 未被取消且未被拦截时,寻找能接收事件的子View if (!canceled && !intercepted) { // 处理无障碍焦点View优先逻辑 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; // 处理DOWN/POINTER_DOWN/HOVER_MOVE事件,寻找新触摸目标 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // 获取事件索引 // 计算要分配的指针ID位(分割模式下每个指针独立,否则全分配) final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // 清理相同指针ID的旧目标,避免状态不同步 removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { // 获取触摸点坐标(鼠标事件使用游标位置) final float x = isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex); final float y = isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex); // 构建子View遍历列表(考虑绘制顺序) final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; // 从最上层子View开始反向遍历(Z-order从高到低) for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); // 优先处理无障碍焦点View if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; // 跳过非焦点View } childWithAccessibilityFocus = null; // 处理完后重置 i = childrenCount; // 重新遍历剩余子View } // 检查子View是否能接收事件且触摸点在其范围内 if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // 检查是否已存在该子View的触摸目标 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // 已有目标,追加当前指针ID newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); // 尝试分发事件给子View if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 子View消费了事件,记录目标信息 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(); } // 未找到新目标但已有目标时,追加到最近目标 if (newTouchTarget == null && mFirstTouchTarget != null) { newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // 分发事件到触摸目标 if (mFirstTouchTarget == null) { // 无触摸目标,将当前ViewGroup作为普通View处理 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 { // 判断是否需要取消子View的事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 分发事件(若取消则发送ACTION_CANCEL) 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; } } // 处理事件结束后的清理工作 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); // 移除对应指针ID的目标 } } // 未处理事件时记录验证信息 if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; } /** * 实现此方法以拦截所有触摸屏移动事件。这允许您监视事件分发给子View的过程, * 并随时获取当前手势的控制权。 * * <p> * 使用此方法需谨慎,因为它与 {@link View#onTouchEvent(MotionEvent)} 有复杂交互, * 需要同时正确实现这两个方法。事件接收顺序如下: * * <ol> * <li>您会首先在此收到 DOWN 事件 * <li>DOWN 事件会被子View处理,或传递到自己的 onTouchEvent() 方法。 * 这意味着您应该让 onTouchEvent() 返回 true 以继续接收后续手势事件, * 返回 true 后将不再通过 onInterceptTouchEvent() 接收事件,所有触摸处理 * 都需在 onTouchEvent() 中完成 * <li>只要本方法返回 false,后续事件(包括 UP)会先传递到这里,再传递给目标 View 的 onTouchEvent() * <li>如果本方法返回 true,子View将收到 ACTION_CANCEL 事件,后续事件直接传递到 * 本 ViewGroup 的 onTouchEvent() 而不再经过此方法 * </ol> * * @param ev 沿视图层级向下传递的触摸事件 * @return 返回 true 将劫持本该传递给子View的事件,改由本 ViewGroup 的 onTouchEvent() 处理。 * 当前目标会收到 ACTION_CANCEL 事件,后续事件不再传递到此方法 */ public boolean onInterceptTouchEvent(MotionEvent ev) { // 检查事件是否来自鼠标设备(InputDevice.SOURCE_MOUSE) // 且动作为按下事件(ACTION_DOWN) // 且按下了主按钮(BUTTON_PRIMARY,通常指左键) // 且触摸点位于滚动条滑块区域 if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { // 当鼠标左键点击滚动条滑块时,拦截事件用于处理滚动条拖动操作 return true; } // 默认不拦截事件,交由子View处理 return false; } }关键事件解析
点击事件和长按事件
Android中View的onTouchEvent方法处理点击和长按事件的流程如下:
1. ACTION_DOWN 事件处理
- 状态初始化:记录按下的初始坐标和时间,设置PRESSED状态(触发按压效果)。
- 长按检测:通过postDelayed发送一个延迟任务(延迟时间为ViewConfiguration.getLongPressTimeout(),默认约500毫秒),用于检测长按事件。
postDelayed(mLongPressRunnable, ViewConfiguration.getLongPressTimeout());2. ACTION_MOVE 事件处理
- 滑动判断:检查触摸点是否滑出View边界或超出TouchSlop(系统定义的阈值,避免微小滑动误判)。
- 取消条件:若滑动超出范围,移除长按任务并重置PRESSED状态。
if (!pointInView(x, y, TouchSlop)) { removeLongPressCallback(); setPressed(false);}3. ACTION_UP 事件处理
- 移除长按任务:无论是否触发点击,先移除延迟的长按检测。
- 触发点击事件:若未触发长按且处于PRESSED状态,调用performClick()。
if (!mHasPerformedLongPress) { performClick(); // 内部调用OnClickListener.onClick()}4. 长按事件触发
- 延迟任务执行:在延迟任务中调用performLongClick(),若成功触发(如设置了OnLongClickListener),标记mHasPerformedLongPress为true,避免后续点击。
private final Runnable mLongPressRunnable = new Runnable() { public void run() { if (performLongClick()) { // 调用OnLongClickListener.onLongClick() mHasPerformedLongPress = true; } }};5. ACTION_CANCEL 处理
- 状态重置:移除长按任务并重置PRESSED状态,确保事件取消后不触发任何回调。
关键逻辑
- 点击与长按互斥:长按触发后(mHasPerformedLongPress = true),ACTION_UP不会触发点击。
- TouchSlop 判断:确保微小滑动不误触发点击或长按。
- PRESSED 状态:用于视觉反馈(如按钮按压效果),并在事件处理中作为条件判断。
源码核心伪代码
public boolean onTouchEvent(MotionEvent event) { if (!isClickable()) return false; switch (event.getAction()) { case ACTION_DOWN: setPressed(true); postDelayed(mLongPressRunnable, LONG_PRESS_TIMEOUT); break; case ACTION_MOVE: if (outOfBounds()) { removeLongPressCallback(); setPressed(false); } break; case ACTION_UP: removeLongPressCallback(); if (isPressed() && !mHasPerformedLongPress) { performClick(); } setPressed(false); break; case ACTION_CANCEL: removeLongPressCallback(); setPressed(false); break; } return true;}总结
- 点击:由ACTION_UP触发,需满足未滑动且未触发长按。
- 长按:由延迟任务触发,若成功消费事件,则屏蔽后续点击。
- 状态管理:通过PRESSED标志和mHasPerformedLongPress控制事件流。
- 这篇博客只写了一半,需要带入上述问题对源码进行解析,才能真正领会设计者的匠心,后续也需要通过一些自定义手势操作的 View,来解释 Android 的触摸反馈机制。