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[] 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。
结论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。设置监听器的代码部分:
- 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)
再给uncle添加OnClickListener。代码如下:
- uncle.setOnClickListener(new OnClickListener(){
- @Override
- public void onClick(View v) {
- System.out.println("uncle onClickListener onClick handles ");
- }
- });
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)实现。