【大牛课堂】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);

至此,标准模型中事件的传递和消费的代码逻辑就分析完了,知道了这些原理以后,在日常的工作和学习当中,就不会再有盲人摸象的感觉,对于事件的处理,就可以得心应手,甚至能改变默认的处理机制,达到一些很神奇的效果。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值