从实例解析安卓事件分发机制

1.为什么要有事件分发机制?
因为安卓上面的View是树形结构,View可能会重叠在一起,当我们点击的地方有多个View的时候,事件应该分发给谁呢?
这时候就有了事件分发机制。
2.View的结构。

Paste_Image.png

3.事件分发,消费与拦截


2017-01-14_202824.png

4.事件分发的流程
前面了解到View是树形的结构,所以我们的事件也是有序的分发的。
事件收集之后最先传给Activity,然后依次向下传递,最后传入到底层的View。前提是事件没有发生拦截

Activity->photoWindow-DecorView->ViewGroup->.....->View

如果View消费了事件的话,则会返回true表示消费了该事件。
也会依次传递给上层的结构。
如果View没有消费事件的话,事件也不会浪费,则View会返回false,按照反方向回传,最终传会给Activity,如果Activity 也没有处理,则该事件就会被抛弃。

Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

由此可见,这种事件传递机制是一种非常经典的责任链模式: 如果我能处理就拦截下来自己干,如果自己不能处理或者不确定就交给责任链中下一个对象。

这种设计是非常精巧的,上层View既可以直接拦截该事件,自己处理,也可以先询问(分发给)子View,如果子View需要就交给子View处理,如果子View不需要还能继续交给上层View处理。既保证了事件的有序性,又非常的灵活。

5.实例讲解
1.首先要明确View和ViewGroup中的方法

Paste_Image.png

2.创建一个Demo演示。
①自定义一个ViewGroup
继承RelativeLayout是为了让RelativeLayout帮助我们将子控件布局好。
并重写ViewGroup的onInterceptTouchEvent,dispatchTouchEvent,onTouchEvent这三个方法。
并将其方法的返回值定义成一个变量,然后将这个变量返回,然后在通过Log将变量打印出来。
通过这个Log打印方式可以清楚的知道,这些方法的调用顺序

  代码如下:

public class MyViewGroup extends RelativeLayout {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.v("view",this.getClass().getName()+"---onInterceptTouchEvent 开始"+ev.getAction());
boolean onInterceptTouchEvent = super.onInterceptTouchEvent(ev);
Log.v("view",this.getClass().getName()+"---onInterceptTouchEvent 结束:"+ev.getAction() +":返回:"+onInterceptTouchEvent);

    return onInterceptTouchEvent;
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 开始:"+ev.getAction());
    boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);
    Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 结束:"+ev.getAction() +":返回:"+dispatchTouchEvent);

    return dispatchTouchEvent;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.v("view",this.getClass().getName()+"---onTouchEvent 开始:"+event.getAction());
    boolean onTouchEvent = super.onTouchEvent(event);
    Log.v("view",this.getClass().getName()+"---onTouchEvent 结束:"+event.getAction() +":返回:"+onTouchEvent);

    return onTouchEvent;
}

}

 ②自定义一个View

代码如下:
重写其dispatchTouchEvent,与onTouchEvent方法,而且Log的方式与ViewGroup一致,就不做多解释。

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

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 开始:"+ev.getAction());
    boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);
    Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 结束:"+ev.getAction() +":返回:"+dispatchTouchEvent);
    return dispatchTouchEvent;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.v("view",this.getClass().getName()+"---onTouchEvent 开始:"+event.getAction());
    boolean onTouchEvent = super.onTouchEvent(event);
    Log.v("view",this.getClass().getName()+"---onTouchEvent 结束:"+event.getAction() +":返回:"+onTouchEvent);

    return onTouchEvent;
}

}

③将其布局到MainActivity中

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.testproject.testproject.MyViewGroup
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:background="@color/colorPrimary"
>

  <com.testproject.testproject.MyView
      android:layout_width="100dp"
      android:layout_height="100dp"
      android:layout_centerInParent="true"
      android:background="@color/colorAccent"
      />

</com.testproject.testproject.MyViewGroup>
</RelativeLayout>

 ④MainActivity代码,我们也将MainActivity的dispatchTouchEvent,onTouchEvent重写,并Log方式与上文一致。
 因为事件传递的源头是Activity,而事件传递的终结点也是Activity,所以此处一定要Log,用于判断事件传递的流程。
 我们先将其注释,等需要用到的时候在使用它。

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

/* @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.v("view",this.getClass().getName() + "---dispatchTouchEvent 开始:" + ev.getAction());
boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);
Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 结束:"+ev.getAction() +":返回:"+dispatchTouchEvent);
return dispatchTouchEvent;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v("view",this.getClass().getName()+"---onTouchEvent 开始:"+event.getAction());
boolean onTouchEvent = super.onTouchEvent(event);
Log.v("view",this.getClass().getName()+"---onTouchEvent 结束:"+event.getAction() +":返回:"+onTouchEvent);
return onTouchEvent;
}*/
}

 ⑤则实际效果如下
Paste_Image.png

ViewGroup 包裹着View
1.当我们点击view的时候,则会产生一系列的Log。

Paste_Image.png

tips:
触摸事件有三种形式:分别由0,1,2表示
Action.DOWN 对应 0
Action.MOVE 对应 1
Action.UP 对应 2
通过Log可以得知,此时我们是点击事件,所以Action =0;
则由Log可以得到
此时事件分发机制的顺序是:
①当产生点击事件时
②先传递到最上层的MyViewGroup中,调用其dispatchTouchEvent 方法,然后在调用其onInterceptTouchEvent方法,如果不拦截则onInterceptTouchEvent结束,并返回false。此时MyViewGroup中的dispatchTouchEvent方法并没有结束,因为我们没有此时还没有方法结束的Log,所以是在dispatchTouchEvent方法中调用了自身的onInterceptTouchEvent方法,来判断是否拦截,不拦截则返回fasle,拦截返回true。
tips:ViewGroup中的onInterceptTouchEvent方法默认是返回false,不拦截。
③则ViewGroup中的onInterceptTouchEvent方法结束之后,会调用View的dispatchTouchEvent,则此时View的dispatchTouchEvent开始,dispatchTouchEvent方法会调用View的onTouchEvent方法,如果onTouchEvent消费事件的话则返回true,不消费的话则会返回false.
因为我们在View的onTouchEvent方法的返回值是默认实现的,所以其返回值是false.则是不消费事件,则事件会回传。
④当View的onTouchEvent返回false,说明事件View不消费,此时会将事件会传到上层,当onTouchEvent返回false后,会调用View的dispatchTouchEvent,因为没有消费事件,所以View的dispatchTouchEvent会结束,并返回false,开始向上一层传递。此时事件开始在View中的处理就结束了,事件开始上传。
⑤当View的dispatchTouchEvent返回false,会上传到ViewGroup的onTouchEvent方法,判断ViewGroup是否消费该事件,返回false,则说明ViewGroup不消费该事件,则会onTouchEvent返回false之后,ViewGroup的dispatchTouchEvent就结束了,然后返回了false。此时Log就结束了。
⑥我们只打印了View和ViewGroup,但是事件分发机制的顺序也差不多就是这样。因为这个事件都没有控件消费,所以最终源头会会传到Activity,然后浪费掉此事件。通过一张图来表示其顺序,

Paste_Image.png

注意: 上图显示分发流程仅仅是一个示意流程,并不代表实际情况,如果按照实际情况绘制,会导致流程图非常复杂和混乱。
上面的流程中存在部分不合理内容,请大家选择性接受。
事件返回时 dispatchTouchEvent
直接指向了父View的 onTouchEvent
这一部分是不合理的,实际上它仅仅是给了父View的 dispatchTouchEvent
一个 false 返回值,父View根据返回值来调用自身的onTouchEvent

ViewGroup 是根据 onInterceptTouchEvent
的返回值来确定是调用子View的 dispatchTouchEvent
还是自身的 onTouchEvent
, 并没有将调用交给 onInterceptTouchEvent

ViewGroup 的事件分发机制伪代码如下,可以看出调用的顺序。

public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = false; // 默认状态为没有消费过 if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View result = child.dispatchTouchEvent(ev); } if (!result) { // 如果事件没有被消费,询问自身onTouchEvent result = onTouchEvent(ev); } return result;}

2.上一个流程View没有消费事件,因为onTouchEvent的返回值为true,当View消费掉事件时,则Log如下:

Paste_Image.png

当View消费掉事件后,则会返回true,当事件回传时,也不会去询问ViewGroup是否消费,则会直接回传,并返回true.

3.当ViewGroup中的onInterceptTouchEvent拦截事件时

Paste_Image.png

当ViewGroup拦截时,则会直接询问是否消费事件,如果不消费事件则返回false,事件会依旧上传。
4.当我们也启用Activity中的dispatchTouchEvent和onTouchEvent时,且其余的事件返回值都是默认。

Paste_Image.png

事件传递和上面的 《1.》是差不多的,不过是又多了一层Activity的Log,说明事件传递的方法调用顺序和我们预想的是一样的。
但是特殊的是,UP事件并没有和DOWN事件一样,有那么多的方法的调用。
这是因为安卓自身的问题,因为:
事件传递过程中,如果一个控件在onTouchEvent方法中没有消费DOWN事件,以后父容器不再给你传递任何事件。

所以需要我们如果要消费事件的话一定要返回true,不然后续的事件也会接收不到。

5.和《3》一样,当ViewGroup拦截事件时。

Paste_Image.png

则事件的传递和《3》是大同小异的,不过是多了回传给Activity。
而且DOWN事件因为没有消费,则UP事件也没有向下传递,更验证了我们《4》的结论。
事件传递过程中,如果一个控件在onTouchEvent方法中没有消费DOWN事件,以后父容器不再给你传递任何事件。

通过以上5个小例子,相信大家对事件传递的机制有了一些了解。
通过这些我们也得到了一些结论:
事件是一组一组的,以DOWN开始,0到n个MOVE,以UP结尾
事件是从外出ViewGroup往内层传递,如果过程中有控件消费(onTouchEvent),事件不再往下传递,如果过程中没有被消费,事件再依次回传,最终回传到Activity中 ViewGroup可以拦截事件,默认5大布局都不拦截事件,如果拦截了事件,调用自己的onTouchEvent方法,事件不往下传递
事件传递过程中,如果一个控件在onTouchEvent方法中没有消费DOWN事件,以后父容器不再给你传递任何事件

其他情况
事件分发机制设计到到情形非常多,这里就不一一列举了,记住以下几条原则就行了。
1.如果事件被消费,就意味着事件信息传递终止。
2.如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃。
3.判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。

总结
View的事件分发机制实际上就是一个非常经典的责任链模式,如果你了解责任链模式,那么事件分发对你来说并不是什么难题,如果你不了解责任链模式,刚好借此机会学习一下啦。
责任链模式:当有多个对象均可以处理同一请求的时候,将这些对象串联成一条链,并沿着这条链传递改请求,直到有对象处理它为止。

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

推荐阅读更多精彩内容