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文件中

    <com.example.godru.demozhurui.MyGroupView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.example.godru.demozhurui.MyView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </com.example.godru.demozhurui.MyGroupView>

现在 我们有了用于测试的代码 现在来谈谈关于事件的细节:
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的路线传递事件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,699评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,124评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,127评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,342评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,356评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,057评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,654评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,572评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,205评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,343评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,015评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,704评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,196评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,690评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,348评论 2 358

推荐阅读更多精彩内容