Android View事件体系

一、关键类与关键方法

1、MotionEvent

在手指接触到屏幕后会产生一系列的点击事件,如

  • 点击屏幕后离开松开,事件序列为DOWN->UP
  • 点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->…->MOVE->UP 通过MotionEven对象我们可以得到事件发生的x和y坐标,我们可以通过getX/getY和getRawX/getRawY得到,它们的区别是:getX/getY返回的是相对于当前View左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标
2、dispatchTouchEvent(MotionEvent ev)

View处理事件的入口,只要事件能到达该View,该方法就会被调用。能到达该View指的是事件在View范围内,且事件未被父View消费。

将事件先后分发给自己与子View处理,如果其中之一消费了该事件,则返回true,告诉上级View该事件已被消费。

3、onInterceptTouchEvent(MotionEvent ev)

返回值表示当前ViewGroup是否拦截该事件,如果拦截了该事件,则不会再将事件分发给子View。并且只要该ViewGroup拦截了事件序列中的一次事件,则后续的事件都不会触发onInterceptTouchEvent(MotionEvent ev)。

4、onTouchEvent(MotionEvent event)

真正开始消费View事件的地方,具体的消费逻辑在其中实现。返回值表示是否消费了该事件。如果返回值表示没有消费该事件,则后续事件也不会到达该方法。

因为父View会维护一个可以消费事件的View的队列,后续事件会直接按照队列顺序发放,而不是以遍历View的方式。

5、ViewGroup/View

在ViewGroup中,上面的三个方法关系可以表述为

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
if (!consume && ev.getAction == MotionEvent.ACTION_DOWN) {
// down事件子View不处理则回传给父View处理
consume = onTouchEvent(ev);
}
}
return consume;
}

在View中,上面的方法关系可以表述为

1
2
3
4
5
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
consume = onTouchEvent(ev);
return consume;
}

二、源码分析

1、ViewGroup的事件分发

以下代码均省略了大部分逻辑,只提取了主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ... dispatchTouchEvent(MotionEvent ev)
// 从这段代码可以知道FLAG_DISALLOW_INTERCEPT对down事件是无效的,因为resetTouchState()会重置mGroupFlags的值。同时如果子view已经消费了事件序列中的事件,则可以通过设置FLAG_DISALLOW_INTERCEPT的值改变事件流向,使后续事件再让ViewGroup处理成为可能。
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// ...

// 如果ViewGroup没有拦截down事件,则会将down事件下发给子View,并且记录下消费了down事件的第一个子view,以便将接下来的事件都直接传递给他。但是如果所有子view都没有消费down事件,则接下来所有的子View都不会收到任何一个View事件
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 在addTouchTarget中会给mFirstTouchTarget赋值,mFirstTouchTarget用以存储消费了View事件的那个子View
newTouchTarget = addTouchTarget(child, idBitsToAssign);
}
}
}
}
// ...

// Dispatch to touch targets.
// 如果没有子view消费事件,则将事件交给当前ViewGroup处理。否则将事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
// 将事件分发给mFirstTouchTarget,如果事件被ViewGroup拦截,则遍历取消mFirstTouchTarget中的所有view,并向它们发送cancel事件,然后将其从队列中去除。如果事件没有被拦截并且mFirstTouchTarget也没有消费事件,则不会将mFirstTouchTarge队列清除。
}
// ...
2、View的事件分发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ... dispatchTouchEvent(MotionEvent ev)
// 优先处理onTouch的监听,再处理onTouchEvent(),然后在onTouchEvent()中响应up事件触发onClick()
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
// ...

三、一些结论

摘抄自 《Android开发艺术探索》

  • 同一事件序列是指手指接触屏幕到离开屏幕中产生的一系列事件。以 down 开始,中间一些 move,最后以 up 事件结束。
  • 正常情况下一个事件序列只能被一个 View 拦截且消费。但是可以通过调用其他View的 onTouchEvent 强行传递给其他 View 处理。
  • 某个 View 一旦决定拦截,那么这一事件序列都只能由它来处理(如果能传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。
  • 某个 View 一旦开始处理事件,如果不消费 ACTION_DOWN 事件(onTouchEvent 返回 false),那么同一事件序列中其他事件都不会交给他处理,并且Down事件重新交给它的父元素去处理,即父元素的 onTouchEvent 会被调用。
  • 如果 View 不消费除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 不会被调用,并且当前 View 可以持续收到后续事件,最终消失的事件会传递给 Activity 处理。
  • ViewGroup 默认不拦截任何事件。Android 源码 ViewGroup 的 onInterceptTouchEvent 默认返回 false。
  • View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递,调用 onTouchEvent 方法。
  • View 的 onTouchEvent 默认都会消费事件(返回true),除非它是不可点击的 (clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认 false,click 看情况。Button 的 clickable 默认为 true,TextView 的 clickable 默认为 false。
  • View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕 View 的 enable 为 false,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
  • onClick 发生的前提是 View 可点击,并且收到了 down 和 up 的事件。
  • 事件传递是由外向内的,先传递给父元素再由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent 方法可以干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

四、事件分发流程图

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

2、点击 View1 区域且事件被 View1 消费

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

五、控制View事件消费过程

在描述事件流控制之前先做一些表述上的约定。

下文出现的圆形表示各种View事件,圆形的颜色,绿色表示该事件被子View消费,蓝色表示该View被父View消费。

而消费则表示该事件的MotionEvent被传递到了父View或者子View的onTouchEvent()中,并且下面的所有表述都默认onTouchEvent()返回了true。实际上除了down事件对onTouchEvent敏感,其他事件的流向并不会受onTouchEvent的影响。

1、事件的消费从子View转移到父View

代码实现:

  • 父View的onInterceptTouchEvent不拦截事件,直到遇到可以拦截的事件,之后后续的事件均会交由父View处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercept = false;
    switch (ev.getAction()){
    case MotionEvent.ACTION_DOWN:
    intercept = false;
    break;
    case MotionEvent.ACTION_MOVE:
    intercept = couldIntercept();
    break;
    case MotionEvent.ACTION_UP:
    default:
    intercept = false;
    break;
    }
    return intercept;
    }
  • 父View的onInterceptTouchEvent不拦截down,但是拦截所有的move和up,然后子View通过设置父View的FLAG_DISALLOW_INTERCEPT来使父View的拦截是否生效。子View一开始让父View的intercept都失效,直到遇到有效的条件,则使父View的拦截生效,之后后续的事件均会交由父View处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 子View的代码
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
    case MotionEvent.ACTION_DOWN:
    // 初始不允许父View拦截
    parent.requestDisallowInterceptTouchEvent(true);
    break;
    case MotionEvent.ACTION_MOVE:
    if (couldIntercept()){
    // 当满足条件时,则使父控件拦截事件
    parent.requestDisallowInterceptTouchEvent(false);
    }
    break;
    default:
    break;
    }
    return super.dispatchTouchEvent(ev);
    }

    // 父View代码
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
    case MotionEvent.ACTION_DOWN:
    return false;
    default:
    return true;
    }
    }
    2、事件消费从父View转移到子View

    从源码上来看,这种情况是无法发生的,因为dispatchTouchEvent的逻辑决定了一旦View开始被父View消费,就不会再分发到子View中,因此想要实现这种事件流,需要去改动dispatchTouchEvent的逻辑。

六、参考资料