Android: 详解触摸事件如何传递

当视图的层次结构比较复杂的时候,触摸事件的响应流程也变得复杂。

举例来说,你也许有一天想要制作一个手势极其复杂的 Activity 来折磨你的用户,你经过简单思索,认为其中应该包含一个 PageViewer,而 PageViewer 中又应包含一个 ListView。你的 ListView 中的每一项 ( item ) 还需要响应左右滑动的手势,来显示删除记录的按钮,按钮自然要响应点击的事件,而整个 ListView 需要响应上下滑动的手势,用来滚动整个列表,同时你还希望通过多个手指左右滑动的手势,可以使整个 PageViewer 翻页,甚至你还希望像 iPad 中一样,响应五指聚拢的手势,来关闭整个 Activity …… 到这里还没完,你还希望,可以通过点击 ListView 中的某一项,查看详细信息;长按某一项,可以弹出上下文菜单,来进行修改。

这时候你就不得不弄清楚,触摸事件到底是如何一步一步从 Activity 传递到 PageViewer,然后再传递到 ListView …… 以便让每一个层级的 View 拦截自己需要处理的手势,而把自己不需要的手势传递给其他 View。你还要弄清楚,怎么区分这次触摸事件,究竟是一次简单的点击,还是一次长按,还是一个复杂手势的一部分。

下文中,我们将简单剖析一下 Android 的触摸传递机制。

涉及到的类和方法

总的来说,触摸传递过程是由上至下的。一个典型的触摸事件,从 Activity 开始,经过根视图,再经过层层 ViewGroup,最终传递到某一个 ViewViewGroup 上,进行处理。主要涉及到的类自然包括 ActivityViewGroup 以及 View 了。

首先,在 ActivityView 中,都定义了下面两个方法 (虽然在这两个类中,这两个方法的方法名,参数列表和返回值类型完全一样,但 Activity 并不是 View 的子类,下面的两个方法在 ActivityView 中被单独定义)。

// 尝试将触摸事件交给自己的子视图 (如果有的话) 处理: 调用子视图的 dispatchTouchEvent()
// 或者自己处理: 调用自己的 onTouchEvent() 或 OnTouchListener.onTouch()
// 无论是自己的子视图,还是自己,完成了事件处理,都返回 true
public boolean dispatchTouchEvent(MotionEvent ev)
// 尝试自己处理触摸事件. 如果完成处理 (不需要再交给其他 View 处理), 则返回 true
public boolean onTouchEvent(MotionEvent event)

由于 ViewGroupView 的子类,所以自然 ViewGroup 中也存在这两个方法。在 ViewGroup 中还单独定义了方法

// 如果事件需要在该 ViewGroup 截断 (自己处理该事件, 不再传递给其子视图), 则返回 true
public boolean onInterceptTouchEvent(MotionEvent ev)

以上3个方法,通常只有在我们需要自定义 View 时,才需要 Override。对于现成的 View,我们可以通过 View (也包括 ViewGroup) 的 setOnTouchListener() 方法,添加触摸事件监听器来监听触摸事件,

someView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 尝试自己处理触摸事件, 完成处理 (不需要其他 View 再处理), 则返回 true
// ...
}
});

同样可以起到和 onTouchEvent() 类似的效果。两者有什么区别,包括前面的几个方法的具体作用,会在下文中慢慢解释。

在这之前,我们应当注意到,

  • 它们都具有一个 MotionEvent 类型的参数,里面包含有触摸事件的详细信息 (包含事件的类型,手指按下还是松开,以及触摸的具体坐标位置等),本文涉及的大部分方法都有这个参数,后面就不再重复了。限于篇幅,这里对该类的使用就不详细介绍了。对 MotionEvent 有疑问可以参考官方文档中对 MotionEvent 的描述。

  • 它们都返回一个 boolean 值,通过返回 true,来声明触摸事件在自己这里已经完成,或者说“消费”掉了。例如,文章开头的例子中, ListView 的某一项 ( item ) 对应的视图 View 监听到一个触摸事件,发现是左右滑动的手势,该 View 就会选择将这个事件“消费”掉,这样其父视图 ListViewPageViewer 以及 Activity 就不会重复处理这一事件。

传递机制详解

先分别看看上面的4个方法的具体作用,以及 boolean 返回值的意义。

Activity

先看第一个方法 dispatchTouchEvent(),该方法是整个触摸传递机制的核心。一般地,父视图 ( parent view ) 通过调用子视图 ( child view ) 的 dispatchTouchEvent() 方法完成触摸事件的向下传递。

焦点所在 ActivitydispatchTouchEvent() 方法,是整个触摸事件的“入口”。该方法首先,无条件地,不可被截断地 (除非你 Override ActivitydispatchTouchEvent() 方法),将事件交给它的下属处理,即调用该 Activity 的根视图的 dispatchTouchEvent() 方法。如果它的下属没有完成该事件的处理 (调用结果返回 false),则尝试自己处理,即调用 Activity 自己的 onTouchEvent() 方法。如果仍然不能完成处理 (调用结果返回 false),则可以认为该事件的处理宣告失败,整个方法返回 false (完成了处理则返回 true)。

注意,如果第一次调用 (即调用下属视图的 dispatchTouchEvent() 方法),返回了 true,表示下属已经完成了对事件的处理工作,此时不会再调用 Activity 自己的 onTouchEvent() 方法。

因为 Activity 没有父视图,自身不能设置触摸事件的监听器 OnTouchListener,也没有 onInterceptTouchEvent() 方法,情况相对简单,就不给大家 show 源代码了。

没有子视图的 View

下面我们再看一下另一个比较简单的情况,即没有子视图的 View (如果套用二叉树的概念,Activity 是树根,这里的 View 指的就是树叶了)。对于这些视图的 dispatchTouchEvent() 方法,由于没有下属可供派遣,事情只能自己解决。如果该 View 被注册过触摸事件监听器 OnTouchListener,则优先调用 OnTouchListener.onTouch() 方法。如果没有注册过监听器,或者 OnTouchListener.onTouch() 方法没有完成处理 (调用结果返回 false),才会再尝试调用自己的 onTouchEvent() 方法。下面这段 Android 源代码 (有所删减),完成了上述逻辑。

// 没有子视图的 View 的 dispatchTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent event) { // ... // View.setOnTouchLisener() 方法设置的触摸事件监听者
ListenerInfo li = mListenerInfo; // 如果设置了监听者, 优先调用 OnTouchListener.onTouch()
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) { // 如果 OnTouchListener.onTouch() 完成了事件处理
return true;
} // 如果监听者没有完成事件处理(或者没有监听者), 再调用 onTouchEvent()
if (onTouchEvent(event)) { // 如果 onTouchEvent() 完成了事件处理
return true;
} // ... // 如果 OnTouchListener.onTouch() 和 onTouchEvent() 都没有完成事件处理
return false;
}

再来看 View 中定义的 onTouchEvent() 方法,

// View (包括 ViewGroup) 的 onTouchEvent() 方法
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags; // ... if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { // 判断和处理 onClick 和 onLongClick 事件
// ... return true;
} // 非 onClick 或 onLongClick 的普通 onTouch 事件
// 默认的 View.onTouchEvent() 不作处理
return false;
}

ViewGroup 并没有 Override View 中的 onTouchEvent(),所以这在 ViewGroup 中同样适用。有两点需要注意

  • onTouchEvent() 中,会判断触摸是否构成一次点击事件,从而交给其他一些监听器,如 onClickListener (监听点击事件),onLongClickListener (监听长按事件) 来处理,同时返回 true。如果你需要自定义自己的 View,并建立自己的触摸事件响应,Override 了原本的 onTouchEvent() 方法,点击事件 OnClickLisener.onClick() 等其他一些事件将永远不会触发,并且,这将导致一个 ClickableViewAccessibility 的编译器警告。如果你仍然需要响应点击事件,你需要在 Override 之后的 onTouchEvent() 方法中,模仿基类版本,手动判断触摸事件是否形成一次点击,并手动调用 performClick() 方法,来触发点击事件 OnClickLisenner.onClick()

  • 如果该 View 注册了触摸事件监听器 OnTouchListener,则 OnTouchListener.onTouch() 会被优先调用。只有该调用返回 false,或者没有注册监听器时 onTouchEvent() 方法才会被调用。如果 onTouchEvent() 没有被调用,点击事件 OnClickListener.onClick() 等,其他一些事件也不会触发。

ViewGroup

上有老下有小的 ViewGroup 情况最为复杂。我们先看 ViewGroupdispatchTouchEvent() 方法,对于这个方法而言,没有父视图的 Activity 和没有子视图的 View (下文简称叶子 View),某种程度上都可以看成 ViewGroup 的特例。

ViewGroupdispatchTouchEvent() 方法被其父视图 (可能是 Activity 的根视图,也可能是其他的 ViewGroup) 调用,dispatchTouchEvent() 方法所要做的事情就是竭尽所能,利用自己的资源 (包括派遣给自己的子视图) 来处理触摸事件,并向父视图反馈处理结果。

Activity 中,首先无条件地将触摸事件派遣给自己的下属,我们可以通过调用 ViewGrouponInterceptTouchEvent() 方法,决定触摸事件是否在此 ViewGroup 处截断。即,如果 onInterceptTouchEvent() 方法返回 true,则不再继续传递给自己的子视图,而是 ViewGroup 自己尝试处理;如果返回 false,则不截断 (就像在 Activity 中一样),首先尝试将任务派遣给子视图完成,如果没有子视图或子视图不能完成 (调用子视图的 dispatchTouchEvent() 方法返回 false),那么 ViewGroup 不得不尝试自己处理触摸事件。逻辑见下图。

Android: 详解触摸事件如何传递

ViewGroup 所谓“自己处理”的方法与叶子 View 相同,如果注册了触摸事件监听器 OnTouchLisener,则优先调用 OnTouchLisener.onTouch() 方法,如果没有注册监听器,或OnTouchLisener.onTouch() 方法返回 false,再尝试调用 ViewGroup 自身的 onTouchEvent() 方法,且 ViewGrouponTouchEvent() 方法完全继承自 View,没有 Override (可以参看上文源代码,做了点击事件和长按事件的判断)。

下面是 ViewGroup 中,dispatchTouchEvent() 的源代码骨架

@Override
public boolean dispatchTouchEvent(MotionEvent ev) { // 省略了关于检查触摸事件类型的代码. 这一部分代码用于处理,
// 组成一次完整的手势(从 ACTION_DOWN 到 ACTION_DOWN) 的
// 各个触摸事件之间的关联性 // ... boolean handled = false; final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK; // ... // 确认是否将事件在此截断 (不再传递给子 View)
final boolean intercepted;
// ...
// 检查 onInterceptTouchEvent()
intercepted = onInterceptTouchEvent(ev);
// 这里做了简化, 源代码中, 考虑的更多的可能发生截断的复杂情况 // 确认事件是否被取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL; TouchTarget newTouchTarget = null; if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // ... final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) { // 触摸事件位置坐标
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex); // 通过子 View 的位置坐标, 确定应该将事件传递给哪一个子 View
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled(); // 遍历所有子 View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ? getChildDrawingOrder(
childrenCount, i) : i;
final View child = children[childIndex]; // 确认这个子 View 是否在合适的位置
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child,
null)) {
continue;
} // 找到了合适位置的子 View // ...
}
} // ...
}
} // 将触摸事件传递下去
if (mFirstTouchTarget == null) {
// 没有找到合适的子 View
// 将 GroupView 自己当作一个一般的 View 一样处理触摸事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 将触摸传递到子 View // ... if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) { // 子 View 处理了触摸事件
handled = true;
} // ...
} // ... return handled;
}

ViewGroup 中,onInterceptTouchEvent() 的默认实现是直接返回 false,即不截断事件, 而是将事件传递给子视图处理 (调用子视图的 dispatchTouchEvent() 方法)。如果需要,可以在自定义的 ViewGroup 中 Override 该函数,改变截断行为。

// ViewGroup 的 onInterceptTouchEvent() 方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}

如果子视图是 ViewGroup (还有子视图),且仍然没有截断的话,会继续调用子视图的子视图,如此递归进行。触摸事件派遣的顺序是自上而下的。

直到到达某个叶子 View (不再有子视图可以派遣),或者某个 ViewGroup 虽然还有子视图可以派遣,但其要求截断 (onInterceptTouchEvent() 方法返回 true)。这时,触摸事件真正开始尝试进行处理 (不再派遣给其他 View,而是由叶子 View 或要求截断的 ViewGroup 开始自己进行处理)。

如果该 View 完成了触摸事件的处理 (返回 true),那么对于其父视图而言,dispatchTouchEvent() 方法派遣给子视图的事件圆满完成,可以向父视图自己的父视图宣称完成事件了 (返回 true)。反之,如果该 View 自己没有完成触摸事件,对于其父视图 ViewGroup 而言,派遣子视图并没有完成事件处理,只好自己处理。如果再次没有完成,父视图会向自己的父视图返回 false,如果各层 ViewGroup 均不能完成事件处理,最终会调用 ActivityonTouchEvent() 方法,做最后的尝试。整个实际处理过程顺序正好相反,是自下而上的。

从 ACTION_DOWN 到 ACTION_UP

从手指按下,触发 ACTION_DOWN 开始,到手指离开,触发 ACTION_UP 为止,这两次触摸事件,以及这中间到其他触摸事件 (如 MOVE 事件),会被视作一次手势。如果我们对一次手势的 ACTION_DOWN 不感兴趣,即我们在监听器的 onTouch() 方法,或在 ViewonTouchEvent() 方法中返回 false。那么我们将不会再接受到该手势到后续触摸事件,直到这一手势结束 (ACTION_UP)。

另外,在一次完整手势中,只要 ViewGrouponInterceptTouchEvent() 方法有一次返回 true,那么该 ViewGroup 将会截断这次手势的全部后续触发事件,并向之前处理事件的子视图,传递一个 ACTION_CANCEL 事件。所以我们应当总是注意捕获可能的 ACTION_CANCEL 触摸事件。

总结

在没有 Override 的情况下,触摸事件的派遣将不会被截断,从 Activity 的根视图,自上而下的派遣到叶子 View ,然后调用该 ViewonTouchEvent() (如果注册了监听器的话,则优先调用 OnTouchListener.onTouch(),返回 false 才会再调用 onTouchEvent())。如果该 View 不能处理事件(onTouchEvent() 返回了 false),其父视图继续尝试处理,直到最后,调用 ActivityonTouchEvent() 方法。另外,值得注意的是,如果没有响应一个手势的开始事件 (ACTION_DOWN),则不会接到该手势的后续事件。

上一篇:JS事件监听手机屏幕触摸事件 Touch


下一篇:Windows Phone开发手记-WinRT下分组拼音的实现