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方法。伪代码如下:

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)实现。

 

原文地址:https://www.cnblogs.com/moonbow/p/4824362.html