好了,那么接下来就来看一下完整的 view.onTouchEvent 代码:
View.java api29 public boolean onTouchEvent(MotionEvent event) { // 获取触控点坐标 // 这里我们发现他是没有传入触控点索引的 // 所以默认情况下view是只处理索引为0的触控点 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; // 一个被禁用的view如果被设置为clickable,那么他仍旧是可以消费事件的 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { // 如果是按下状态,取消按下状态 setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // 返回是否可以消费事件 return clickable; } // 如果设置了触摸事件代理你,那么直接调用代理来处理事件 // 如果代理消费了事件则返回true if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 如果该控件是可点击的,或者长按会出现工具提示 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // 如果是长按显示工具类标志,回调该方法 if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } // 如果是不可点击的view,同时会清除所有的标志,恢复状态 if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } // 判断是否是按下状态 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 如果可以获取焦点但是没有获得焦点,请求获取焦点 // 正常的触摸模式下是不需要获取焦点,例如我们的button // 但是如果在按键模式下,需要先移动光标选中按钮,也就是获取focus // 再点击确认触摸按钮事件 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // 确保用户看到按下状态 setPressed(true, x, y); } // 两个参数分别是:长按事件是否已经响应、是否忽略本次up事件 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 这是一个单击事件,还没到达长按的时间,移除长按标志 removeLongPressCallback(); // 只有不能获取焦点的控件才能触摸click监听 if (!focusTaken) { // 这里使用发送到消息队列的方式而不是立即执行onClickListener // 原因在于可以在点击前触发一些其他视觉效果 if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClickInternal(); } } } // 取消按下状态 // 这里也是个post任务 if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // 如果发送到队列失败,则直接取消 mUnsetPressedState.run(); } // 移除单击标志 removeTapCallback(); } // 忽略下次up事件标志设置为false mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: // 输入设备源是否是可触摸屏幕 if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } // 标志是否是长按 mHasPerformedLongPress = false; // 如果是不可点击的view,说明是长按提示工具的view // 直接检查是否发生了长按 if (!clickable) { // 这个方法会发送一个延迟的任务 // 如果延迟时间到还是按下状态,那么就会回调onLongClickListener接口 checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); break; } // 判断是否是鼠标右键或者手写笔的第一个按钮 // 特殊处理直接返回 if (performButtonActionOnTouchDown(event)) { break; } // 向上遍历view查看是否在一个可滑动的容器中 boolean isInScrollingContainer = isInScrollingContainer(); // 如果在一个可滑动的容器中,那么需要延迟一小会再响应反馈 if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); // 利用消息队列来延迟检测一个单击事件,延迟时间是ViewConfiguration.getTapTimeout() // 这个时间是100ms postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // 没有在可滑动的容器中,直接响应触摸反馈 // 设置按下状态为true 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: // 通知view和drawable热点改变 // 暂时不知道什么意思 if (clickable) { drawableHotspotChanged(x, y); } final int motionClassification = event.getClassification(); final boolean ambiguousGesture = motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; int touchSlop = mTouchSlop; // view已经被设置了长按标志且目前的事件标志是模糊标志 // 系统并不知道用户的意图,所以即使滑出了view的范围,并不会取消长按标志 // 而是延长越界的误差范围和检查长按的时间 // 因为这个时候系统并不知道你是想要长按还是要滑动,结果就是两种行为都没有响应 // 由你接下来的行为决定 if (ambiguousGesture && hasPendingLongPressCallback()) { final float ambiguousMultiplier = ViewConfiguration.getAmbiguousGestureMultiplier(); // 判断此时触控点的位置是否还在view的范围内 // touchSlop是一个小范围的误差,超出view位置slop距离依旧判定为在view范围内 if (!pointInView(x, y, touchSlop)) { // 移除原来的长按标志 removeLongPressCallback(); // 延长等待时间,这里是原来长按等待的两倍 long delay = (long) (ViewConfiguration.getLongPressTimeout() * ambiguousMultiplier); // 减去已经等待的时间 delay -= event.getEventTime() - event.getDownTime(); // 添加新的长按标志 checkForLongClick( delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); } touchSlop *= ambiguousMultiplier; } // 判断此时触控点的位置是否还在view的范围内 // touchSlop是一个小范围的误差,超出view位置slop距离依旧判定为在view范围内 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 /* 延迟时间为0 */, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); } break; } return true; } // 对应if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) return false; } 最后如果你能看到这里,说明你对于viewGroup和view的事件处理源码已经了如指掌了。(高兴之余不如给笔者点个赞?(: ~)
最后这里再来总结一下:
触摸事件,从屏幕产生后,经过系统服务的处理,最终会发送到viewRootImpl来进行分发;
viewRootImpl会调用它所管理的view的 dispatchTouchEvent 方法来分发事件,那么这里就会分为两种情况:
如果是view,那么会直接处理事件
如果是viewGroup,那么会向下派发事件
viewGroup会为每个触控点尽量寻找感兴趣的子view,最后再自己处理事件。viewGroup的任务就是把事件分发按照原则精准地分发给他子view。
事件分发中一个非常重要的原则就是:一个触控点的事件序列,只能给一个view消费,除了特殊情况,如被viewGroup拦截。
viewGroup为了践行这个原则,touchTarget的设计是非常重要的;他将view与触控点进行绑定,让一个触控点的事件只会给一个view消费
view的 dispatchTouchEvent 主要内容是处理事件。首先会调用onTouchListener,如果其没有处理则会调用onTouchEvent方法。
onTouchEvent的默认实现中的主要任务就是辨别单击与长按事件,并回调onClickListener与onLongClickListener