Android事件分发机制(源码分析一)

一、关于事件分发的几个问题

1. 为什么要进行事件分发?

用户在Android系统屏幕上进行操作后,会有相应的事件产生。当产生事件的区域,有多个组件可以响应这个事件时,Android系统需要事件分发机制,来决定该事件传递给哪一个组件进行处理。

2. 什么是事件分发?

是指Android系统对用户行为产生的事件,进行传递处理的过程。

3. 事件分发指的是什么事件?

用户操作行为所产生的MotionEvent事件,具体可以是:ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_CANCEL等。

4. 如何进行事件分发?

采用责任链式的设计模式,事件层层传递,从上往下,再从下往上,寻找最终消费事件的组件,未找到,则将该次事件丢弃。

二、事件分发详解

事件分发的主体主要是ViewGroup和View,虽然ViewGroup也是继承自View,但是在事件的处理上ViewGroup和View所需要考虑的因素不同,处理过程有所不同,因为ViewGroup除了要考虑自己,还需要考虑其中的各个子View。所以,将事件分发分为两类进行分析:

(1) ViewGroup的事件分发。

(2) View的事件分发。

ViewGroup事件分发

ViewGroup事件分发主要涉及到以下三个方法:

(1)public boolean dispatchTouchEvent(MotionEvent ev)。

(2)public boolean onInterceptTouchEvent(MotionEvent ev)。

(3)public boolean onTouchEvent(MotionEvent ev)。

其中的核心方法是dispatchTouchEvent(),所以从这个方法开始分析。onInterceptTouchEvent()会在dispatchTouchEvent()的执行过程中被调用。onTouchEvent()是ViewGroup父类View的方法,只有当ViewGroup被当成View进行事件分发的时候,才会被调用。

ViewGroup被当成View进行事件分发的情况:

(1)ViewGroup对事件进行了拦截。

(2)事件发生在没有子View的区域。

(3)ViewGroup中所有子View未消费事件,事件被回传给了ViewGroup。

所以,开发过程中,如果要让ViewGroup处理事件(不管点击事件在不在有子View的区域),也就是被当成View进入onTouchEvent方法。有两种方法可以实现,一种是重写ViewGroup的onInterceptTouchEvent方法,让它返回true。另一种是让所有子View都不消费事件。

开始贴源码,抓关键点进行分析与理解。我们先在源码ViewGroup.java类中,找到public boolean dispatchTouchEvent(MotionEvent ev)这个函数,按从上往下顺序看这个方法。

1. 获取事件后,初始化相关变量,并判断是否进行了拦截

找到dispatchTouchEvent()这个方法后,我们可以先看到下面这几行代码。本来想多贴一点,但是贴在这里密密麻麻,容易让人产生恐惧,而且找不到重点。所以,只贴关键的几行。后面也基本这样,每个重要的地方,只挑关键的几行代码贴出来。

cancelAndClearTouchTargets(ev);

resetTouchState();

这两行是取消和清除上一次事件,并重置相关的变量。

intercepted = onInterceptTouchEvent(ev);

这一行主要在检测ViewGroup有没有拦截事件。onInterceptTouchEvent()方法,依据它的执行结果,改变intercepted标志的值,这个值将作为事件是否分发给子View的依据。而onInterceptTouchEvent()方法里的代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {

if(ev.isFromSource(InputDevice.SOURCE_MOUSE)

            && ev.getAction() == MotionEvent.ACTION_DOWN

            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)

            && isOnScrollbarThumb(ev.getX(), ev.getY())) {

return true;

    }

return false;

}

这几行是这个方法的所有代码,在开发的时候,可以重写这个方法,进行事件拦截。重写这个方法,让它返回true,表示不分发事件给子View,ViewGroup会被当成View进行事件分发。这就是第一种让ViewGroup处理事件的方法。当intercepted为false时,它后面一大块和子View相关的代码才会被执行。

2. 判断是否取消了这次事件

// Checkforcancelation.

final boolean canceled = resetCancelNextUpFlag(this)

        || actionMasked == MotionEvent.ACTION_CANCEL;

3. 未被拦截,并且未取消

if(!canceled && !intercepted) {

// 省略

}

当if (!canceled && !intercepted)条件满足的时候,开始处理ViewGroup里面的组件。

4. 拿到事件发生位置的点坐标

final float x = ev.getX(actionIndex);

final float y = ev.getY(actionIndex);

这个地方先拿到坐标,是为了后面判断子View有没有进行事件分发的条件。

5. 收集子View

final ArrayList preorderedList = buildTouchDispatchChildList();

buildTouchDispatchChildList()这个方法,它返回了一个List集合。这个集合里面装着,按Z轴方向从小到大排序的所有子View,即Z值较大的子View放在这个List的后面。

6. 遍历ViewGroup里的子View

for(int i = childrenCount -1; i >=0; i--) {

//省略

}

上一步已经拿到了一个按Z轴从小到大排好序的子View集合。这里遍历的时候是从后往前,所以,先处理List最后面的子View,即布局中盖在最上面的那个子View。

7. 检测当前拿到的子View是不是处在事件产生的位置

if(!canViewReceivePointerEvents(child)

        || !isTransformedTouchPointInView(x, y, child, null)) {

  ev.setTargetAccessibilityFocus(false);

continue;

}

先检测能否能接受事件,并且和前面拿到的点坐标结合,判断当前遍历到的这个View是否包含这个点坐标。不能接受事件或不包含事件的点坐标,continue,跳过后面的代码,开始下一次循环。如果子View包含这个坐标点的话,帮子View包装一下,赋值给newTouchTarget,然后break,跳出循环,不再处理剩下的子View。因为它表示当前View正在接收处理事件,不需要继续分发给其他View。

那么,事件发生的点坐标在非子View和子View区域时,ViewGroup分别做了什么?

(1)事件的点坐标在非子View区域

遍历所有子View,发现它们都不在事件发生点的位置,for循环里后面的代码都不会执行。最终清空了一下装子View的List,跳出了if (!canceled && !intercepted)后面的语句块,而到了下面这里。

// Dispatch to touch targets.

if(mFirstTouchTarget == null) {

// No touch targets so treat thisasan ordinary view.

    handled = dispatchTransformedTouchEvent(ev, canceled, null,

            TouchTarget.ALL_POINTER_IDS);

}else{

//省略

}

mFirstTouchTarget为空,表示没有子View能处理这个事件,而开始执行dispatchTransformedTouchEvent()方法,child的入参为null。

其中关键的代码如下:

if(child == null) {

    handled = super.dispatchTouchEvent(transformedEvent);

}

可以看到,child为空的时候,调用了super.dispatchTouchEvent(),即ViewGroup父类的dispatchTouchEvent(),而ViewGroup的父类是View。此时,ViewGroup被当成了一个View进行事件分发。

(2)事件点坐标在子View区域

事件点坐标在子View区域时,继续循环里面的代码:

if(dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

//省略

}

调用dispatchTransformedTouchEvent()方法后,会执行里面的下面这部分代码:

if(child == null || child.hasIdentityMatrix()) {

if(child == null) {

        handled = super.dispatchTouchEvent(event);

}else{

        final float offsetX = mScrollX - child.mLeft;

        final float offsetY = mScrollY - child.mTop;

        event.offsetLocation(offsetX, offsetY);

        handled = child.dispatchTouchEvent(event);

        event.offsetLocation(-offsetX, -offsetY);

    }

return handled;

}

其中child对象,就是我们当前处理的这个子View。人家有料,所以handled = child.dispatchTouchEvent(event)会执行,开始了它的事件分发,这里也涉及到了View的事件分发,后面再单独介绍。这个事件如果被这个子View消费了,事件传递结束,它下面的其他子View也就没什么事。因为dispatchTransformedTouchEvent()返回值为true的话,break,退出了for循环,后边的子View没有机会。如果返回值为false,那表示当前的活(事件)这个子View干不了,换下一个。如果事件点坐标位置的所有子View都不消费事件,也会执行如下代码:

if(mFirstTouchTarget == null) {

// No touch targets so treat thisasan ordinary view.

    handled = dispatchTransformedTouchEvent(ev, canceled, null,

            TouchTarget.ALL_POINTER_IDS);

}

这时候和事件发生在ViewGroup的非子View区域时一样,执行了相同的代码块,把ViewGroup当成View来进行事件分发。值得注意的是,和拦截的时把ViewGroup当成View的情况相比,此时这些子View已经进行过分发事件,即它们都会调用各自的dispatchTouchEvent()方法,只是都没有消费事件。好了,ViewGroup的事件分发先讲到这里,下一篇再继续介绍View的事件分发!

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,946评论 25 707
  • 本笔记整理自: https://www.gitbook.com/book/tom510230/android_...
    01_小小鱼_01阅读 998评论 0 4
  • 大学毕业两年多,我还依然是当初单纯的小姑娘吗?我还在用单纯的目光看待这个社会吗?渐渐的我意识到世界真的不完全像课本...
    pureheart_阅读 145评论 0 0
  • 没想到,在一次社交活动上意外的碰到了一位高中毕业后就没再见过的同学,老同学相见千言万语不知道从何说起,双方打...
    曾建宇阅读 156评论 0 0
  • 今天是在德国的最后一天,明天我们就要启程回家啦!早上起来是个阴雨天气,正好我们计划是去参观德意志博物馆。到达时10...
    蜂蜜001阅读 212评论 0 0