Android 源码剖析:View 的 Touch 事件分发
这篇文章讲 View 的 Touch 事件分发,内容比较简单。是从源码去分析 View 的 onTouchEvent
、setOnClickListener.onClick
、setOnTouchListener.onTouch
、、
setOnTouchListener.onTouch``中的布尔返回值关系。
Android 手势
手指触发到 View 到离开手指主要经历了三个阶段,分别是:
- ACTION_DOWN(0) 手指按下时
- ACTION_MOVE(2) 手指移动时
- ACTION_UP(1) 手指离开时
复写代码
我们新建一个TouchView
开启今天的实验,当我们复写onTouchEvent
方法或者调用setOnTouchListener
方法时候,Android Studio 有警告提示,提示意思就是我们也应该复写performClick
方法。
Custom view TouchView overrides onTouchEvent but not
Custom view
TouchView
has setOnTouchListener called on it but does not override performClick
既然是提示必定有它的道理,那么我们根据要求也复写了performClick
。
TouchView#onTouchEvent should call TouchView#performClick when a click is detected more...
onTouch should call View#performClick when a click is detected more...
我们复写了performClick
,却提示另外一个警告,意思就是说我们应该在onTouchEvent
和onTouch
方法中去调用performClick
,那么这个也是我们今天所需要研究的一个问题之一,为何要在这两个方法中调用performClick
?
测试事件分发的顺序
public class TouchView extends View {
...略去构造方法
public static String toActionString(int action) {
switch (action) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
}
return null;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("onTouchEvent", TouchView.toActionString((event.getAction())));
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e("dispatchTouchEvent", TouchView.toActionString((event.getAction())));
return super.dispatchTouchEvent(event);
}
@Override
public boolean performClick() {
Log.e("performClick", "performClick");
return super.performClick();
}
TouchView touchView = findViewById(R.id.touch_view);
touchView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("onClick","onClick");
}
});
touchView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("onTouch", TouchView.toActionString((event.getAction())));
return false;
}
});
上面我们复写了onTouchEvent
、dispatchTouchEvent
、performClick
,调用实现了setOnClickListener
和setOnTouchListener
方法,手指点击 View 然后离开,看看打印的顺序。
注意:下面的例子我是刻意稍稍停留手指才离开,如果是点击手指马上离开,是不会有
ACTION_MOVE
。
dispatchTouchEvent: ACTION_DOWN
onTouch: ACTION_DOWN
onTouchEvent: ACTION_DOWN
dispatchTouchEvent: ACTION_MOVE
onTouch: ACTION_MOVE
onTouchEvent: ACTION_MOVE
dispatchTouchEvent: ACTION_UP
onTouch: ACTION_UP
onTouchEvent: ACTION_UP
performClick: performClick
onClick: onClick
可以看到事件的分发是从dispatchTouchEvent
> onTouch
> onTouchEvent
> performClick
,
当把onTouch的返回值从false 改成 true,再次运行。
dispatchTouchEvent: ACTION_DOWN
onTouch: ACTION_DOWN
dispatchTouchEvent: ACTION_MOVE
onTouch: ACTION_MOVE
dispatchTouchEvent: ACTION_UP
onTouch: ACTION_UP
当把dispatchTouchEvent的返回值改成 true,再次运行。
dispatchTouchEvent: ACTION_DOWN
dispatchTouchEvent: ACTION_MOVE
dispatchTouchEvent: ACTION_UP
可以看到当 onTouch 返回 true 的时候,onTouchEvent 和 performClick 和 onClick 都不再调用,这个也是为何IDE会提示我们为何要复写并调用performClick的原因,如果我们自己处理手势相关的问题,那么点击的事件也应该由我们自行去分发,避免点击无效,除非我们不需要点击事件。我们一会儿会去源代码分析 onTouch 的返回值 对 onTouchEvent的影响。
带着问题出发
下面就带着这三个问题出发,阅读源代码,理解 View 的事件分发过程。
- onClick 和 onLongClick 的实现原理。
-
dispatchTouchEvent
>onTouch
>onTouchEvent
>performClick
的过程,事件是如何是层层被消费掉的? - onLongClick 的返回值对onClick的影响?
- 实验案例:实现当触摸超过5秒钟,点击事件无效。
源码分析 dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
... 略去无关代码
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
... 略去无关代码
return result;
}
由于本文只分析触摸上面提出的几个问题,所以略去部分不相干的代码,这个方法就变得非常简单了,这个代码当中可以看到li.mOnTouchListener.onTouch(this, event)
返回true时候,result就变成了true,如果result = true,那么就不再调用onTouchEvent(event)
方法,所以就解析了,onTouch为何会拦截掉onTouchEvent。
源码分析 onTouchEvent
这个方法比较复杂,View 大多数手势相关的操作都是在这里完成,onClick 和 onLongClick 都是在这个方法实现,由于代码很多,所以我就精简保留了重要的代码,和原来方法差别有点大。
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 (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
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();
}
// 如果 长按事件执行了并且返回true,那么点击事件将会不再生效
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();
}
}
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (!clickable) {
checkForLongClick(0, x, y);
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;
case MotionEvent.ACTION_MOVE:
// 判断手指是否已经离开的当前 View 的区域,如果离开了就移除长按相关的事件
if (!pointInView(x, y, mTouchSlop)) {
removeTapCallback();
removeLongPressCallback();
}
break;
}
return true;
}
return false;
}
CheckForTap
除了处理Pressed事件,最重要就是和CheckForLongPress
一样是处理长按事件,CheckForTap 的使用是在可滚动的容器中使用,通过延迟 100 毫秒,判断避免手指只是用于滑动。
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
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);
}
}
调用checkForLongClick
并不会马上触发onLongClick
事件,而是延迟 500 毫秒才执行。
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
当onLongClick
返回true的时候,mHasPerformedLongPress
就变成了 true,这个属性会影响 onTouchEvent
的,onClick事件将会不再调用。
private final class CheckForLongPress implements Runnable {
...
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
...
}
总结
从上一节onTouchEvent的源码,我们可以得知,实际上在按下去的时候就已经添加了长按事件(如果是在可以滚动的父容器,会先延迟100毫秒添加长按事件)但是并没有立刻执行,而是延迟500毫秒,如果在500毫秒内,手指离开了 View 的区域,将会取消分发长按事件,否则长按事件就会正常执行。当长按事件返回 true 时候,onClick 就不再执行。