Android的Touch事件处理机制详解

Android的Touch事件是有ACTION_DOWN, ACTION_MOVE,ACTIOB_UP,ACTION_CANCEL(由系统产生)。且Touch事件的处理是以组为单位的。一组touch事件一定是以ACTION_DOWN开始,ACTION_UP结束。中间可以有0至若干次ACTION_MOVE。

处理Touch事件的对象 就是activity中的View对象,在这里定义为视图元素,可以分为两类:ViewGroup(其内部可以再包含子视图元素:ViewGroup或View),View(不能包含子视图元素)。ViewGroup实际上是继承自View类的。新增加了一个方法onInterceptTouchEvent方法。所以处理Touch事件涉及到的方法如下: View类:

public boolean dispatchTouchEvent(MotionEvent event) public boolean onTouchEvent(MotionEvent event)

ViewGroup类: public boolean dispatchTouchEvent(MotionEvent event) public boolean onInterceptTouchEvent(MotionEvent event) public boolean onTouchEvent(MotionEvent event)

要弄清楚Android的 Touch事件,涉及到以下几种情况:

1. 事件的传递顺序是怎样的?是从最外层的父视图开始传递, 一层一层传给子视图元素呢 还是由内而外传递。

2. 在一个视图元素中, 其内部的处理事件的3个方法之间是怎样的关系?顺序调用?内部嵌套?互斥型调用?

3. 事件在什么情况下终止传递?

4. 添加的各种监听器(如添加了onTouchListener,onClickListener, onLongClickListener) 又会如何影响事件的传递, 以及监听器中的回调方法与这3个touch事件处理方法的调用顺序是怎样的?

为了加快大家的理解, 我先把结论在这说明出来。  下面再通过代码 验证。

1. 事件的传递顺序是从最外层开始, 一层一层往内传给子视图的。

2. 实际上 事件的处理归根结底是由dispatchTouchEvent方法处理的。 只不过dispatchTouchEvent方法内部由调用onInterceptTouchEvent和onTouchEvent方法。伪代码如下:

 
 
  1. public void dispatchTouchEvent() {
  2. if (!onInterceptTouchEvent()) { // 判断自己是否拦截掉信号,
  3. View[] getChilds();
  4. for (View view: childs) {
  5. inMyRange(view,location)
  6. if (child.dispatchTouchEvent()==true) {
  7. return true; // 只有dispatch返回true,表示消费了事件就立马return,事件不再传递了
  8. } // dispatch返回false, 则还会传递给下一个或跳转到下面的
  9. }
  10. }
  11. // 如果onInterceptTouchEvent()返回true,也就是拦截了, 则调用自己的处理逻辑
  12. if (mListener!=null && mListener.onTouch()==true....) {
  13. retuen true;
  14. }
  15. onTouchEvent();
  16. }

如果dispatchTouchEvent方法返回true,则该事件被消费了。 会传递后续的ACTION_MOVE, ACTION_UP事件。事件的传递

3. 一旦ACTION_DOWN被某个视图元素 view对象 给消费了(返回true),则后续信号不会再做命中测试, 直接从最外层开始一层层传递,直接传递到消费了ACTION_DOWN事件的view对象。 即如果down事件的传递轨迹为: 爷爷—> 爸爸 –> 儿子 –> 爸爸(爸爸消费了,返回true), 则后续的事件传递轨迹为: 爷爷 —> 爸爸 (处理事件)

4. 如果添加了各种监听器, 如OnTouchEventListener, OnCLickListener, onLongClickListener。则有如下机制: a)设置了OnTouchListener监听器的view对象的onTouchEvent方法的调用, 会受到监听器的回调方法onTouch方法的左右, 如果onTouch方法返回true, 则表示事件被消费,onTouchEvent不会执行。 如果onTouch方法返回false,事件再传给onTouchEvent方法执行。b)如果设置了OnClickListener监听器, 则监听器的回调方法onClick方法 会在ACTION_UP信号传递过来时执行。 且ACTION_MOVE,ACTION_UP一定会被系统接收,传递。不再受到ACTION_DOWN的处理结果必须为true的限制。 c) 如果设置了OnLongClickListener监听器, 则监听器的回调方法onLongClick方法 会在ACTION_DOWN信号传递过来后一段时间(2s)后执行。其返回值 影响其他onClickListener的回调方法的执行与否。 如果onLongClick方法返回true, 表示点击事件已经处理完毕,OnClickListener不用再处理了。如果onLongClick方法返回false,则OnClickListener的onClick方法会得到执行。 onClickListener 和OnLongClickListener的不会破坏前面1,2,3 和4的a)原则。

 图0

下面通过实验来探究上面的几种情况的结论:

实验中,定义3个类, GrandpaFrameLayout继承子FrameLayout类, FatherLinearLayout继承自LinearLayout类,ChildTextView继承自TextView。分别在各自的事件处理方法中增加了log输出。 通过log输出顺序来呈现事件的传递路径

源码

  
  
  1. public class GrandpaFrameLayout extends FrameLayout {
  2. private static final String[] ACTIONS = { "DOWN", "UP", "MOVE", "CANCEL" };
  3. public GrandpaFrameLayout(Context context, AttributeSet attrs) {
  4. super(context, attrs);
  5. }
  6. @Override
  7. public boolean dispatchTouchEvent(MotionEvent event) {
  8. // 输出形式: Grandpa dispatchTouchEvent handles DOWN
  9. // 哪个视图元素 + 哪个方法 handles + 哪个事件
  10. System.out.println(getTag() + " dispatchTouchEvent handles "
  11. + ACTIONS[event.getAction()]);
  12. return super.dispatchTouchEvent(event);
  13. }
  14. @Override
  15. public boolean onInterceptTouchEvent(MotionEvent event) {
  16. // TODO Auto-generated method stub
  17. System.out.println(getTag() + " onInterceptTouchEvent handles"
  18. + ACTIONS[event.getAction()]);
  19. return super.onInterceptTouchEvent(event);
  20. }
  21. @Override
  22. public boolean onTouchEvent(MotionEvent event) {
  23. // TODO Auto-generated method stub
  24. System.out.println(getTag() + " onTouchEvent handles"
  25. + ACTIONS[event.getAction()]);
  26. return super.onTouchEvent(event);
  27. }
  28. }
---------------------------------------------------------------------------------------------------------------------------------------------
 
 
  1. public class ChildTextView extends TextView {
  2. private static final String[] ACTIONS = { "DOWN", "UP", "MOVE", "CANCEL" };
  3. public ChildTextView(Context context, AttributeSet attrs) {
  4. super(context, attrs);
  5. }
  6. @Override
  7. public boolean dispatchTouchEvent(MotionEvent event) {
  8. // TODO Auto-generated method stub
  9. System.out.println(getTag() + " dispatchTouchEvent handles"
  10. + ACTIONS[event.getAction()]);
  11. return super.dispatchTouchEvent(event);
  12. }
  13. @Override
  14. public boolean onTouchEvent(MotionEvent event) {
  15. // TODO Auto-generated method stub
  16. System.out.println(getTag() + " onTouchEvent called");
  17. return super.onTouchEvent(event);
  18. }
  19. }
 
 
  1. 布局文件: 通过给各自添加tag属性, 来方便判断是哪个视图元素。
 
 
  1. <com.example.toucheventtest.GrandpaFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:id="@+id/grandpa"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:tag="Grandpa"
  6. android:background="#ff888877"
  7. >
  8. <!-- <com.example.toucheventtest.GrandpaFrameLayout > -->
  9. <com.example.toucheventtest.FatherLinearLayout
  10. android:id="@+id/uncle"
  11. android:layout_width="400dp"
  12. android:layout_height="200dp"
  13. android:background="#ff00ff00"
  14. android:tag="Uncle" />
  15. <com.example.toucheventtest.FatherLinearLayout
  16. android:id="@+id/father"
  17. android:layout_width="300dp"
  18. android:layout_height="300dp"
  19. android:background="#ffff0000"
  20. android:tag="Father">
  21. <com.example.toucheventtest.ChildTextView
  22. android:id="@+id/child"
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:background="#ffffffff"
  26. android:tag="child"
  27. android:text="I am a child" />
  28. </com.example.toucheventtest.FatherLinearLayout>
  29. </com.example.toucheventtest.GrandpaFrameLayout>
 
 
  1. 界面如下:

点击 I am a child log输出结果如下:

 图1

如果点击绿色区域,log输出如下:

 图2

从图1,图2 结果来看, 事件的传递是先从父View开始,且会做一次 点击区域归属地判断,只会传给包含了点击区域的子view。

结论1: 事件传递是 从外层往内层传递的。

调用dispatchTouchEvent方法,再调用onInterceptTouchEvent方法,然后向下传递给最上层的子View的dispatchTouchEvent方法,再调用子view的onTouchEvent方法,再从子view传给父view的onTouchEvent方法。

如果我们修改FatherLinearLayout的onInterceptTouchEvent方法,让返回值为true。再看运行结果

图3

因此从两次结果图1, 图3的对比来看 我们可以得出如下结论:

onIntercept方法决定事件是否向下传递,如果返回true,则事件不再向下传递了。

再做实验, 修改FatherLinearLayout的 onTouchEvent方法, 让返回值为true,log结果如下:

 图4

如果点击绿色区域。即只属于grandpa和uncle的区域时 log输出结果为:

 图5

从图3 与图4,图5 对比来看, onTouchEvent方法决定事件是否向右(点击区域有2个或以上同一层级的view对象时)和向上传递。 返回true, 事件不再传递了, 返回false才会继续传递。

如果FatherLinearLayout 的onInterceptTouchEvent方法返回false, 且点击绿色区域,并移动。结果如下:

 图6

从图5, 图6结果对比来看。 如果事件没有被被任何一个方法返回true。 则后续的ACTION_MOVE, ACTION_DOWN,不会被系统传递。即忽略后续的touch信号。

我们给uncle添加事件监听器OnTouchListener。设置监听器的代码部分:

代码:
  
  
  1. FatherLinearLayout uncle = (FatherLinearLayout) findViewById(R.id.uncle);
  2. uncle.setOnTouchListener(new OnTouchListener() {
  3. @Override
  4. public boolean onTouch(View v, MotionEvent event) {
  5. // TODO Auto-generated method stub
  6. System.out.println("uncle onTouchListener onTouch handles " + ACTIONS[event.getAction()]);
  7. return false;
  8. }
  9. });

在绿色区域点击,即uncle和grandpa都占有的区域。结果:

 图7

如果修改onTouch的返回值为true, 结果为:

  图8

图7,8验证了结论4. a)

再给uncle添加OnClickListener。代码如下:

 
 
  1. uncle.setOnClickListener(new OnClickListener(){
  2. @Override
  3. public void onClick(View v) {
  4. System.out.println("uncle onClickListener onClick handles ");
  5. }
  6. });

log输出结果如下:

 图9

将onTouch方法的返回值修改为false, log输出结果如下:

 图10

从图9, 图10对比 可验证结论:4.b)的结论。

再添加OnLongClickListener, 且onLongClick方法返回true, 让OnTouchListener返回false,log输出结果如下:

  图11

更改onLongClick方法返回值为false, log输出结果如下:

  图12

从图11, 图12对比来看,  onLongClick的返回值 会影响onClickListener的回调方法onClick是否会被执行。true表示 其他的点击监听器不需要处理了。false表示其他点击监听器要处理。 验证了结论4. c)

总结: 用一个生动的比喻来概括:有一个慈善家,家里面有食物大礼包,大礼包里的食物有3样食物:面包, 牛奶, 鸡蛋(对应ACTION_DOWN,ACTION_MOVE,ACTION_UP),或者是面包和鸡蛋(对应ACTION_DOWN,ACTION_MOVE,ACTION_UP) 慈善家呢来到一户人家。这户人家有爷爷, 爸爸, 孙子3代。中国礼仪之邦,都是先给长辈。

1. 爷爷收到了一个面包, 肚子饿吗?(onInterceptTouchEvent方法是否返回true),如果饿,跳转到第步。如果不饿(返回false),把面包给儿子,跳转到第 步

2. 爸爸收到面包,也是同样的处理逻辑。

3. 孙子吃面包(执行方法onTouchEvent)。把面包吃完了(对应的是返回true), 慈善家一看食物吃完了,就又从食物大礼包中把牛奶,鸡蛋依次拿过来给爷爷。爷爷,就直接给爸爸, 爸爸直接给孙子。孙子继续吃。

4. 孙子吃面包 还有剩, 就把剩下的面包给爸爸 , 跟老爸说。把我吃撑了, 那大礼包里剩下的食物你别给我了。

5. 爸爸吃面包(执行方法onTouchEvent)。把面包吃完了(对应的是返回true), 那慈善家就会继续将食物大礼包中的牛奶拿过来。给爷爷。爷爷由于之前已经知道了自己不饿。就直接传给自己的儿子:爸爸。爸爸由于收到儿子说别拿食物来了的话, 就不再把牛奶,鸡蛋再给孙子了。 于是自己吃。

4. 爸爸吃面包(执行方法onTouchEvent)。面包没吃完(对应的是返回false), 又把剩下的面包给自己的父亲:爷爷。 也对他说:爸爸, 我吃撑了。爷爷觉得浪费粮食可耻, 虽然不饿, 但能吃点。 于是就也吃食物。

5. 爷爷吃面包(执行方法onTouchEvent)。把面包吃完了(对应的是返回true), 那慈善家就会继续将食物搭理包中的牛奶拿过来。给爷爷, 爷爷如果最后传给了最小的孙子,孙子在发育期,不管三七二十一, 有食物 我就吃,onTouchEvent()但是呢 会有两种情况嘛, 食物吃完了,也就是onTouchEvent返回true。OK,后面的牛奶,鸡蛋都直接从长辈手中传给我吧。可能还没吃饱呢。如果onTouchEvent返回false, 食物还有剩啊。不能浪费,我给我爸爸吧。爸爸看儿子给的。 说明儿子现在很饱嘛, 好,那我吃。 所以后面的牛奶,鸡蛋啊,都会经过长辈的手传递给爸爸, 因为爸爸之前有知道儿子吃饱了嘛, 根本不用再往下给儿子了。 牛奶, 鸡蛋都在爸爸这里处理。如果爸爸吃了食物,还有剩,onTouchEvent返回false, 那爸爸就给爷爷。 爷爷就吃。 如果爷爷吃完了 后面的鸡蛋,牛奶就由爷爷处理了。 如果爷爷也有剩。 一家人都吃饱了嘛。 后面的牛奶, 鸡蛋你都不用送过来了, 谢谢啊。

还有一类人有可能在这户人家,鸡蛋超人:健美运动员。 健美运动员只吃鸡蛋白,所以一定会有剩:蛋黄嘛,如果我们有设置setOnClickListener, 就意味着这户人家来了健美运动员朋友,他对应的动作是:鸡蛋onClick,所以健美运动员吃食物的时机一定是在慈善家将鸡蛋送来。

onClick方法会得到执行的条件:注册onClickListener的对象一定要是可点击的, 即必须确保ACTION_DOWN一定会返回true

注册了onTouchlistener 但又不影响 该控件自身的事件处理逻辑, 则必须将listener中的onTouch方法返回false。特殊属性: ViewGroup类有一个属性:disallowIntercept, 标志是否禁用拦截功能。 默认为false。可以通过调用requestDisallowInterceptTouchEvent()方法将该属性设为true。 这就为以下需求:”设置了拦截方法onInterceptTouchEvent()为true的ViewGroup对象, 但在其子View中又想临时争取到处理事件的权利”提供了方法, 只需在临时申请事件处理的子view对象child,child.getParent().requestDisallowInterceptTouchEvent(true)实现。


本文系转载文章,感谢原作者的辛勤付出!

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值