一、前言:
触摸事件的处理对于android手机来说恐怕是最重要的一个机制了,当你在使用手机时,绝大多事都是通过触摸屏幕来控制手机的。所以把触摸事件搞清楚对于我们理解android系统,开发android应用来说,都有着非常重要的意义。对于一个初学者来说,搞清楚触摸事件的处理机制不是一件简单的事情,本文将触摸事件的讲解分为三步,由浅入深,循序渐进的为读者讲解,希望这遍文章对读者能有所帮助。
二、标准模型:事件的传递和消费
我们都知道android中的view能够响应触摸事件,一般情况下是通过重写该View的onTouchEvent(MotionEvent event)方法来实现的,如果该方法返回true,意思是说当前对象需要消费触摸事件,如果返回false,那就是说当前这个view对象不需要消费触摸事件。那么现在问题来了,看下图当中:如果这张图片需要响应事件,即这个ImageView的onTouchEvent方法返回true,那么事件就由这个ImageView来处理;如果这个图片不需要处理事件,那么事件就交由图片外面的布局来处理,即,去判断布局对象的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是否消费了事件,其实看的是dispatchTouchEvent方法的返回结果,如果没有touchListener 的话,也可以认为是看 onTouchEvent 方法的返回结果。
三、进阶:事件的中断
前面所说的是一个事件传递和消费的标准模型,但这个模型有些简陋,不能适应所有的情况,如下图所示:我们先来考滤一个问题,上面我们已经说过了,当事件发生时,总是父view先收到的事件,然后通过计算将该事件传递给正确的子view,这是一般情况,那么,还有个特殊情况,就是父view拿到事件以后,他改变主意了,他并没有传递给子view,而是中断了事件的正常传递,由自己直接来处理了。对应的代码为:
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } |
还拿之前的爷孙仨分苹果的比喻来说明中断的问题:现在爷爷最先拿到,按正常的处理,将苹果传递给了父亲,而父亲现在正好想吃苹果呢,于是,吧唧一口,把苹果给吃掉了,那这样儿子就收不到这个苹果了。如上图所示:父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)) { |
然后,我们看,是如何找的,继续看:
// 判断 是否要中断事件, 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)) { // 看这个区域是否包含当前触摸点 |
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; } } |
然后调用 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://www.itcast.cn/android/