0x00 事件分发源码
View 事件分发是 Android 开发者无法避免的问题,针对 Android-10.0.0_r1
中 ViewGroup 和 View 源码,进行一波解读。
在源码的方法中,使用的注释都是
//
单行注释,文章中将使用/**/
多行注释来解读代码。
1. ViewGroup 和 View 源码分析
1.1 ViewGroup.dispatchTouchEvent() 方法
/* 先说明一下,mFirstTouchTarget 会在消费 DOWN 事件后赋值,代码中会指出 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
/* 在 View 中可以看到 mInputEventConsistencyVerifier 的注释,debug 使用,跳过 */
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
/* 辅助功能相关,跳过 */
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
/* handled 保存返回结果的变量 */
boolean handled = false;
/* onFilterTouchEventForSecurity 出于安全考虑,Window 被遮挡的情况事件不响应 */
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
/* ACTION_MASK 和 ACTION_POINTER_INDEX_MASK 分别表示事件和第几个触摸点 */
/* 这里是通过与运算获取低 8 位表示的事件类型 */
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.
/* 根据注释,出现应用切换、ANR 等事件,可能会没有 CANCEL 或 UP 事件分发,新一轮事件分发会清除掉之前状态
* 对应代码不再列出来,如其名,下发 cancel 事件并清理 touchtarget */
cancelAndClearTouchTargets(ev);
/* 这里也是清除 touchtarget,并且将 FLAG_DISALLOW_INTERCEPT 标识清除,后面会考 */
resetTouchState();
}
// Check for interception.
/* 拦截相关逻辑 */
/* 在 DOWN 事件中,或者 touchtarget 不为空的情况下,根据 FLAG_DISALLOW_INTERCEPT 决定是否调用
* onInterceptTouchEvent,外层 else 逻辑直接查看原注释 */
/* 后面可以看到,如果消费了 DOWN 事件,mFirstTouchTarget 不会为空 */
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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
/* 辅助功能相关,跳过 */
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
/* cancel 事件判断 */
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
/* FLAG_SPLIT_MOTION_EVENTS 这里先不展开,默认为 false */
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
/* 事件正常分发的处理(既不是取消也不拦截) */
if (!canceled && !intercepted) {
// If the event is targeting accessibility 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;
/* DOWN 事件处理 */
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.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
/* newTouchTarget 还没赋值,逻辑就是判断是否有子 View */
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 的遍历顺序,有两个属性会影响:
* 1. 自定义顺序,setChildrenDrawingOrderEnabled(boolean)
* 2. Z 轴顺序 */
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
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;
}
/* 1. View 不可见或者是关联了动画 2. 事件位置在 View 内 */
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
/* 如果 TouchTarget 包含当前 View,修改其对应的触摸事件多点触控信息 */
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.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
/* 清除 UP 事件处理标识 */
resetCancelNextUpFlag(child);
/* 调用子 View dispatchTouchEvent,子 View 消费事件则结束循环遍历 */
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
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();
/* 这里是关键,给 mFirstTouchTarget 增加一个节点,之后不为空 */
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
} /* DOWN 事件分发结束 */
} /* 非取消和拦截事件的处理 */
// Dispatch to touch targets.
/* DOWN 事件的后续事件,都是通过 mFirstTouchTarget 是否为空确定 */
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
/* 调用父类的 dispatchTouchEvent 方法 */
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 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;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
/* 直接向 target 分发事件,如果事件被拦截 分发 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;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
/* 处理一轮事件分发完成后的重置状态操作 */
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;
}
1.2 View.dispatchTouchEvent()
/**
* 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.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
/* 辅助功能,跳过 */
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
/* 对事件序列做一个检查,跳过 */
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
/* ENABLE 状态下被 ScrollBar 消耗 */
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
/* ENABLE 状态下被 TouchListener 消耗 */
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
/* 没有被 ScrollBar 和 TouchListener 消耗的情况下,由 onTouchEvent 处理 */
if (!result && onTouchEvent(event)) {
result = true;
}
}
/* 跳过 */
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// 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.
/* 嵌套滑动事件结束,分为 UP、CANCEL、未处理的 DOWN */
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
1.3 View.onTouchEvent()
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
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;
/* DISABLED 状态下,UP 事件中清除 pressed 状态 */
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
/* 重复一遍,DISABLED 状态的可点击 View 依旧会消耗触摸事件,只是不会响应,在这里就直接返回了 */
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
/* 可点击或者是设置了提示最后返回 true,否则直接返回了 false */
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
/* 具体事件响应,按照 DOWN、MOVE、UP、CANCEL 顺序查看 */
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;
/* UP 事件后续就是处理 prepressed 和 pressed 状态,否则不会有任何处理 */
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
/* 焦点处理 */
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
/* 这是 pressed 状态 */
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
/* UP 事件时,长按还没有响应,移除长按回调 */
removeLongPressCallback();
/* 点击事件响应,此处位于 pressed 和 prepressed 状态 */
/* 在 MOVE 事件中,移出 View 范围,pressed 状态为 false */
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
/* pressed 状态处理 */
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
/* 移除轻触回调 */
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
/* 重置长按判断 */
mHasPerformedLongPress = false;
if (!clickable) {
/* 这里检查长按是判断提示显示,长按也是 clickable */
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
/* 鼠标操作,跳过 */
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
/* 延迟 pressed 状态,并且需要检查长按操作 */
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
/* 直接设置 pressed 状态,并且设置长按检查 */
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) {
/* 设置 Drawable 状态 */
drawableHotspotChanged(x, y);
}
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
final float ambiguousMultiplier =
ViewConfiguration.getAmbiguousGestureMultiplier();
if (!pointInView(x, y, touchSlop)) {
/* 触摸点不在 View 内,事件具体行为还不确定,延迟长按回应 */
// The default action here is to cancel long press. But instead, we
// just extend the timeout here, in case the classification
// stays ambiguous.
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* ambiguousMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= ambiguousMultiplier;
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
/* 触摸点不在 View 内,移除触摸、长按回调,修改 pressed 状态为 false,UP 事件要用 */
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
/* DEEP_PRESS 直接响应长按 */
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
0x01常见问题
如果你有遇到事件相关的(面试)问题,如果一下问题中没有提及,可留言,我将保持持续更新。
1. CANCEL 发生的场景
- 事件分发过程中出现了 Window 焦点变化(如启动一个 Activity),会给之前消费事件的 View 发送。
- 事件分发过程中,ViewGroup 拦截了消息(指 onInterceptTouchEvent 返回 true),会给之前的 View 发送。
- 事件分发过程中,ViewGroup 没有继续往下分发后续事件(不建议这么做,事件分发流程中,需要拦截事件,重写 onInterceptTouchEvent),下一次点击对应 View 会触发。
2. 事件分发过程中移动到 View 外面,会触发哪些事件?
- View 消费了 Down 事件,由于 ViewGroup 中会记录 TouchTarget 之后的事件直接分发给 Target,所以 View 可以正常收到 MOVE 和 UP 事件。
- View 没有消费 Down 事件,后续事件不会分发到 View,因此移动手指到任何地方都没有关系。
3. 事件分发过程中,移动手指到 View 外,会触发点击事件吗?
- 不会,在 View 的 onTouchEvent 方法中,Move 事件会根据当前点击位置是否在 View 内,设置press 状态,Up 事件会根据 press 状态决定是否回调 performClickInternal,此方法里面会回调 listener.onClick()。如果移到 View 外面再移动回 View 最后再离开呢?看代码!
4. 多点触控分开处理
- ViewGroup 中存在标志位 FLAG_SPLIT_MOTION_EVENTS,当此标志位为 true 时,可同时点击多个 View。ABC 三个 View 依次点击之后抬起,A 收到三指触摸事件,B 收到两个,C 收到一个
FLAG_SPLIT_MOTION_EVENTS 在 Build.VERSION_CODES.HONEYCOMB(11) 之后在 initViewGroup() 中默认打开,但是会被之后的 initFromAttributes() 默认 false 覆盖。
5. 判断事件是否消费的流程
- View 的 dispatchTouchEvent 会先由 OnTouchListener 去处理,没有消费就交给 onTouchEvent
- 在 onTouchEvent 中,如果 View 设置了点击、长按、上下文监听之一,即便是
disable
状态也一定会消费掉事件,只是不会执行后续调用逻辑。之后会由 TouchDelegate 处理,之后是判断点击、长按等。
6. requestDisallowInterceptTouchEvent() 作用周期
下一次的 Down 事件中 resetTouchState() 方法会清除掉对应标志。如果在 View 的 Up 事件中设置禁止拦截,实际不会生效。