研究了一下android的touch事件,从doc到google,算是有了一些初步的理解。以下是经过消化的个人理解,有可能与事实不符,欢迎指正。
首先,来了解一下android的事件机制。android的基本元事件我猜应该有5种,理由是MotionEvent类里有5个事件常量,分别是ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL和ACTION_OUTSIDE。其中DOWN/MOVE/UP是人为触发的,CANCEL是系统触发,至于OUTSIDE,doc里写是当事件发生在UI元素之外的时候出发,但实际上我还从来没有成功触发过这个事件。也许你会觉得按钮按下,然后移出按钮边界会触发这个事件,但很遗憾实际上不会。
也就是说,其实我们能够用到的元事件只有三种,DOWN/MOVE/UP,就像三原色调配出多彩的世界,android其余的复杂事件都是由这三个元事件组合而成的。比如scroll(滚动)/fling(就是DOWN然后快速的MOVE一小段距离然后UP)/longpress(长按)/singletapup(单击)等等……
那么系统是怎么响应这些事件的呢?响应事件的方法有几种。
最简单也是最常用的是实现一个OnClickListener的接口,然后用view的setOnClickListener方法绑定这个接口,就可以处理view的单击事件。这种方法简便易行,但是只能处理单击这一种事件。实际上,click这个事件是顺序触发ACTION_DOWN和ACTION_UP两个事件组成的,中间可以存在ACTION_MOVE,但不能MOVE出view的边界,不然即使再MOVE回来也不能触发click事件了。OnClickListener接口的onClick方法是在click事件的ACTION_UP的timing执行。onClick方法只有一个参数,就是被单击的View,这种方法是不能获取单击事件的,也就无法获取单击点的坐标等属性。
第二种略微复杂一点的事件处理方法是用OnTouchListener接口而不是OnClickListener,实现方法跟上面一种差不多,实现这个接口的onTouch(View v, MotionEvent e)方法之后,就可以响应touch的元事件了,注意,是元事件!也就是说,一次简单的click事件会在DOWN和UP的时候分别执行onTouch方法各一次(如果是scroll一下会触发一次DOWN和很多次的MOVE)。同时,由于有了MotionEvent的参数,我们可以用e.getAction()来获取元事件类型或者e.getX()/e.getY()来获取事件发生点的坐标,以及许多其他的事件属性。这样,能干的事情就多那么一点点了。这个方法还有一个boolean的返回值,至于这个返回值是做什么用的,一会儿再交代。
第三种是连接口都用不着,而是直接覆写一个现成的方法view.onTouchEvent(MotionEvent e),这个方法和onTouch方法的不同,直观上来讲,没有view参数了。那这个方法不就没有onTouch方法强大了?那要它有啥用呢?这就要从android的事件传播机制讲起了。
android的事件传播机制跟网页W3C的标准有一点点类似,都是一个事件产生之后经过一个由上到下的捕捉过程再经过一个由下到上的冒泡过程。但是不同的是,android事件在传播过程中的某一层如果被消费了,就会终止传播。也就是说,发生在一个按钮上的ACTION_DOWN事件实际上是先发生在他的父父父父控件(某一个ViewGroup)上然后再层层传过来的,按钮如果消费这个事件,那么传播终止(如果他爸爸消费了这个事件,那么按钮就悲催地根本获取不到这个事件),如果它不消费这个事件,那么这个事件又会冒泡回去,过程大致是这样。
具体来讲,android的事件产生以后,捕捉过程是由ViewGroup.onIntercepTouchEvent(MotionEvent e)传递的,即产生之后,由上向下,依次传递给子ViewGroup的onInterceptTouchEvent方法。这个方法有个boolean的返回值,false表示本人不消费这个事件,这个事件继续传给我儿子。true表示本人留下这个事件啦,捕捉过程到此为止,不传给儿子了。儿子的onInterceptTouchEvent方法就不会再执行了。注意,这个方法只有在ViewGroup里有,而最孙子的View里是没有的,因为到View为止,捕捉过程一定会结束。
那么,捕捉过程终止过后,冒泡过程是由谁来处理的呢?答案就是onTouchEvent方法。当捕捉过程达到最孙子的View(我知道应该叫叶子View……),或者某一层的ViewGroup的onInterceptTouchEvent返回值为true的时候,该View或者该ViewGroup的onTouchEvent方法就会执行,这个方法同样会返回一个boolean,false代表我不消费这个事件,这个事件冒泡回去孝敬我爸爸,true表示我消费啦,事件传播就此终止。而冒泡回去的事件,就由上一层的onTouchEvent来处理。这就是说,网页的事件是捕捉冒泡走一条线,并且默认不会被消费而阻断的(关于网页事件的流程,感谢stauren提供技术支持),而android的事件则是捕捉冒泡分两个方法线来完成,一旦某一个节点消费了这个事件,事件就停止传播了。
写到这里,有人可能会发现一个问题。什么叫“事件被消费了”?不就是事件被处理了么?如果我把事件处理的方法写在onInterceptTouchEvent/onTouchEvent里,但是返回值却是false,那么不就做到,既处理了事件,又没有停止事件传播,不就跟网页一样了么?(这种需求实际工作中非常有用!)
愿望是好的,但实际操作一下,你就会发现,除了ACTION_DOWN事件以外,其余的事件你都只有返回值是true的那个方法才能捕捉的到。这是为什么呢?为什么DOWN事件又可以呢?这是因为,android的事件传播只有在第一个事件发生的时候(所有事件第一个发生都是DOWN,你得先把手按上去嘛……)按前述顺序进行一次,找到那个返回值是true的方法,然后,所有后续事件都会直接传给那个View,不再经历中间的传播过程!所以,中间路径上的那些返回false的方法,只能捕捉到每次的第一个DOWN事件,而后续的MOVE和UP事件就捕捉不到了。
那么有没有办法能够让消费之后传播过程继续呢?有,只要你在返回true的方法里人为调用上一层的onTouchEvent或者下一层的onInterceptTouchEvent,传播不就进行下去了么?
最后,处理复杂的三元事件的组合事件,android提供了一个GestureDetector类,实现GestureDetector.GestureListener接口之后(就是各种onLongPress/onFling/onScroll等方法)并实例化一个GestureDetector对象之后,就可以在本来要处理事件的onTouchEvent方法里人工调用这个detector实例自带的onTouchEvent方法,改由detector来处理事件,于是各种复杂事件就能被识别和处理了。