【大牛课堂】Android系统触摸事件三步曲
传智·华佗
拥有丰富的开发经验和教学经验。精通Java技术,对Android系统框架有深入地研究。参与开源框架phoneGap的开发。讲课内容丰富,通俗易懂,善于将工作中的经验和技巧,结合在教学工作中,深得学员的喜爱。
触摸事件的处理对于Android手机来说恐怕是最重要的一个机制了,当你在使用手机时,基本都是通过触摸屏幕来控制手机的。所以把触摸事件搞清楚对于我们理解Android系统以及开发Android应用来说,都有着非常重要的意义。
对于一个初学者来说,搞清楚触摸事件的处理机制不是一件简单的事情,本文将触摸事件的讲解分为三步,由浅入深、循序渐进地进行讲解。
一
标准模型:事件的传递和消费
我们都知道Android中的View能够响应触摸事件,一般情况下是通过重写该View的onTouchEvent(MotionEvent event)方法来实现的,如果该方法返回true,意思是说当前对象需要消费触摸事件,如果返回false,那就是说当前这个View对象不需要消费触摸事件。那么现在问题来了,看下图:
外框是一个普通的线性布局,布局当中有一个ImageView图片,红色的点是我们触摸的位置,那么这个触摸事件是应由谁来处理呢?我们先来回答一个问题:外面的布局和里面的图片,谁先收到这个触摸事件?答案是外面的布局,事件总是由最外层的布局,一层一层向里面传递,最终传递给了这张图片。
如果这张图片需要响应事件,即这个ImageView的onTouchEvent方法返回true,那么事件就由这个ImageView来处理;如果这个图片不需要处理事件,那么事件就交由图片外面的布局来处理,即去判断布局对象的onTouchEvent方法返回true,还是返回false。
一句话的经验:事件的传递是由外向里一层层传递的,而消费时,是由里向外一层层的判断,最终找到某一个需要处理事件的对象。如下图所示:
记忆小技巧:我们可以将*父View当做爷爷,父View就是父亲,子View就是儿子,而触摸事件就是一个苹果,爷爷拿到一个苹果,给了父亲,父亲又给了儿子,而儿子正好需要这个苹果,就把苹果给吃掉了,即儿子这个对象的onTouchEvent方法返回true,如果儿子现在不想吃苹果,对这个苹果不感兴趣,那么就把这个苹果又还给了父亲,由父亲来判断是否来消费这个苹果,就是看父View中的onTouchEvent方法是返回true还是返回false,如此循环,以次类推。
知识点说明:本文中为了便于理解,判断View是否处理事件,就是根据该View的onTouchEvent方法是返回true,还是返回false。但我们都知道,一个View除了可以重写onTouchEvent方法外,还可以通过设置一个setOnTouchListener 来处理touch事件,那如果两个动作都做了,情况会是如何呢?
看类View中的如下代码:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null
&& mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
这里可以很明显的看出,如果一个View有touchListener对象,同时该对象的onTouch方法返回为true的时候,onTouchEvent方法根本就没有机会执行。
一个View是否消费了事件,其实看的是dispatchTouchEvent方法的返回结果,如果没有touchListener 的话,也可以认为是看onTouchEvent 方法的返回结果。
二
进阶:事件的中断
前面所说的是事件传递和消费的标准模型,但这个模型有些简陋,不能适应所有的情况,如下图所示:
ListView的条目当中有个按钮,点中这个按钮,上下滑动。在此场景中,如果按前面的标准模型来讲,这个事件应由按钮来处理,但此时显然并不是用户的本意,用户并非要真的点击按钮,而是要滑动ListView,事件应该由ListView来处理,那这又是如何实现的呢?
我们先来考虑一个问题,上面我们已经说过了,当事件发生时,总是父View先收到的事件,然后通过计算将该事件传递给正确的子View,这是一般情况,那么,还有个特殊情况,就是父View拿到事件以后,他改变主意了,他并没有传递给子View,而是中断了事件的正常传递,由自己直接来处理了。对应的代码为:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
这个方法默认情况下返回false,意思就是:并不中断事件的传递,按标准模型进行,但如果某个ViewGroup重写该方法,并返回true,就意味着,当事件传递到该ViewGroup时,中断了事件的正常传递,由当前这个ViewGroup直接来处理该事件。于是我们可以将Touch事件的流程图改进如下:
任何一个父View都有能力中断事件的正常传递,如果所有的父View都没有中断事件的正常传递,那么和前面的标准模型是一样的,如果某个父View收到事件后,将事件中断了,那么,就由当前这个父View直接来处理该事件。
还拿之前的爷孙三人分苹果的比喻来说明中断的问题:现在爷爷最先拿到,按正常的处理,将苹果传递给了父亲,而父亲现在正好想吃苹果呢,于是,吧唧一口,把苹果给吃掉了,那这样儿子就收不到这个苹果了。如上图所示:父View的 onInterceptTouchEvent方法返回true,那么触摸事件直接接收父View的onTouchEvent来处理,而后的操作和标准模型就一样了。
三
终极必杀:事件传递机制的代码分析
知道了事件的传递、中断、消费以后,普通的开发工作就能够满足了,如果你对技术的追求永无止境的话,那么我们再来进行更深一层的研究。在标准模型中,我们在讲解事件的传递和消费时,都是用文字和图表来说明的,其实我们都知道,这些机制肯定有对应的、可执行的代码。这些代码就在类ViewGroup中的dispatchTouchEvent方法,(我们以Android2.3的源码来讲解)
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction(); // 获得触摸的动作类型
final float xf = ev.getX(); // 获得触摸点的X坐标
final float yf = ev.getY(); // 获得触摸点的Y坐标
final Rect frame = mTempRect; // 获得一个临时需要的矩形
// 判断标记位,一般情况下为 true
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {// 如果是down 事件,判断点中的目标是谁
if (mMotionTarget != null) { // 如果之前有目标,那么清空目标
mMotionTarget = null;
}
// 判断 是否要中断事件,
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
首先,做一些准备性的工作,获得触摸点的X、Y坐标等。如果当前是down事件,那么就判断当前点击的目标是谁,每一个父View都有一个自己的目标,这些目标串起来,像链条一样,直接指向最终消费事件的对象。在这里调用onInterceptTouchEvent,默认返回的是false,意思是不中断。若没有中断,那就应该找一下目标是哪个;如果中断了,就不用找了,就由自己来处理事件了。
然后,我们继续看,是如何找的:
// 判断 是否要中断事件,
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
final int scrolledXInt = (int) scrolledXFloat; // X坐标点
final int scrolledYInt = (int) scrolledYFloat; // Y坐标点
final View[] children = mChildren; // 获得当前所有的子View
final int count = mChildrenCount; // 当前子View的数量,也就是这个数组的长度
for (int i = count - 1; i >= 0; i--) { // 遍历所有的子View
final View child = children[i]; // 获得其中一个子View
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ) { // 这个View是否可见
child.getHitRect(frame); // 获得这个View的矩形区域
if (frame.contains(scrolledXInt, scrolledYInt)) { // 看这个区域是否包含当前触摸点
通过这段代码我们可以看出,父View查找子View是通过for循环获得每一个子View的位置,然后,判断这个位置是否包含了触摸点的坐标,如果包含了,就是说,点中了这个子View,通过标准模型我们知道,下一步就该将这个事件传递给子View,由子View来处理:
if (frame.contains(scrolledXInt, scrolledYInt)) { // 看这个区域是否包含当前触摸点
final float xc = scrolledXFloat - child.mLeft; // 对X坐标进行换算
final float yc = scrolledYFloat - child.mTop; // 对Y坐标进行换算
ev.setLocation(xc, yc); // 将新坐标设置给 MotionEvent 对象
if (child.dispatchTouchEvent(ev)) { // 将这个事件,交由子View进行处理
mMotionTarget = child;
return true;
}
}
如果点中了当前子View,首先将event的坐标进行换算,以保证我们在处理touch时用,event.getX()方法获得的X坐标,是以前这个View的左上角为原点的坐标。其中child.mLeft是子View在父View中左边界的距离,child.mTop是子View在父View中上边界的距离。
然后调用if(child.dispatchTouchEvent(ev))语句,将事件传递给子View,此时,这个child可能是一个布局,也可能只是一个普通的View。如果是一个布局,那么我们在上面所分析的代码,会在这个child布局中再一次被执行,如此嵌套执行;如果这个child不是布局,比如说是一个ImageView或TextView,那么,会去执行这个View的dispatchTouchEvent方法,判断该View是否消费事件,该方法在标准模型中已经有介绍。如果此时child.dispatchTouchEvent返回值是true,即消费事件,那么当前这个ViewGroup就有了目标,就是当前这个child。同样,当前ViewGroup的父View也有目标,就是当前这个ViewGroup,如果循环,我们就知道了,要消费事件的目标是谁。
也就是说:在down事件发生时,系统会确定点击的目标是谁,一但确定了目标,当move事件发生时,系统会直接将事件交给目标来执行:
// 将坐标换算成点击目标的坐标
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
至此,标准模型中事件的传递和消费的代码逻辑就分析完了,知道了这些原理以后,在日常的工作和学习当中,就不会再有盲人摸象的感觉,对于事件的处理,就可以得心应手,甚至能改变默认的处理机制,达到一些很神奇的效果。
欢迎大家添加播妞微信哦!
∨阅读原文 学习路上不孤单,加入程序员交流群!