Android事件分发详解以及需要注意的细节

关于android中的事件分发 可以查看这篇文章:
https://www.jianshu.com/p/38015afcdb58

在文章中 用一张图片总结了android事件分发的主要过程:

android事件分发

从这张图上 我们可以说已经把android的事件分发过程看了个大概 但是实际上 上述的过程只是android事件分发的一个部分 在实际的事件处理中 已经遇见更复杂的情况 那时 事件不一定按这张图的过程走 也就是我们需要在事件分发过程中注意的“细节”(在上面提到的文章中也说起过这部分的事 也就是所谓的“事件后续” 这里我们会详细的解释下这部分的知识)

先设置以下代码:
1.MyGroupView 我们的 外层-父布局

public class MyGroupView extends ViewGroup {

    public MyGroupView(Context context) {
        super(context);
    }

    public MyGroupView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        getChildAt(0).layout(l, t, r, b);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
        return false;
    }
}

2.MyView 我们的子view

public class MyView extends View {

    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
            return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
        return false;
    }
}

3.在activity的xml文件中

    

        

    

现在 我们有了用于测试的代码 现在来谈谈关于事件的细节:
ps:在android中 事件以int类型表示 0-DOWN 1-UP 2-MOVE 3-CANCEL 具体可以点击进android的MotionEvent类中详细查看

1.只要View在onTouch中返回false 就会将事件向上传递到ViewGroup吗?

从上述的事件分发图中好像是这么一回事 现在我们用上文代码测试一下
先假设事情会按图上走 那么我们想看到的log应该是:
MyGroupView dispatchTouchEvent DOWN
MyGroupView onInterceptTouchEvent DOWN
MyView dispatchTouchEvent DOWN
MyView onTouchEvent DOWN
MyGroupView onTouchEvent DOWN

MyGroupView dispatchTouchEvent MOVE
MyGroupView onInterceptTouchEvent MOVE
MyView dispatchTouchEvent MOVE
MyView onTouchEvent MOVE
MyGroupView onTouchEvent MOVE
....

现在看看实际结果:

只有DOWN事件走过了流程

我在测试中实际动作并不只有点击 而是有划动的 但是图上的结果却很奇怪 不但是没有move事件 连up事件都没有出现 这是为什么?

这就是Android事件分发中关于事件的一个细节:android对手势事件是如何定性的?

Android对事件的定义

在android中 一个完整的事件是由DOWN事件“开头” 以UP或CANCEL(这个事件后面会说到)“结束” 这整个过程被认为是一个“完整”的事件

而重点就在于这个“开头” android的事件不是时时刻刻都在分发中 为了性能 为了不必要的浪费

---只有那些”表示“需要这个事件的控件才会被分发后续的事件---

而这个”需要“就是指在onTouchEvent 或者 dispatchTouchEvent中对DOWN事件返回ture(只有对DOWN返回true才有这个功能 因为DOWN事件是一切事件的开始 其它事件 如:MOVE 还是会按照流程图的路线行走) 也就是向android表示本控件需要本次事件以及后续的其它事件 这样才会接收到后续MOVE和UP
如果在一个事件走完了整个流程 还是没有任何控件”消费“DOWN事件 android就认为这个事件没有任何控件想要 事件也就被取消了

2.ViewGroup在dispatchTouchEvent中返回默认值 且在onInterceptTouchEvent 返回false时 一定会将事件传递到View吗?

我们将上代码中的ViewGroup改为:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
        return true;
    }

View的代码改为:

   @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
            return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
        return false;
    }

这次有控件在onTouchEvent中返回了true 所以事件应该会有后续传递来 我们理想中的情况是:

MyGroupView dispatchTouchEvent DOWN
MyGroupView onInterceptTouchEvent DOWN
MyView dispatchTouchEvent DOWN
MyView onTouchEvent DOWN
MyGroupView onTouchEvent DOWN

MyGroupView dispatchTouchEvent MOVE
MyGroupView onInterceptTouchEvent MOVE
MyView dispatchTouchEvent MOVE
MyView onTouchEvent MOVE
MyGroupView onTouchEvent MOVE
....

但是实际情况却是:
DOWN事件后没有再向下传递事件

从上图可以看出 事件并非是什么时候到在向下传递的 这也和上文中说到的”事件需求者“有关

Android中事件传递的意义

我们发现事件并不总是在向下传递的 这是因为android向下传递后又向上返回的这整个过程的本质就是在寻求”需求者“ 需要事件的控件返回 要不在dispatchTouchEvent中返回true 要不在onTouchEvent中返回true 以确定是否由自己来处理这个事件
而在android中 它们的设计是一个事件面向一个对象 不会出现一个事件由多个控件来处理的情况(你可以通过代码来实现 但是事件分发机制本身只会以一个控件为目标来传递事件)所以在已经明确了处理者后 再继续向下传递事件是没有意义的

---谁需求 谁处理 即使还存在子控件---

也就是说 android不但只在确定有人”需求“事件后才传递后续事件 且控件本身在明确了由自己处理后也只接收后续事件了 不再继续传递给自己的子控件

3.ViewGroup是否一定可以通过onInterceptTouchEvent返回true的方式来获取事件?

从上文中我们已经知道了事件分发的流程 并且应该也可以发现 这个流程中 其实子view是非常被动的 因为无论DOWN事件是否是被子view消费 ViewGroup都可以用onInterceptTouchEvent来拦截事件 因为事件一定会经过它 那是否意味着 我们可以无限制的使用onInterpecTouchEventt来获取事件呢? 答案是 不是的 因为ViewGroup有一个公共方法:
requestDisallowInterceptTouchEvent(true);

这个方法的描述是锁定ViewGroup的拦截方法 事实上就是确保事件一定不会被拦截 和一定会被拦截

我们修改下代码来测试一下

MyGroupView:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
        return ev.getAction() != MotionEvent.ACTION_DOWN;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
        return true;
    }

MyView:

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
            return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
        if (event.getAction() == MotionEvent.ACTION_DOWN)
            getParent().requestDisallowInterceptTouchEvent(true);
        return true;
    }

这次我们在拦截方法中除了DOWN事件以外都返回true 在View里 我们在接收到DOWN事件后 对父布局使用一次requestDisallowInterceptTouchEvent() 锁定拦截方法

一般理想过程应该是:
MyGroupView dispatchTouchEvent DOWN
MyGroupView onInterceptTouchEvent DOWN
MyView dispatchTouchEvent DOWN
MyView onTouchEvent DOWN

MyGroupView dispatchTouchEvent MOVE
MyGroupView onInterceptTouchEvent MOVE
MyGroupView onTouchEvent MOVE
MyGroupView dispatchTouchEvent MOVE
MyGroupView onTouchEvent MOVE
MyGroupView dispatchTouchEvent MOVE
MyGroupView onTouchEvent MOVE
....

实际运行情况:


拦截方法没有起到作用

如上 拦截方法甚至都没有走到过 因为requestDisallowInterceptTouchEvent方法会影响ViewGroup的锁定判断 这个判断的益在onInterceptTouchEvent方法调用前 所以锁定开启后 甚至都不会走这个方法

这个方法的作用是避免本View获取的事件被其它View拦截
本方法的存在也可以解释为什么有的view的事件无法被拦截的问题

但是这个方法说到底还是子view对父view的调用 所以如果DOWN事件都被直接拦截 这个方法也就没有任何作用了

4.CANCEL事件的意义 以及 使用了拦截方法就会直接调用其onTouchEvent方法吗

CANCEL事件在有的文章中介绍是手指滑动到超过手机屏幕时出现 其实这个说法是错误的 因为实际在这种情况下 出现的仍然是UP事件
要再现CANCEL事件可以直播修改代码:

MyGroupView:

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("测试用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
        return ev.getAction() != MotionEvent.ACTION_DOWN;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
        return true;
    }

MyView:

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
            return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("测试用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
        return true;
    }

运行效果:


CANCEL事件出现

从图中 我们就可以解答一开始的两个问题
CANCEL事件其实是在事件被view拦截时 由被拦截的事件转化而来 是一个代码性质上的事件
拦截方法在拦截后并不是会直接调用onTouchEvent方法 而是向下传递了一个CANCEL事件

这是因为android中事件本身是有目标的(也就是需求者)在事件传递的过程中 决定是否向下和向上传递的实际是view自己
如果一个事件没有传递到目标view 而是被拦截了 android不会让目标view一无所知 而是向他传递一个CANCEL事件 表示你已经不是需求者了 之后才会继续向新的需求者传递事件

这本质上也符合之前所说的事件-控件一一对应的原则

总结

1.文章最上方的事件传递图只对事件的开始-DOWN事件完全适用 后续事件的情况与其不一样

2.android中事件的开始是DOWN事件 只有在有控件确定自己为需求者(消费了DOWN事件)的情况下 后续事件才会出现 不然就没有后续事件

对DOWN事件的消费有两种:
在dispatchTouchEvent里返回true or 在onTouchEvent里返回true

3.除DOWN事件外 后续事件传递的对象都是消费了DOWN事件的控件 传递到这个控件后 无论其是否在onTouchEvent或dispatchTouchEvent里消费与否 都会继续向这个控件传递后续事件(直到UP事件或CANCEL事件出现 意味着本次事件的结束)

4.在已经确定了“需求者”的情况下控件不再会因为像文章顶部的图中一样 在onTouchEvent或dispatchTouchEvent里返回了false 而将事件向上返回 因为已经确定了需求者

同理 在确定了“需求者”的情况下 事件也不会因为控件在dispatchTouchEvent里返回默认值 和在拦截方法onInterceptTouchEvent里返回false 而把事件向下传递

5.拦截事件onInterceptTouchEvent可以让ViewGroup强行确定自己“需求者”的地位 这个方法在返回了一次true后 就会让后续事件向它传递 之后不会再调用 而在拦截开始时 被拦截的事件并不是直接就返回到ViewGroup的onTouchEvent方法中 而是将这个事件转化为一个CANCEL事件 向下传递到了之前的“需求者”控件 之后的事件才会传递到ViewGroup

6.可以对一个viewGroup调用requestDisallowInterceptTouchEvent来使其拦截方法失效或开启 这个方法可以用于让子view避免事件被父view拦截 设置为true会让ViewGroup不经过拦截方法 直接按false的路线传递事件

你可能感兴趣的:(Android事件分发详解以及需要注意的细节)