本文出自http://blog.csdn.net/zhaizu/article/details/50489398,转载请注明出处。
本文简单介绍安卓应用层的事件分发机制,并辅以案例进行分析。
视频版教程:http://v.youku.com/v_show/id_XMTY5MjczMjE3Ng==.html。
授人以鱼不如授人以渔。
安卓系统源码是深入学习安卓开发的首选资料,原汁原味,营养丰富。
而且大部分源码带有注释,具有很强的可读性,你只需要战胜的自己的畏惧心理然后 read the fucking source code。
关于阅读源码,我有两个小建议:
1. 选择合适的源代码版本
从 2008 年 9 月份发布的 1.0 版,到 2015 年 10 月份的 6.0,部分源码发生了很大变化。
以应用层的事件分发机制为例,2.3.3 版本的源码仅仅处理单点触控,而在 6.0 版本则具有完善的多点触控。相应的,后者的代码比前者更为复杂,但思路是一脉相承的。
所以,如果首次学习事件分发机制,建议从低版本,如 2.2 或 2.3 版本开始,先了解单点触控触控的流程,然后再过度到最新版本的多点触控,这样学习曲线相对平滑,降低难度。
阅读 2.x 版本的源码,可以参考郭霖大牛的博客《 Android事件分发机制完全解析,带你从源码的角度彻底理解(下)》;更高版本的源码,可以参考这两篇的注释:《Android Touch事件传递机制全面解析(从WMS到View树)》 和 《Android事件传递之子View和父View的那点事》。
2. 单步调试的重要性
学习源代码,仅仅通过阅读和思考是远远不够的,而且容易陷入死胡同。动手尝试是不可或缺的。
在 OnTouchLisnter.onTouch() 和 OnClickListener.onClick() 方法中打印日志,观察日志输出的时机和顺序,这是一种很直观的方法。
单步调试也是很有效的尝试方法,通过观察不同案例下(如设置监听事件和未设置监听事件)源码的执行路线和中间变量的赋值,我们往往有种茅塞顿开、豁然开朗的感觉。阅读和调试,一静一动,相得益彰。
为了避免发生断点失效或执行顺序混乱等诡异情况,我们要做到如下两点:
其实,知道以上两点之后,大家完全自己去单步调试了。
以下内容都是基于本人阅读源码和单步调试和日常经验总结得出的。
先来看个效果图:
<FrameLayout>
<ImageView />
<TextView />
FrameLayout>
父View 和子 View 是以“叠罗汉”的方式放在一起的,就像 HTML 里的 CSS(Cascading Style Sheet,层叠样式表)一样。二者通过“父子”关系联系起来,事件也是通过这种联系顺藤摸瓜来寻找“目标”View 的,处于上层的子 View 后于父 View 接收事件,但先于父 View 处理事件(如果愿意消费的话)。
本文基于安卓 SDK 23 版本源代码。
View.java 和 ViewGroup.java 中与事件分发相关的几个方法:
继承关系如下:
“手势”的定义:以 ACTION_DOWN 开始,以 ACTION_UP 结束的一连串触摸事件;触摸事件的类型可以分为:
ACTION_POINTER_UP
其中,带 POINTER 的类型是多点触控特有的,我们可以认为一个 pointer 就是一个手指。
以下只讨论单点触控。
本文的 demo 布局是一个 RelativeLayout(后面会用 ScrollView 代替),里面有两个 TextView。我们假设红色的 TextView 上绑定了一个 OnClickListener。在我们自己的布局外面,系统还会再套几层布局,也就是虚线以上的部分。由于尺寸原因,后面的事件消费示意图中我们将省略部分系统层级。
这里解释一下 mFirstTouchTarget 这个成员变量。
每个 ViewGroup 实例中都有 mFirstTouchTarget。mFirstTouchTarget 的类型是 TouchTarget,TouchTarget 是一个链表节点的数据结构,每个 TouchTarget 实例里面封装着 mFirstTouchTarget 变量所在实例的一个子 View(也可能是 ViewGroup,以后统称为子 View),表示能接收事件的子 View。
当打头阵的 DOWN(以下将 ACTION_DOWN 简写为 DOWN,对 ACTION_MOVE、ACTION_UP、ACTION_CANCEL 做同样处理) 事件被某个子 View 消费时,mFirstTouchTarget 就指向该子 View,然后后续事件(MOVE / UP)到来时就直奔该子 View 供其消费;而当没有子 View 消费 DOWN 事件时,后续事件到来时,顶层的 DecorView.mFirstTouchTarget 为 null,DecorView 就直接调用 super.dispatchTouchEvent(event) 处理它们,而不再下发了。
单点触控时,mFirstTouchTarget 指向的链表最多只有一个节点;多点触控时可能会有多于一个节点。
关于 DOWN 和 MOVE / UP 事件执行过程,可参见下面代码的注释。
// more code
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// DOWN
handled = true;
} else {
// MOVE and UP
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;
}
}
// more code
DOWN 之后发生了什么?
DOWN 事件是手势的开始,好似一个侦察兵,任务是检查有没有子 View 愿意消费它及后续事件,如果有做好标记(TouchTarget)。
标记的作用是后续事件到来时,就不用再遍历寻找被击中的子 View 了,而是通过标记直接找到该子 View。
MOVE / UP 之后发生了什么?
后续事件直接交给每个 ViewGroup 的 mFirstTouchTarget 里面的子 View,调用子 View 的 dispatchTouchEvent() 方法,直到红色的 TextView 消费了这些事件。
假设所有的 ViewGroup 或 View 上都没有绑定监听事件(或者绑定了 OnTouchListener,但是其 onTouch() 返回 false),红色 TextView 上也没有任何监听事件。DOWN 事件一直被各级 ViewGroup 谦虚的传递到最底层的 TextView,这时连 TextView 也不愿消费之,于是其 dispatchTouchEvent() 返回false,然后 TextView 的父 View 接着执行自己的 dispatchTouchEvent(),走到 OnTouchListener.onTouch() 和 onTouchEvent() 里面,当然这两个都返回 false;然后 TextView 的父 View 的 dispatchTouchEvent() 方法执行完毕,返回到了 TextView 的父 View 的父 View 的dispatchTouchEvent() 方法中,然后执行其 OnTouchListener.onTouch() 和 onTouchEvent() 里面……
最后,各个层级的 mFirstTouchTarget = null,ViewGroup.dispatchTransformedTouchEvent() 方法的 child 参数为 null,每个 ViewGroup 都被当做 View 处理,从最底层开始,依次调用 View.dispatchTouchEvent() 方法(最终调用是 View.onTouchEvent() 方法),直至 Activity.dispatchTouchEvent()。
对于后续的事件,由于 DecorView.mFirstTouchTarget = null,调用dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS),被 DecorView 的父类即 View.dispatchTouchEvent() 方法消费。
我们把 RelativeLayout 换成可以上下滑动的 ScrollView,其子 View 是一个 TextView,TextView 绑定了监听事件。
如果给 TextView 的背景设置了按压态,我们按住 TextView 不松手也不滑动时,我们能看到背景变色,说明 DOWN 确实被 TextView 消费了。
仍然保持手指的按压状态,然后上下滑动,ScrollView 开始滚动,说明 MOVE 被 TextView 的父 View 即 ScrollView 消费了,这时 TextView 会接收到 CANCEL 事件。
从上面的分析可以看出,不管有没有设置监听事件,每次点击,都会触发从 View 树的自上而下的单路径遍历,层级越多遍历耗时越多,响应时间越大,用户体验越差。从这个角度,也可以说明布局层级优化的重要性。关于布局优化,请移步《 布局优化技巧笔记》。
还说明,没事别闲的蛋疼在屏幕上乱摸乱点,虽然屏幕没什么反应,但是屏幕后面是有代码在空跑的,当然也在耗电。
如果给某个 View 同时绑定 OnClickListener,OnTouchListener,TouchDelegate(关于 mTouchDelegate 的使用参见《用 TouchDelegate 扩大子 View 的点击区域》)三个事件(简直丧心病狂),那么它们的执行顺序是怎样的呢?
这时我们需要查看 View.dispatchTouchEvent() 方法,看里面是怎么处理这些监听事件的:
public boolean dispatchTouchEvent(MotionEvent event) {
// some code
if (onFilterTouchEventForSecurity(event)) {
//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;
}
}
// some code
return result;
}
public boolean onTouchEvent(MotionEvent event) {
// some code
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// some code
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)) {
performClick();
}
}
}
// more code
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
// more code
}
我们可以看到,现在 View.dispatchTouchEvent() 方法中执行了 mOnTouchListener,然后进入 View.onTouchEvent() 方法,依次执行了 mTouchDelegate.onTouchEvent(event) 和 performClick(),后者执行了 OnClickListener.onClick(this)。
所以,三者的执行顺序为: OnTouchListener,TouchDelegate,OnClickListener。
同时,我们应当注意到在 View.onTouchEvent() 方法的注释:
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
即,同时满足 disabled 和 clickable = true 的 View 会消费触摸事件,但不会有任何反应。
回头看下这个例子:
<FrameLayout>
<ImageView />
<TextView />
FrameLayout>
需求:点击图片和数字具有相同的响应点击事件(如页面跳转等)。
这种情况下,只给 FrameLayout 绑定监听事件即可,虽然 DOWN 事件会下发到 ImageView 和 TextView,询问它们是否愿意消费,被拒绝后 DOWN 事件向上传递,依次经过 ImageView或TextView(具体是谁取决于点击坐标落在哪个的区域范围内) 和 FrameLayout 的 onTouchEvent() 方法,最后被 FrameLayout.onTouchEvent() 消费。后续事件将直接被 FrameLayout.onTouchEvent() 消费,而不再下发;
如果能继承 FrameLayout(假设为 MyFrameLayout),并重写 MyFrameLayout 的 onInterceptTouchEvent() 方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
这样 MyFrameLayout 直接拦截点击事件进行消费,不再下发给 ImageVIew 和 TextView,此时的事件消费过程为:
如果不在 FrameLayout 上绑定,而是给 ImageView 和 TextView 绑定,所有的事件都会经过 FrameLayout 继续下发给ImageView 或 TextView,方法调用层数增加,相应时间延长;
如果在 FrameLayout、ImageView 和 TextView 上同时绑定事件,则根据事件的落点进行判断。例如,落在 ImageView 范围内的事件(当然也落在了 FrameLayout 范围内)只触发 ImageView 的事件;落在 FrameLayout 范围内但是既未落在 TextView 范围内也未落在 ImageView 范围内的事件,才会由 FrameLayout 进行消费。这种效果与我们的直观认识一致的。
这是一个经典案例(虽然在实际开发中很少见到),通过重写 ViewGroup.onInterceptTouchEvent() 方法和 ViewGroup.onTouchEvent() 方法来控制事件的分发过程,详见该博客《Android事件分发机制练习—打造属于自己的瀑布流》。
请移步《TextView ClickableSpan 事件触发的坑》。
本文出自http://blog.csdn.net/zhaizu/article/details/50489398,转载请注明出处。