Android自定义View——事件分发机制学习笔记

Posted by Csming on 2017-03-15

为什么有事件分发机制

  • 因为Android的View是树状结构的;View可能会重叠在一起;

  • 当我们点击的地方有很多歌View都可以响应时,就很尴尬了;

  • 于是事件分发机制就用来解决这种问题

如图,View是一层一层嵌套的;当点击View1时,下面的ViewGroupA、RootView等也能够响应;为了确定到底要由哪个View处理盖点击事件,就需要事件分发机制来处理了;**

(图片来自原文)

View的结构

View的结构是树形的;

  • 最顶层 View(Group) 的大小并不是填满父窗体的,留下了大量的空白区域
    其中图中的DecorView用于显示主体颜色和标题栏等内容

PhoneWindows

Windows是一个抽象类,是所有视图的最顶层容器;视图的外观和行为都由他管,无论是北京显示,标题栏还是事件处理都是他管理的范畴;
但是,虽然能够管理的事情很多,但是不能直接使用**

  • 于是PhoneWindows作为Window的唯一实现类;
    而DecorView是PhoneWindow的一个内部类
    DecorView还负责消息的传递;PhoneWindow的指示通过DecorView传递给下面的View,而下面的View也通过DecorView回传给PhoneWindow

事件分发、拦截与消费

类型 相关方法 Activity ViewGroup View
事件分发 dispatchTouchEvent
事件拦截 onInterceptTouchEvent × ×
事件消费 onTouchEvent

三个方法均有一个boolean类型的返回值;通过返回true和false来控制时间传递的流程

  • Activity和View都是没有时间拦截的;因为:Activityu作为原始的事件分发这;若Activity拦截了时间,就会导致整个屏幕都无法;View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截

View相关

为什么View会有dispatchTouchEvent

View可以注册很多事件监听器;那么这么多时间便由dispatchTouchEvent管理;

与View事件相关的各个方法的调用顺序

  • **单击事件(onClickListener) **需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用(最后)

  • **长按事件(onLongClickListener) **同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)

  • **触摸事件(onTouchListener)**如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)

  • **View自身处理(onTouchEvent)**提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)

onTouchListener > onTouchEvent > onLongClickListener > onClickListener

  • 1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。

  • 2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
    eg:

1
2
3
4
5
6
7
8
9
10
11
<RelativeLayout
android:background="#CCC"
android:id="@+id/layout"
android:onClick="myClick"
android:layout_width="200dp"
android:layout_height="200dp">
<View
android:clickable="true"
android:layout_width="200dp"
android:layout_height="200dp" />
</RelativeLayout>

ViewGroup相关

ViewGroup不进需要考虑自身,还要考虑各种childview;
ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView;

默认情况下,ViewGroup的时间分发机制如下:

1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
3.如果子 ChildView 不需要则调用自身的 onTouchEvent。

用伪代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过

if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View
result = child.dispatchTouchEvent(ev);
}

if (!result) { // 如果事件没有被消费,询问自身onTouchEvent
result = onTouchEvent(ev);
}

return result;
}

此外,还有一些问题需要解决:

  1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?

把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View

  1. 当该点的 ChildView 有重叠时应该如何分配?

当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView
后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的;


可点击事件:给View注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的

给 View 注册 OnTouchListener 不会影响 View 的可点击状态。即使给 View 注册 OnTouchListener ,只要不返回 true 就不会消费事件


  1. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),哪个会执行?

事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。

  1. 所有事件都应该被同一 View 消费

在上面的例子中我们分析后可以了解到,同一次点击事件只能被一个 View 消费,这是为什呢?主要是为了防止事件响应混乱,如果再一次完整的事件中分别将不同的事件分配给了不同的 View 容易造成事件响应混乱。


为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。
如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来


  • 要点:
    1.事件分发原理: 责任链模式,事件层层传递,直到被消费。

2.View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。

3.View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。

4.不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
事件是否被消费由返回值决定,true 表示消费,false表示不消费,与是否使用了事件无关。

5.ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。

6.ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。

7.一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。

8.只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。

9.如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

事件分发流程

1
Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View

如果最后分发到View,如果这个View也没有处理事件;
如果没有任何View消费掉事件,那么这个事件会回传,传回到Activity,若Activity也没有处理,本次事件才会被抛弃

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

上层View既可以直接拦截该事件,自己处理,也可以先询问(分发给)子View,如果子View需要就交给子View处理,如果子View不需要还能继续交给上层View处理

// PhoneWindow 和 DecorView 无法直接操作

1.点击View1区域但没有任何View消费事件

当手指在 View1 区域点击了一下之后,如果所有View都不消耗事件,你就能看到一个完整的事件分发流程

红色箭头方向表示事件分发方向。

绿色箭头方向表示事件回传方向。

作者说:按照实际情况绘制,会导致流程图非常复杂和混乱
//我也是这么想的……

事件返回时dispatchTouchEvent直接指向了父View的 onTouchEvent 这一部分是不合理的,实际上它仅仅是给了父View的 dispatchTouchEvent 一个 false 返回值,父View根据返回值来调用自身的 onTouchEvent。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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.点击View1区域且被View1消费

如果被View1消费掉了,测时间会回传告诉上层View这个事件已经被解决了,上层View无需再响应;

3.点击 View1 区域但事件被 ViewGroupA 拦截

上层的View有权拦截事件,不传递给下层View,例如 ListView 滑动的时候,就不会将事件传递给下层的子 View

要点(原则)

1.如果事件被消费,就意味着事件信息传递终止。
2.如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃。
3.判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。

常见事件

  • ACTION_DOWN 手指 初次接触到屏幕 时触发。
  • ACTION_MOVE 手指 在屏幕上滑动 时触发,会会多次触发。
  • ACTION_UP 手指 离开屏幕 时触发。
  • ACTION_CANCEL 事件 被上层拦截 时触发。

一般单指触控的交互流程为:ACTION_DOWN->ACTION_MOVE->ACTION_UP;若是单击,则不会出发ACTION_MOVE


出处:http://www.gcssloop.com/customview/dispatch-touchevent-theory
http://www.gcssloop.com/customview/dispatch-touchevent-source