Android: 详解触摸事件如何传递
当视图的层次结构比较复杂的时候,触摸事件的响应流程也变得复杂。
举例来说,你也许有一天想要制作一个手势极其复杂的 Activity
来折磨你的用户,你经过简单思索,认为其中应该包含一个 PageViewer
,而 PageViewer
中又应包含一个 ListView
。你的 ListView
中的每一项 ( item ) 还需要响应左右滑动的手势,来显示删除记录的按钮,按钮自然要响应点击的事件,而整个 ListView
需要响应上下滑动的手势,用来滚动整个列表,同时你还希望通过多个手指左右滑动的手势,可以使整个 PageViewer
翻页,甚至你还希望像 iPad 中一样,响应五指聚拢的手势,来关闭整个 Activity
…… 到这里还没完,你还希望,可以通过点击 ListView
中的某一项,查看详细信息;长按某一项,可以弹出上下文菜单,来进行修改。
这时候你就不得不弄清楚,触摸事件到底是如何一步一步从 Activity
传递到 PageViewer
,然后再传递到 ListView
…… 以便让每一个层级的 View
拦截自己需要处理的手势,而把自己不需要的手势传递给其他 View
。你还要弄清楚,怎么区分这次触摸事件,究竟是一次简单的点击,还是一次长按,还是一个复杂手势的一部分。
下文中,我们将简单剖析一下 Android 的触摸传递机制。
涉及到的类和方法
总的来说,触摸传递过程是由上至下的。一个典型的触摸事件,从 Activity
开始,经过根视图,再经过层层 ViewGroup
,最终传递到某一个 View
或 ViewGroup
上,进行处理。主要涉及到的类自然包括 Activity
,ViewGroup
以及 View
了。
首先,在 Activity
和 View
中,都定义了下面两个方法 (虽然在这两个类中,这两个方法的方法名,参数列表和返回值类型完全一样,但 Activity
并不是 View
的子类,下面的两个方法在 Activity
和 View
中被单独定义)。
// 尝试将触摸事件交给自己的子视图 (如果有的话) 处理: 调用子视图的 dispatchTouchEvent()
// 或者自己处理: 调用自己的 onTouchEvent() 或 OnTouchListener.onTouch()
// 无论是自己的子视图,还是自己,完成了事件处理,都返回 true
public boolean dispatchTouchEvent(MotionEvent ev)
// 尝试自己处理触摸事件. 如果完成处理 (不需要再交给其他 View 处理), 则返回 true
public boolean onTouchEvent(MotionEvent event)
由于 ViewGroup
是 View
的子类,所以自然 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
就会选择将这个事件“消费”掉,这样其父视图ListView
,PageViewer
以及Activity
就不会重复处理这一事件。
传递机制详解
先分别看看上面的4个方法的具体作用,以及 boolean
返回值的意义。
Activity
先看第一个方法 dispatchTouchEvent()
,该方法是整个触摸传递机制的核心。一般地,父视图 ( parent view ) 通过调用子视图 ( child view ) 的 dispatchTouchEvent()
方法完成触摸事件的向下传递。
焦点所在 Activity
的 dispatchTouchEvent()
方法,是整个触摸事件的“入口”。该方法首先,无条件地,不可被截断地 (除非你 Override Activity
的 dispatchTouchEvent()
方法),将事件交给它的下属处理,即调用该 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
情况最为复杂。我们先看 ViewGroup
的 dispatchTouchEvent()
方法,对于这个方法而言,没有父视图的 Activity
和没有子视图的 View
(下文简称叶子 View
),某种程度上都可以看成 ViewGroup
的特例。
ViewGroup
的 dispatchTouchEvent()
方法被其父视图 (可能是 Activity
的根视图,也可能是其他的 ViewGroup
) 调用,dispatchTouchEvent()
方法所要做的事情就是竭尽所能,利用自己的资源 (包括派遣给自己的子视图) 来处理触摸事件,并向父视图反馈处理结果。
与 Activity
中,首先无条件地将触摸事件派遣给自己的下属,我们可以通过调用 ViewGroup
的 onInterceptTouchEvent()
方法,决定触摸事件是否在此 ViewGroup
处截断。即,如果 onInterceptTouchEvent()
方法返回 true
,则不再继续传递给自己的子视图,而是 ViewGroup
自己尝试处理;如果返回 false
,则不截断 (就像在 Activity
中一样),首先尝试将任务派遣给子视图完成,如果没有子视图或子视图不能完成 (调用子视图的 dispatchTouchEvent()
方法返回 false
),那么 ViewGroup
不得不尝试自己处理触摸事件。逻辑见下图。
ViewGroup
所谓“自己处理”的方法与叶子 View
相同,如果注册了触摸事件监听器 OnTouchLisener
,则优先调用 OnTouchLisener.onTouch()
方法,如果没有注册监听器,或OnTouchLisener.onTouch()
方法返回 false
,再尝试调用 ViewGroup
自身的 onTouchEvent()
方法,且 ViewGroup
的 onTouchEvent()
方法完全继承自 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
均不能完成事件处理,最终会调用 Activity
的 onTouchEvent()
方法,做最后的尝试。整个实际处理过程顺序正好相反,是自下而上的。
从 ACTION_DOWN 到 ACTION_UP
从手指按下,触发 ACTION_DOWN
开始,到手指离开,触发 ACTION_UP
为止,这两次触摸事件,以及这中间到其他触摸事件 (如 MOVE 事件),会被视作一次手势。如果我们对一次手势的 ACTION_DOWN
不感兴趣,即我们在监听器的 onTouch()
方法,或在 View
的 onTouchEvent()
方法中返回 false
。那么我们将不会再接受到该手势到后续触摸事件,直到这一手势结束 (ACTION_UP
)。
另外,在一次完整手势中,只要 ViewGroup
的 onInterceptTouchEvent()
方法有一次返回 true
,那么该 ViewGroup
将会截断这次手势的全部后续触发事件,并向之前处理事件的子视图,传递一个 ACTION_CANCEL
事件。所以我们应当总是注意捕获可能的 ACTION_CANCEL
触摸事件。
总结
在没有 Override 的情况下,触摸事件的派遣将不会被截断,从 Activity
的根视图,自上而下的派遣到叶子 View
,然后调用该 View
的 onTouchEvent()
(如果注册了监听器的话,则优先调用 OnTouchListener.onTouch()
,返回 false
才会再调用 onTouchEvent()
)。如果该 View
不能处理事件(onTouchEvent()
返回了 false
),其父视图继续尝试处理,直到最后,调用 Activity
的 onTouchEvent()
方法。另外,值得注意的是,如果没有响应一个手势的开始事件 (ACTION_DOWN
),则不会接到该手势的后续事件。
Android: 详解触摸事件如何传递的更多相关文章
- MTP in Android详解
MTP in Android详解 最近好长一段时间没有做笔记了,今天主要学习一下MTP相关的知识. MTP的全称是Media Transfer Protocol(媒体传输协议),它是微软公司提出的一套 ...
- 百度地图API详解之事件机制,function“闭包”解决for循环和监听器冲突的问题:
原文:百度地图API详解之事件机制,function"闭包"解决for循环和监听器冲突的问题: 百度地图API详解之事件机制 2011年07月26日 星期二 下午 04:06 和D ...
- [学习总结]4、Android的ViewGroup中事件的传递机制(一)
本文主要针对dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent三个方法,通过简单的例子来简单的介绍下. 根据字面意思的理解,dispatchTo ...
- JAVASCRIPT事件详解-------原生事件基础....
javaScirpt事件详解-原生事件基础(一) 事件 JavaScript与HTML之间的交互是通过事件实现的.事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间,通过监听特定事件的发生,你能 ...
- [学习总结]5、Android的ViewGroup中事件的传递机制(二)
下面是第一篇的连接 Android的ViewGroup中事件的传递机制(一) 关于onInterceptTouchEvent和onTouchEvent的详细解释. 1 public class Mai ...
- Android 手势&触摸事件 MotionEvent
1.http://blog.csdn.net/omg_2012/article/details/7881443 这篇相当好啊 2.http://blog.csdn.net/android_tutor/ ...
- 【C#】详解C#事件
目录结构: contents structure [+] 事件基本介绍 定义事件类型 定义事件成员 定义引发事件的方法 以线程安全的方式引发事件 登记事件关注 揭秘事件 显式实现事件 为什么需要显式实 ...
- Android ViewGroup拦截触摸事件具体解释
前言 在自己定义ViewGroup中.有时候须要实现触摸事件拦截.比方ListView下拉刷新就是典型的触摸事件拦截的样例. 触摸事件拦截就是在触摸事件被parent view拦截,而不会分发给其ch ...
- [iOS UI进阶 - 3.1] 触摸事件的传递
A.事件的产生和传递 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发 ...
随机推荐
- [转帖]win10 .Net Runtime Optimization Service占用大量CPU资源解决方法
win10 .Net Runtime Optimization Service占用大量CPU资源解决方法 https://blog.csdn.net/cwg2552298/article/detail ...
- OneZero第五周第二次站立会议(2016.4.19)
1. 时间: 15:15--15:25 共计10分钟. 2. 成员: X 夏一鸣 * 组长 (博客:http://www.cnblogs.com/xiaym896/), G 郭又铭 (博客:http ...
- Python网络编程:IO多路复用
io多路复用:可以监听多个文件描述符(socket对象)(文件句柄),一旦文件句柄出现变化,即可感知. sk1 = socket.socket() sk1.bind(('127.0.0.1',8001 ...
- linux客户端WinSCP
WinSCP是一个Windows环境下使用SSH的开源图形化SFTP客户端.同时支持SCP协议.它的主要功能就是在本地与远程计算机间安全的复制文件. 这是一个中文版的介绍.从这里链接出去的大多数文 ...
- nginx mp3
location /mp3 { alias "d:/mp3"; default_type audio/mpeg; autoindex on; }
- tomcat Failed creating java C:\Program Files\Java\jre6\bin\client\jvm.dll %1 不是有效的 Win32 应用程序。
jdk版本搞的鬼 请下载64位的jdkj进行安装
- 【枚举Day1】20170529-2枚举算法专题练习 题目
20170529-2枚举算法专题练习 题解: http://www.cnblogs.com/ljc20020730/p/6918360.html 青岛二中日期 序号 题目名称 输入文件名 输出文件名 ...
- 洛谷 P1457 城堡 The Castle 解题报告
P1457 城堡 The Castle 题目描述 我们憨厚的USACO主人公农夫约翰(Farmer John)以无法想象的运气,在他生日那天收到了一份特别的礼物:一张"幸运爱尔兰" ...
- Mac 删除应用卸载后无法正常移除的图标
经常会不通过appstore下载软件,也就是从网页中下载dmg,自己安装,但是当我不再想要这个软件,然后把它卸载掉之后就会发现,launchpad里还是遗留了这个软件的图标,而且删不掉.这个时候,就可 ...
- fidder及Charles使用
1. fidder抓https包的基本配置,可参见以下博文 http://blog.csdn.net/idlear/article/details/50999490 2. 遇到问题:抓包看只有Tunn ...