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方法。伪代码如下:
public void dispatchTouchEvent() { if (!onInterceptTouchEvent()) { // 判断自己是否拦截掉信号, View[] childs = getChilds(); for (View view: childs) { inMyRange(view,location) if (child.dispatchTouchEvent()==true) { return true; // 只有dispatch返回true,表示消费了事件就立马return,事件不再传递了 } // dispatch返回false, 则还会传递给下一个或跳转到下面的 } } // 如果onInterceptTouchEvent()返回true,也就是拦截了, 则调用自己的处理逻辑 if (mListener!=null && mListener.onTouch()==true....) { retuen true; } onTouchEvent(); }
如果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输出顺序来呈现事件的传递路径
源码
---------------------------------------------------------------------------------------------------------------------------------------------public class GrandpaFrameLayout extends FrameLayout { private static final String[] ACTIONS = { "DOWN", "UP", "MOVE", "CANCEL" }; public GrandpaFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent event) { // 输出形式: Grandpa dispatchTouchEvent handles DOWN // 哪个视图元素 + 哪个方法 handles + 哪个事件 System.out.println(getTag() + " dispatchTouchEvent handles " + ACTIONS[event.getAction()]); return super.dispatchTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { // TODO Auto-generated method stub System.out.println(getTag() + " onInterceptTouchEvent handles" + ACTIONS[event.getAction()]); return super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub System.out.println(getTag() + " onTouchEvent handles" + ACTIONS[event.getAction()]); return super.onTouchEvent(event); } }
public class ChildTextView extends TextView { private static final String[] ACTIONS = { "DOWN", "UP", "MOVE", "CANCEL" }; public ChildTextView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent event) { // TODO Auto-generated method stub System.out.println(getTag() + " dispatchTouchEvent handles" + ACTIONS[event.getAction()]); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub System.out.println(getTag() + " onTouchEvent called"); return super.onTouchEvent(event); } }
布局文件: 通过给各自添加tag属性, 来方便判断是哪个视图元素。
<com.example.toucheventtest.GrandpaFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/grandpa" android:layout_width="match_parent" android:layout_height="match_parent" android:tag="Grandpa" android:background="#ff888877" > <!-- <com.example.toucheventtest.GrandpaFrameLayout > --> <com.example.toucheventtest.FatherLinearLayout android:id="@+id/uncle" android:layout_width="400dp" android:layout_height="200dp" android:background="#ff00ff00" android:tag="Uncle" /> <com.example.toucheventtest.FatherLinearLayout android:id="@+id/father" android:layout_width="300dp" android:layout_height="300dp" android:background="#ffff0000" android:tag="Father"> <com.example.toucheventtest.ChildTextView android:id="@+id/child" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ffffffff" android:tag="child" android:text="I am a child" /> </com.example.toucheventtest.FatherLinearLayout> </com.example.toucheventtest.GrandpaFrameLayout>
界面如下:
点击 I am a child log输出结果如下:
图1
如果点击绿色区域即 爷爷和叔叔包含的区域,log输出如下:
图2
从图1,图2 结果对比来看, 事件的传递是先从父View开始,且会做一次 点击区域归属地判断,事件只会传给包含了点击区域的子view。(白色区域为3者共同都包含的区域, 而绿色区域只有grandpa 和uncle区域包含)
结论1: 事件传递是 从外层往内层传递的。(但后面的log输出可看到内层可以回传给外层。什么情况下会回传)
如果我们修改FatherLinearLayout的onInterceptTouchEvent方法,让返回值为true。再看运行结果
图3
因此从两次结果图1, 图3的对比来看 我们可以得出如下结论:
onIntercept方法决定事件是否向下传递,如果返回true,则事件不再向下传递了。
再做实验, 修改FatherLinearLayout的 onTouchEvent方法, 让返回值为true,即现在是father的onInterceptTouchEvent方法和onTouchEvent都返回true,log结果如下:
图4
如果点击绿色区域。即只属于grandpa和uncle的区域时 log输出结果为:
图5
从图3 与图4,图5 对比来看, onTouchEvent方法决定事件是否向右(点击区域有2个或以上同一层级的view对象时)和向上传递。 返回true, 事件不再传递了, 返回false才会继续传递。 (onTouchEvent方法的返回值决定事件是否回传。)
如果FatherLinearLayout 的onInterceptTouchEvent方法返回false, 且点击绿色区域,并移动。结果如下:
图6
从图5, 图6结果对比来看。 如果事件没有被被任何一个方法返回true。 则后续的ACTION_MOVE, ACTION_DOWN,不会被系统传递。即忽略后续的touch信号。
结论总结: 1. 事件的传递是由外层往内层传递, 且传递过程是调用dispatchTouchEvent来决定事件的传递轨迹。 dispatchTouchEvent内部先调用onInterceptTouchEvent,看是否拦截事件, 如果返回true表示拦截, 则事件交由自己处理; 如果返回false, 则交由点击区域所属的子view处理。子view的处理逻辑同父view, 递归调用。 如果点击区域属于2个或以上子view(只有在FramLayout布局中才存在这种情况), 则布局最后添加的子view优先处理事件。多层级共享点击区域的事件传递是一个深度优先原则。
2. 一组事件中 只有ACTION_DOWN返回true, 后续的事件才会被接收和传递。
3. 如果事件传到子view没有被消费, 则事件会逐层往上回传。
添加各种监听器对事件传递的影响
我们给uncle添加事件监听器OnTouchListener。 回调方法onTouch返回false,设置监听器的代码部分:
代码:
FatherLinearLayout uncle = (FatherLinearLayout) findViewById(R.id.uncle); uncle.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub System.out.println("uncle onTouchListener onTouch handles " + ACTIONS[event.getAction()]); return false; } });
在绿色区域点击,即uncle和grandpa都占有的区域。结果:
图7
如果修改onTouch的返回值为true, 结果为:
图8
图7,8验证了结论4. a): onTouchListener 的回调方法的返回值决定了事件是否被消费, 且该回调方法会在view的onTouchEvent方法之前先处理事件。
再给uncle添加OnClickListener。代码如下:
uncle.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { System.out.println("uncle onClickListener onClick handles "); } });
log输出结果如下: OnTouchListener 方法true, 同时设置了OnClickListener的情况:
图9
将onTouch方法的返回值修改为false, log输出结果如下:OnTouchListener 返回false, 同时设置了OnClickListener的情况:
图10
从图9, 图10对比 可验证结论:4.b)的结论。
OnClickListener监听器的回调方法onClick方法 会在ACTION_UP信号传递过来时执行。 且ACTION_MOVE,ACTION_UP一定会被系统接收,传递。不再受到ACTION_DOWN的处理结果必须为true的限制。
再添加OnLongClickListener, 且onLongClick方法返回true, 让OnTouchListener返回false,log输出结果如下:OnTouchListener, onClickListener, OnLongClickListener,且onTouchListener, view自身也不消费事件, 但onLongClick返回true的情况:
图11
更改onLongClick方法返回值为false, log输出结果如下:
图12
从图11, 图12对比来看, onLongClick的返回值 会影响onClickListener的回调方法onClick是否会被执行。OnLongClickListener方法true表示 点击事件会被长点击独占,其他的点击监听器不需要处理了。false表示其他点击监听器要处理。 验证了结论4. c)结论
总结: 用一个生动的比喻来概括:有一个慈善家,家里面有食物大礼包,大礼包里的食物有3样食物:面包, 牛奶, 鸡蛋(对应ACTION_DOWN,ACTION_MOVE,ACTION_UP),或者是面包和鸡蛋(对应ACTION_DOWN,ACTION_MOVE,ACTION_UP) 慈善家呢来到一户人家。这户人家有爷爷, 爸爸, 孙子3代。中国礼仪之邦,都是先给长辈。
1. 爷爷收到了一个面包, 肚子饿吗?(onInterceptTouchEvent方法是否返回true),如果饿,跳转到第2步。如果不饿(返回false),把面包给儿子,对儿子说: 儿啊, 爸爸这有好东西, 给你吃, 你吃不完别丢了啊, 还给我吃。跳转到第3步
2. 爷爷吃面包, 吃完了, 慈善家会把大礼包里面的其他食物 牛奶, 鸡蛋依次拿给爷爷。(只要其中的某种食物没吃完 则大礼包里剩下的就不会再拿了, 因此已经吃撑了)
3. 爸爸收到面包,也是同样的处理逻辑。 肚子饿吗?(onInterceptTouchEvent方法是否返回true),如果饿,跳转到第6步。如果不饿(返回false),把面包给自己的儿子,也对儿子说: 儿啊, 爸爸这有好东西, 给你吃, 你吃不完别丢了啊, 还给我吃。跳转到第4步
4. 孙子吃面包(执行方法onTouchEvent)。把面包吃完了(对应的是返回true), 慈善家一看食物吃完了,就又从食物大礼包中把牛奶,鸡蛋依次拿过来给爷爷。爷爷,就直接给爸爸, 爸爸直接给孙子。孙子继续吃。
5. 孙子吃面包 还有剩, 就把剩下的面包给爸爸 , 跟老爸说。把我吃撑了, 那大礼包里剩下的食物你别给我了。跳到第6步,
6. 爸爸吃面包(执行方法onTouchEvent)。把面包吃完了(对应的是返回true), 那慈善家就会继续将食物大礼包中的牛奶拿过来。给爷爷。爷爷见儿子没剩食物给自己,知道儿子可能还没吃饱,就直接传给自己的儿子:爸爸。爸爸由于收到儿子说别拿食物来了的话, 就不再把牛奶,鸡蛋再给孙子了。 于是自己吃。
7. 爸爸吃面包(执行方法onTouchEvent)。面包没吃完(对应的是返回false), 又把剩下的面包给自己的父亲:爷爷。 也对他说:爸爸, 我吃撑了。爷爷觉得浪费粮食可耻, 虽然不饿, 但能吃点。 于是就也吃食物。
8. 爷爷吃面包(执行方法onTouchEvent)。把面包吃完了(对应的是返回true), 那慈善家就会继续将食物大礼包中的牛奶拿过来。爷爷依据吃面包所获得的信息(儿子有剩下面包给我 就说明儿子吃撑了,没必要给他们了, 我自己吃。 儿子没剩面包给我吃, 说明儿子可能还没吃饱, 我再给他。 爸爸处理逻辑和爷爷一样。)
还有一类人有可能在这户人家,鸡蛋超人:健美运动员。 健美运动员只吃鸡蛋白,所以一定会有剩:蛋黄嘛,如果我们有设置setOnClickListener, 就意味着这户人家来了健美运动员朋友,他对应的动作是:吃鸡蛋onClick,所以健美运动员吃食物的时机一定是在慈善家将鸡蛋送来时, 并且慈善家很善解人意,见到爷爷家有客人, 就不管三七二十一,大礼包里每一种食物我都给你送给来, 不管你吃不吃得完。
注册了onTouchlistener 但又不影响 该控件自身的事件处理逻辑, 则必须将listener中的onTouch方法返回false。
补充知识::ViewGroup类有一个属性:disallowIntercept, 标志是否禁用拦截功能。 默认为false。可以通过调用requestDisallowInterceptTouchEvent()方法将该属性设为true。 这就为以下需求:”设置了拦截方法onInterceptTouchEvent()为true的ViewGroup对象, 但在其子View中又想临时争取到处理事件的权利”提供了方法, 只需在临时申请事件处理的子view对象child,child.getParent().requestDisallowInterceptTouchEvent(true)实现。