一、前言:
触摸事件的处理对于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); |
至此,标准模型中事件的传递和消费的代码逻辑就分析完了,知道了这些原理以后,在日常的工作和学习当中,就不会再有陌人摸象的感觉,对于事件的处理,就可以得心应手,甚至改变默认的处理机制,达到一些很神奇的效果。这也是android开源的魅力所在,让我们可以尽情的去研究他的原理,从而灵活应用,达到自己想要的效果。
本文版权归黑马程序员Android+物联网培训学院所有,欢迎转载,转载请注明作者出处。谢谢!作者:黑马程序员Android+物联网培训学院首发:http://android.itheima.com