前语
Touch 相关问题是 Android 面试中常问的点,不一定要求大家都从 InputFlinger 底层开端回答,但起码需要了解 Touch 抵达 App 之后的完整处理。而即使是这段偏上层的链路,也不要局限在陈词滥调的进程复述,需要深刻理解、灵活运用其中的细节和准则。
本文结合一个简略的 Touch 场景的问答,带大家加深一下 Touch 分发的理解。
- Button A 和 B 相邻,手指不抬起、从 A 平移到 B,A 会发生什么?为什么?
- 此时,B 又会发生什么?为什么?
- 之后,手指再从 B 平移回 A 后,又会发生什么?为什么?
- 最终,在 A 上抬起手指,A 会触发点击吗?为什么?
验证
咱们自定义两个 Button 分别覆写其 onTouchEvent()
,在一个 ConstraintLayout
中上下严密地放置它们,并为了区分设置为不同的背景色。
依照提问的问题过程开端尝试一下。
可以看到手指平移到 B 的那一刻,A 的 press 作用没有了,而 B 没有任何反响。即使移动回 A,A 也无法康复 press 作用,抬起之后也没有触发 click。
回答
回答原理之前,咱们先看下 log,再逐个解说。
// 手指在 A 上按下
2023-09-12 18:11:25.209 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=109.9668, y[0]=74.92432, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1823125, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=530500549 }
// 手指开端向下移动
2023-09-12 18:11:25.586 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=109.9668, y[0]=78.92334, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1823538, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=348888341 }
2023-09-12 18:11:25.633 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=109.9668, y[0]=82.92236, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1823591, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=354173977 }
...
2023-09-12 18:11:26.200 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=155.50244, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1824161, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=195296965 }
2023-09-12 18:11:26.216 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=163.84363, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1824177, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=273686682 }
// Button 高度为 168px,此时已开端出界到 B
2023-09-12 18:11:26.233 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=174.2472, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1824194, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=758026894 }
2023-09-12 18:11:26.250 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=121.96387, y[0]=178.18982, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=1, eventTime=1824211, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=498491454 }
...
2023-09-12 18:11:26.801 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=129.96191, y[0]=266.87744, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1824754, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=936130601 }
// 手指开端往上移动
2023-09-12 18:11:27.484 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=129.96191, y[0]=262.87842, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1825443, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=17662257 }
...
2023-09-12 18:11:27.585 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=137.95996, y[0]=244.88281, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825541, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=507118427 }
...
2023-09-12 18:11:27.966 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=145.16235, y[0]=175.69556, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825927, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=876127266 }
// Button 高度为 168px,此时已移动回到 A
2023-09-12 18:11:27.985 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=145.95801, y[0]=166.91626, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825944, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=352798882 }
2023-09-12 18:11:28.000 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=149.15863, y[0]=162.90283, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=2, eventTime=1825961, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=99105321 }
...
2023-09-12 18:11:28.369 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=161.9541, y[0]=86.92139, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1826312, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=764248821 }
2023-09-12 18:11:28.722 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_MOVE, actionButton=0, id[0]=0, x[0]=161.9541, y[0]=90.92041, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1826673, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=197617005 }
// 手指从 A 上抬起
2023-09-12 18:11:28.947 8944-8944/com.ellison.demo D/Touch: ButtonA#onTouchEvent() event:MotionEvent { action=ACTION_UP, actionButton=0, id[0]=0, x[0]=161.9541, y[0]=90.92041, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, classification=NONE, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=1826912, downTime=1823125, deviceId=8, source=0x5002, displayId=0, eventId=250168391 }
1. 平移到 B,A 会发生什么?
A 的 pressed 作用会被重置。
以往大家会直观地认为这是 ViewGroup 发送 ACTION_CANCEL 给 ButtonA 造成了的。
但调查 log 你会发现,即使出界了,ACTION_MOVE 一直发给了 ButtonA。同时,跟着手指的不断向下移动,ACTION_MOVE 的 y 相对坐标不断增大,当该 y 数值超越了 mBottom – mTop 的高度差的时分,Button 的父亲 View 的 onTouchEvent()
会基于其离开了 View 边界调用 setPressed(false)
去刷新 View 的 Press 状况,继而促进 ButtonA 的按下状况消失了。
public class View ... {
...
public boolean onTouchEvent(MotionEvent event) {
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
...
case MotionEvent.ACTION_MOVE:
...
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
...
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
...
break;
}
return true;
}
return false;
}
/*package*/ final boolean pointInView(float localX, float localY) {
return pointInView(localX, localY, 0);
}
public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
...
}
2. B 会发生什么?为什么?
B 没有任何反响。
其实,回答问题 1 时现已旁边面回答了 B 没有反响的直接原因:ButtonB 没有收到任何 TouchEvent。
那为什么即使手指移动到了 B 区域,系统仍不发送事情过去呢?
Button 的父布局 ViewGroup
在分发 ACTION_DOWN 的时分,经过 addTouchTarget()
将处理 DOWN 事情的 child 赋值到 mFirstTouchTarget。后续来了 ACTION_MOVE 的时分,发现 mFirstTouchTarget 已存在,就将后续事情经过 dispatchTransformedTouchEvent()
继续发给该 TouchTarget
。
源码中的注释也体现了这点:
Dispatch to touch targets, excluding the new touch target if we already dispatched to it.
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN ...) {
...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
...
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
...
resetCancelNextUpFlag(child);
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();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
...
}
...
}
}
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;
}
...
}
predecessor = target;
target = next;
}
}
...
}
...
return handled;
}
...
}
3. B 平移回 A 后,又会发生什么?
A 也不再有任何反响。
Button 的父亲 View 只在接受到 ACTION_DOWN
的时分可以调用 setPressed()
展现 pressed 作用。所以即使手指回到了 A 区域也不会触发按下 UI 的改动。
public class View ... {
...
public boolean onTouchEvent(MotionEvent event) {
...
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) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
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
removeLongPressCallback();
// 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();
}
}
}
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 (isInScrollingContainer) {
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
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
...
}
return true;
}
return false;
}
/*package*/ final boolean pointInView(float localX, float localY) {
return pointInView(localX, localY, 0);
}
public boolean pointInView(float localX, float localY, float slop) {
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
...
}
4. A 会触发点击吗?为什么?
无法触发点击。
原因很简略,从 A 移走的那刻将履行 performClick
的 Runnable
删除了,继而没有机会触发 click 或 longClick。
public class View ... {
...
public boolean onTouchEvent(MotionEvent event) {
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
...
case MotionEvent.ACTION_MOVE:
...
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
...
}
...
break;
}
return true;
}
return false;
}
...
}
结语
回忆下这 4 个问题的答案和原因。
-
Button A 和 B 相邻,手指不抬起、从 A 平移到 B,A 会发生什么?为什么?
A 的按下作用会消失。
即使手指移出界了,但 MOVE 事情仍然发给了 A,View 发现坐标超越 Button 范围之后重置了 pressed 状况。
-
此时,B 又会发生什么?为什么?
B 没有任何改动。
Button A 先收到了 DOWN 事情,导致后续的事情都发给了 A,B 没有收到任何事情,故没有反响。
-
之后,手指再从 B 平移回 A 后,又会发生什么?为什么?
A 也不康复按下作用。
View 只在接受到 DOWN 时设置 pressed 状况,即使手指回到了 A,因为没有新的 DOWN 发生,所以无法再次出现按下作用。
-
最终,在 A 上抬起手指,A 会触发点击吗?为什么?
无法触发 A 的点击。
手指从 A 出界的那刻将履行 click runnable 同时移除了,后边 UP 的时分没有可以履行的 runnable,故不会履行任何点击、长按点击的回调。
毫无疑问,Android 进行这样的处理是没有问题的。那假如咱们想要改动这个逻辑:
- 让移动到的方针 Button 出现 pressed 状况,并在手指抬起的时分呼应 click 呢,该怎样实现?
思路也不复杂,简略来说复写 ViewGroup
的 dispatchTouchEvent()
作如下处理即可:
- 发现 touchTarget 改变了,向原 target 发送 CANCEL 取消 pressed 作用
- 手动 obtain 一个 DOWN event 发送给移动到的 target,从而能使得新 target 能展现 pressed 状况和设置 click runnable
- 之后再发送物理上的实际 MOVE 事情给新 target,后边当 UP 的时分因为 DOWN 的时分补充了 runnable,确保 up 时可以履行 click
到这儿也就讲完了,这 5 个问题你都答对了吗? 期望本文能帮你加深 Touch 处理的理解。