引子
现代人每天看的和触摸的最多的,莫过于手机屏幕,安卓开发中触摸事件的分发机制也是很有意思的一部分,本文就和大家一起不深入但浅出的学习一下Android触摸事件机制。
一、触摸事件
对于触摸(Touch)触发的事件,在Android中,事件主要包括点按(onClick)、长按(onLongClick)、拖拽(onDrag)、滑动(onScroll)等,点按又包括单击和双击,另外还包括单指操作和多指操作。其中Touch的第一个状态是 ACTION_DOWN, 表示按下了屏幕。之后,touch将会有后续事件,比如移动、抬起等,一个Action_DOWN, n个ACTION_MOVE, 1个ACTION_UP,就构成了Android中众多的事件。触摸事件的产生序列通常是这样的:
- 按下(ACTION_DOWN) //表示用户按下了屏幕
- 移动(ACTION_MOVE) //表示用户在屏幕移动
- 抬起(ACTION_UP) //表示用户离开屏幕
- 取消手势(ACTION_CANCEL) //表示,不会由用户产生,而是由程序产生的
这几个事件在代码中如此定义:
public final class MotionEvent extends InputEvent implements Parcelable {
// 代码省略
public static final int ACTION_DOWN = 0; // 按下事件
public static final int ACTION_UP = 1; // 抬起事件
public static final int ACTION_MOVE = 2; // 手势移动事件
public static final int ACTION_CANCEL = 3; // 取消
// 代码省略
}
当然触摸产生的事件远远不止这几个,只不过一般接触的最多的是这几个罢了。
所有的操作事件首先必须执行的是按下操作(ACTION_DOWN),之后所有的操作都是以此作为前提,当按下操作完成后,接下来可能是一段移动(ACTION_MOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。
二、与触摸有关的组件及操作
2.1 组件
所有的事件操作都发生在触摸屏上,而在屏幕上与用户交互的就是各种各样的视图组件(View),在Android中,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。所以,事件操作主要就是发生在View和ViewGroup之间。
2.2 操作
与触摸事件有关的操作有如下3个方法:
- public boolean dispatchTouchEvent(MotionEvent event)
- public boolean onTouchEvent(MotionEvent event)
- public boolean onInterceptTouchEvent(MotionEvent event)
在View和ViewGroup中都存在dispatchTouchEvent
和onTouchEvent
方法,特别的,在ViewGroup中还有一个onInterceptTouchEvent
方法。这些方法的返回值全部都是boolean型,都返回true或者是false,这是因为事件传递的过程就是一个接一个,某一个点后根据方法boolean的返回值判断是否要继续往下传递。
三个方法可以总结为一张表格:
Touch 事件相关方法 | 方法功能 | ViewGroup | View | Activity |
---|---|---|---|---|
public boolean dispatchTouchEvent(MotionEvent ev) | 事件分发 | Yes | Yes | Yes |
public boolean onInterceptTouchEvent(MotionEvent ev) | 事件拦截 | Yes | Yes | No |
public boolean onTouchEvent(MotionEvent ev) | 事件响应 | Yes | Yes | Yes |
从这张表中我们可以看到 ViewGroup 和 View 对与 Touch 事件相关的三个方法均能响应,而 Activity 对 onInterceptTouchEvent(MotionEvent ev)
也就是事件拦截不进行响应。
另外需要注意的是 View 对 dispatchTouchEvent(MotionEvent ev)
和onInterceptTouchEvent(MotionEvent ev)
的响应的前提是可以向该 View 中添加子 View,如果当前的 View 已经是一个最小的单元 View(比如 TextView),那么就无法向这个最小 View 中添加子 View,也就无法向子 View 进行事件的分发和拦截,所以它没有dispatchTouchEvent(MotionEvent ev)
和 onInterceptTouchEvent(MotionEvent ev)
,只有 onTouchEvent(MotionEvent ev)
。
三、事件分发、拦截和消费
我们先从整体脉络上了解触摸事件机制的整个流程,大致可以按一个倒置的U型图来理解整个机制:
触摸事件从Activity触发事件然后传递到布局文件,一层一层的往子容器传递到最底层的view,如果每层布局文件未对该事件进行处理或者消费那么该事件会从最底层开始往上传到Activity进行消费。类似于一个倒置的U型。
我们按这么一个例子来理解这个U型流程:
将其中的细节略微展开,则可以得到这么一张流程图:
3.1 事件的消费
事件的消费主要由view类中的dispatchTouchEvent(MotionEvent ev)
函数和onTouchEvent(MotionEvent event)
完成,而具体的消费函数则是用户自己定义的OnTouchListener()
和onClickListener()
。
dispatchTouchEvent函数位于View.java类中。可被继承View的ViewGroup重写。
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)) {
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;
}
}
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.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
这部分源码最重要的部分在于
//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;
}
上面第一个if判断语句中前3个条件一般都是true, 关键在于li.mOnTouchListener.onTouch(this, event)
,这个就是我们可以自己复写的OnTouchListener()
的内容,返回值也是由我们定义的。如果这个返回值为true,那么就会将resut赋值为true,从而不会执行接下来的onTouchEvent(event)
函数。如果返回值为false,则会执行onTouchEvent(event)
函数。onTouchEvent(event)
主要执行我们为组件添加的ClickListener中的方法。
3.2 事件的分发
事件的拦截由ViewGroup中的dispatchTouchEvent()
完成。伪代码可以这么描述:
public boolean dispatchTouchEvent(MotionEvent event) {
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev)
}else{
consume = child.dispatchTouchEvent(ev)
}
return consume;
}
源代码有这么一段:
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;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
这里就是将触摸事件传递给子View的代码部分,其中的handled
是整个dispatchTouchEvent
的返回值,由View中定义的处理函数决定。
3.3 事件的拦截
在ViewGroup中,如果定义了拦截器,那么将不会将触摸事件进行分发,而是检测自己是否消费,然后根据返回值返回给父ViewGroup。
事件的拦截通过onInterceptTouchEvent(MotionEvent event)
实现。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
源码就简单多了,而且是肯定被之前的分发函数dispatchTouchEvent
调用。
四、总结
关于触摸事件的分发、拦截和消费,我们不必纠结源码中的详细实现,重点是了解View和ViewGroup的分发和消费逻辑,保证最终只有一个View会消费事件并成功返回一个布尔值。
参考文章
Android touch 事件传递机制 - 易术军 - 博客园 (cnblogs.com)
Android Touch事件分发过程_Mr.Simple的专栏-CSDN博客
Touch事件传递学习笔记 - (jianshu.com)