前言
在 Android 中,View 是十分庞大的一个体系。对于一个 APP 来说,与用户产生交互的各种控件都是通过最为基本的 View 扩展而来的,例如 TextView
、Button
等。而对 View 的学习,我会从两个方面入手,一个是事件分发机制,另一个是 View 的绘制流程。本着先易后难的原则,这一篇我们首先从源码的角度介绍 View 的事件分发机制。
进行事件分发机制的意义
对于这个问题,先举一个例子:
假设我们的手机屏幕现在有如上布局(并不标准,仅用于问题叙述),这个布局非常简单,仅有一个 ViewGroup
,它里面包含了一个子 View
。然后我们分别在两个黑点所示的位置产生点击事件,命名为点击1和点击2。可以看到,点击1产生的点击事件必然是由 ViewGroup
来处理这的;但是我们看到点击2,它点击的位置是在 View
里面,那么这个点击事件应当由谁来处理呢?在这里我们可能就有比较大的疑惑了,所以它是需要一个规则来进行这类情况的处理的。
可能你也已经猜到了,这类情况就是按着 View
的事件分发机制来进行处理的,这也就是事件分发机制存在的意义了,即由它来决定谁来干这个活(处理点击事件)。
准备工作
在开始真正分析源码之前,有几个概念我们先来理清楚,这有助于我们后面对源码进行理解。
点击事件
当我们点击手机屏幕的时候,就会产生点击事件,它被封装成了一个类:MotionEvent
。这个类里面有许多的点击事件类型,例如 ACTION_DOWN
、ACTION_UP
、ACTION_MOVE
和 ACTION_CANCEL
等,表示按下、移动、抬起和非人为因素的关闭等操作。我们产生的点击事件类型基本上可以由这上面3个组成。
例如我们点击一个按钮,就是先按下然后抬起手指,即 ACTION_DOWN
>> ACTION_UP
;而拖动一个列表时,就是按下,移动,最后抬起,即 ACTION_DOWN
>> ACTION_MOVE...
>> ACTION_UP
。这里需要注意的是 ACTION_MOVE
不止产生了一次,它会产生若干个 ACTION_MOVE
。
然后我们从这两个点击的例子中还可以注意到一个共同点,就是点击事件永远是以 ACTION_DOWN
为开始,以 ACTION_UP
为结束的。这其实也非常好理解,我们按下的时候,点击事件就开始了,直到我们抬手离开屏幕,这个点击事件也就结束了。
责任链模式
分发事件机制是一种比较典型的责任链模式,以上面展示的图为例,当 ViewGroup
收到这个点击事件以后,在条件允许的情况下,它会先向 View
分发这个事件,如果 View
处理了这个点击事件,那么这个分发就结束了;如果 View
不愿意处理这个事件,它会就将事件回退给 ViewGroup
,交由 ViewGroup
来处理这个点击事件。
也就是说,分发事件机制不外乎就是找到一个“责任人”来对这个点击事件进行处理,将责任落实到实处。刚才说到,只有在条件允许的情况下,ViewGroup
才会向 View
分发这个事件,那么这个条件是什么呢?其实就是我们接下来要介绍的拦截机制了。
拦截机制
在事件分发机制中,是依据拦截与否来决定是否由该视图来处理这个点击事件的。如果这个视图拦截了这一点击事件,表示它不再将事件继续向子 View
传递,而是自己处理掉(消费掉这个事件);如果不进行拦截,则表示它会先将这个点击事件向子 View
传递(分发这个事件),而后再决定是否由自己处理这个点击事件。
在源代码中,我们会有两个重要的 boolean
型变量 disallowIntercept
和 intercepted
。
disallowIntercept
的作用是判断是否允许拦截,不允许拦截的话,事件就会直接分发给子 View
;而如果允许拦截的话,就有了选择拦截的权利。这里要特别注意一点,允许拦截不代表一定会拦截!这里相当于提供给了我们的视图一个可以拦截的权利。也就是说,disallowIntercept
的值为 true
时,我们可以选择拦截下这个事件,自己消费掉,也可以不拦截,向子视图分发这个事件;而当值为 false
时,只能选择向子视图分发这个事件了。
而拦截与否的决定权是交由 intercepted
的,当 intercepted
值为 true
时,表示拦截了这个点击事件,也就表示要自己处理了!而为 false
时,顾名思义就是不拦截,果断向子视图分发事件。要注意到当 disallowIntercept
值为 false
时,intercepted
的值必定为 false
,这也很好理解,不准进行拦截,那就只能选择分发啦。
Activity 的构成
要理解事件分发机制的源码,还是需要知道 Activity 的构成是什么样子的,限于篇幅,这里就不再做赘述,如果你还不知道的话,可以先看下笔者的这篇文章 Activity 的构成,里面对于 Activity 的构成做了介绍和分析。
好了,准备工作做完之后,我们就来开始分析源码吧!首先申明一点,笔者在这里进行源码分析时,只会截取出与该主体有关的关键代码进行分析,不会讲解整片代码,如果想要研究整片代码的话也可以在看完笔者的分析后再去自行研究。
源码分析
Acitvity 层
当我们的点击事件产生之后,点击事件首先会传入我们的 Activity
,调用的方法是 Activity 的 dispatchTouchEvent
方法,我们来看看这个方法里面是做什么的:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
看到第一个 if
语句,如果为 ACTION_DOWN
的话,就会调用 onUserInteraction
方法。我们前面分析过,每一个点击事件的开头都是 ACTION_DOWN
,所以必然会进入到这个 if
语句里面去调用 onUserInteraction
方法,那么我们就来看看这个方法做了什么:
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
可以看到这居然是一个空方法,从注释中可以得知当点击 home,back,menu 键等都会调用此方法,如果希望知道用户在 Acitvity
运行时以何种方式与设备进行了交互,那么可以实现这个方法,这个方法不是我们这一篇的重点,所以我们跳过,直接看第二个 if
语句。
第二个 if
语句里面首先调用了 getWindow
方法,这个方法会返回 mWindow
。我们知道 PhoneWindow
是 Window
这个抽象类的唯一实现类,换言之 mWindow
是 PhoneWindow
类型的,所以调用的 superDispatchTouchEvent
方法也就是 PhoneWindow
这个类下的了,我们来看看它做了什么:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可以看到,这个方法直接返回了 mDecor
的 superDispatchTouchEvent
方法,mDecor
是 DecorView
类型的变量,它是 PhoneWindow
的一个子类。我们点进去这个类 superDispatchTouchEvent
的看看里面有什么:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
从这里我们可以看到,它返回的是父类的 dispatchTouchEvent
方法,而 DecorView
是继承自 FrameLayout
的,而 FrameLayout
又是继承自 ViewGroup
, 所以也就相当于我们返回的 super.dispatchTouchEvent(event)
实际上返回的是 ViewGroup
的 dispatchTouchEvent
方法。
好了,分析完了这段代码之后,我们先回到 Activity
的 dispatchTouchEvent
方法,为了方便观看我重新贴出来:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 实际调用的是 ViewGroup 的 dispatchTouchEvent 方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
分析完了第2个 if
语句里面的方法,我们看到如果 superDispatchTouchEvent
返回 true
,就意味着这个点击事件被处理了,那么 Activity
的 dispatchTouchEvent
方法也就会返回 true
。换句话说也就是如果 ViewGroup
的 dispatchTouchEvent
方法返回 true
就返回 true
,否则它就会调用 Activity
的 onTouchEvent
方法。既然如此,我们就来看看 onTouchEvent
方法里面做了什么吧:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
这段代码我就直接解释作用了,首先 if
语句判断是否应该结束掉这次点击事件,如果为 true
就结束 Activity
并返回 true
,否则就返回 false
。需要注意的是这个方法一般情况下都不会被执行,因为它的执行情况是这个 Activity
下的所有子 View
都不会处理这个点击事件,所以才会将这个点击事件回退到这个方法进行处理,而一般情况下我们总会有一个 View
愿意处理这个点击事件的。
分析完了 Activity 的 dispatchTouchEvent
方法,我们来对它做一个小结:
Acitvity
是分发事件的起点,由它开始向下经过一系列的处理,最终交由ViewGroup
的dispathcTouchEvent
方法进行分发。- 如果
ViewGroup
的dispatchTouchEvent
返回true
,表示这个点击事件已经被处理了,Activity
的dispatchTouchEvent
就会直接返回true
,否则它就会执行onTouchEvent
方法,因为ViewGroup
的点击事件被回退回来了,所以只能由Activity
自己来处理。 - 从第2条的结论我们可以知道,
Activity
的onTouchEvent
方法也就是回退点击事件的终点了。
为了将结论更清晰的表达出来,上面所说的过程可以用下图进行表示:
ViewGroup 层
好了,在我们理清了 Activity
的 dispatchTouchEvent
方法就是要调用 ViewGroup
方法之后,我们接下来就来对 ViewGroup
的 dispatchTouchEvent
方法进行一个分析吧!
第一部分
这个方法的代码非常的长,我们只从中截取出了关键部分,并分为两部分来进行分析。首先来看看第一部分的代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
......
// 第一部分
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;// 1
// 关键点1:如果点击事件为 ACTION_DOWN,就将进行初始化
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// 将 mFirstTouchTarget 设置为 null
resetTouchState();
}
// 关键点2
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // IF1
// 是否允许进行拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果允许拦截
if (!disallowIntercept) {
// 关键点3:对 intercepted 进行赋值,onInterceptTouchEvent 方法
// 默认会返回 false,即不进行拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else { // 不允许拦截的话就直接放行,不拦截
intercepted = false;
}
} else {
// 关键点4:当 mFirstTouchEvent == null && 非 ACTION_DOWN 事件
intercepted = true;
}
......
}
在关键点 1 处,actionMask
先获取了点击的类型,如果为 ACTION_DOWN
的话,说明产生了一个新的点击事件,所以我们要进行初始化操作,以免上一次遗留下来的东西影响了这次的点击事件。那么我们的初始化操作做什么呢?
我们在关键点2处下面的 if
语句中会看到一个变量 mFirstTouchTarget
,这个变量非常关键,它在初始化操作的时候首先会被置空。然后接下来我们说说它的作用:mFirstTouchTarget
为空时,表示当前的 ViewGroup
拦截了这个事件或者 ViewGroup
下的子 View
没有一个可以处理这个事件;而当 ViewGroup
下的子 View 处理了这个点击事件时,当前 mFirstTouchTarget
就为非空。所以我们的初始化会将 mFirstTouchTarget
置空,以免之前的事件影响到了这次点击事件的处理。
接下来我们回到关键点2处的开头,可以看到有一个 boolean
型的变量 intercepted
,它用于表示是否要对点击事件进行拦截。然后我们先看到 IF1
处的条件判断,我们刚刚把 mFirstTouchTarget
置空,所以要进入这个 if
,只有可能为 ACTION_DOWN
了。这个 if
这么判断是为了屏蔽除 ACTION_DOWN
以外的所有事件,因为我们的事件序列开端只需要关注 DOWN 事件就够了。
我们接着往下看到关键点3处的 if
语句,它会先会判断是否允许拦截,disallowIntercept
变量可以通过该类的对象直接调用方法 requestDisallowInterceptTouchEvent
进行赋值。如果允许拦截的话,intercepted
就会通过 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;
}
这个方法的 if
里面的条件在默认情况下是不成立的,所以这个方法在默认情况下会返回 false
,如果我们在自定义的控件中希望拦截下这个事件自己处理,那么就可以重写这个方法返回 true
。
我们继续回到 dispatchTouchEvent
方法的关键点4处,如果为其他非 DOWN
事件并且 mFirstTouchTarget == null
,说明这个事件被 ViewGroup
拦截下来自行处理了,也就是说 ViewGroup
自己会去处理这个点击事件,所以将 intercepted
设置为 true
。
总结一下:
- 这部分代码是通过获取到的
MotionEvent
的类型,如果为DOWN
事件,代表一个点击事件序列的开端,就会执行初始化,将变量mFirstTouchTarget
置空,然后通过两个boolean
型变量disallowIntercept
和intercepted
进行判断是否拦截该点击事件。 disallowIntercept
变量可以通过外部调用requestDisallowInterceptTouchEvent
方法进行赋值,intercepted
可以通过重写onInterceptTouchEvent
方法进行赋值。
第二部分
在判断完是否需要拦截之后,我们来继续阅读第二部分的源码吧。
public boolean dispatchTouchEvent(MotionEvent ev) {
......
// 关键点1
if (!canceled && !intercepted) {
......
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
......
// 关键点2
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
......
// 关键点3
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 关键点4
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
我们先看到关键点1处,如果 ViewGroup
不进行拦截并且点击事件没有被取消的话,就会执行下面 if
中的语句。里面使用了一个 for 循环来遍历 ViewGroup
中的子 View
,值得一提的是这个 for
循环是按照倒序遍历的,也就是从最外层的子 View
往内层遍历。
接下来看到关键点2处,这个 if
主要是判断点击事件位置是否在子 View
范围内或者子 View
是否在播放动画。如果均不符合说明这个子 View
不是我们要寻找的子 View
,直接 continue
跳过。
我们继续看到关键点3处的 if
语句,我们看到 if
语句中有一个方法 dispatchTransformedTouchEvent
,我们来看看它做了什么:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
可以看到在这个方法的 if
语句中,如果传进来的 View
类型参数 child
为空,也就说明这个点击事件的位置下 ViewGroup
是没有子 View
的,那么 ViewGroup
就只能选择自己处理这个事件,所以调用的是父类 View
的 dispatchTouchEvent
方法,如果 child
非空,那么就会调用 child
的 dispatchTouchEvent
方法了。
View 层
假设我们的 child
就是 View
类型的,我们来看看 View
下面的 dispatchTouchEvent
方法做了什么吧:
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
// 关键点1
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 关键点2
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
在关键点1处,我们可以看到这个 if 语句满足的条件是:
- 设置了外部监听
ListenerInfo
- 该
View
必须是ENABLE
的 - 监听的
onTouch
方法返回true
如果均满足的话,返回值 result
就会被设置为 true
,其中 onTouch
方法是个接口方法,如果我们没有实现这个方法的话,即默认情况下,第一个 if
语句的条件不会成立。如果第一个 if
条件不被满足的话,onTouchEvent
方法就会得到执行,因为此时 !result == true
,所以第二个 if
会执行这个方法查看它的返回值。我们来看看这个方法里面的代码是什么:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 关键点1:判断是否是可点击的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { // 关键点2
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick(); // 关键点3
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
......
}
return true;
}
return false;
}
这个方法也是非常的长,我们主要看两个点,一个是关键点1,在这里,我们定义了一个 boolean
型的变量 clickable
判断 View
是否是可点击的,可以看到 CLICKABLE
和 LONG_CLICKABLE
都属于可点击类型,它们分别对应点击事件的点击和长按。
接下来看到关键点2,clickable
为 true
时,进入到这个 if
语句,在这个 if
语句里面会用到 switch
对多种点击事件类型进行不同的处理,这里我们只关注 UP
事件,因为这是点击事件序列结束的标志,之后它就会执行相应的点击处理了。
最后经过一系列的执行,我们会来到关键点3处,执行 performClick
方法,这个方法的源代码如下所示:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
从它的 if
语句可以看出,如果 View
设置了点击事件 onClickListener
,那么它就会返回 true
,它的 onClick
方法就会得到执行。这个 onClick
方法就是我们平常经常使用到的那个按钮监听方法了。所以 onTouchEvent
方法对应的就是我们设置的 onClick
方法。而且有一点需要特别注意的是,只要 View
是可点击的,onTouchEvent
方法就会返回 true
,换言之 View
的 dispatchTouchEvent
方法也会返回 true
,也就是说 View
会消费掉这个点击事件。
总结一下:
- 默认情况下
View
处理事件的方法是onTouchEvent
,除非onTouch
方法被实现并返回true
。 - 只要
View
是可点击的,onTouchEvent
方法就会返回true
。 View
的dispatchTouchEvent
方法可能处理事件的地方有两处,分别是调用onTouch
方法和onTouchEvent
方法。- 可以看出,如果我们在设置了
onTouch
监听并返回true
的话,那么result
在第一个if
语句中就会被设置为true
,所以第二个if
语句的onTouchEvent
方法也就不会被执行。onTouch
先于onClick
获得点击事件。
分析到这里,假设我们子 View
的 dispatchTouchEvent
方法返回了 true
,说明这个点击事件被消费了,那么它的父视图 ViewGroup
的 dispatchTouchEvent
方法也会返回 true
,继续向上通知 Activity
的 dispatchTouchEvent
方法不用执行 Activity
的 onTouchEvent
方法了,这个事件已经被消费掉了!因为前面我们分析过,只有 ViewGroup
的 dispatchTouchEvent
方法也会返回 false
时 Activity
的 onTouchEvent
方法才会得到执行。
回退
从我们刚才分析完整个事件,一切看起来都是那么的和谐,但是我们没有考虑到一种情况:如果子 View
拒绝处理事件,即 View
的 dispatchTouchEvent
返回 false
时,该怎么处理呢?讲到这里,我们就要返回到我们 ViewGroup
的 dispatchTouchEvent
方法的第二部分了,第二部分剩下的代码如下所示:
public boolean dispatchTouchEvent(MotionEvent ev) {
......
// 关键点4
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
我们看到关键点4处,假设我们的子 View
的 dispatchTouchEvent
方法返回 false
的话,根据我们之前的分析,ViewGroup
的 mFirstTouchEvent
就会被设置为空,那么这个 if
里面的语句就会得到执行,这里面调用了 dispatchTransformedTouchEvent
方法,注意到它的参数 child
直接传入了 null
,也就是自己执行 super.dispatchTouchEvent
方法,这也就是所谓的回退,因为子 View
拒绝处理点击事件,所以会回退给 ViewGroup
自行处理,而假设 ViewGroup
的 dispatchTouchEvent
也返回 false
的话,它也会继续回退,依次类推,可以一直回退到 Activity
处,Activity
发现没有任何 View
愿意处理这个事件,就只能调用 onTouchEvent
方法自行处理了。
流程图
所以完整的事件分发的完整流程图应当是这样子的:
这个图应该就很清晰了吧,从最左边看起,向上的箭头代表的是底层的 View
一直向上对自己的父视图进行通知是否事件分发完成了。
然后左二一路向下的箭头就是父视图向子 View
分发事件的过程,可以看到只有 ViewGroup
需要判断是否需要进行拦截。
然后就是最右边看到我们 onTouchEvent
方法这一列,说明的就是我们上面所说的回退进制了。这个图其实说的不准确,因为通过源码我们也知道,父视图的 dispatchTouchEvent
方法只会根据子 View
的 dispatchTouchEvent
的返回值来进行判断,而 onTouchEvent
方法在默认的情况下,其实是在 dispatchTouchEvent
内部被调用的,所以实际情况应当是子 View
和 ViewGoup
的 onTouchEvent
方法之间是个间接影响的过程,并不是直接影响,这是上面这个图不准确的地方。要注意:当我们实现了 onTouch
方法的时候我们应当根据我们的实际情况进行判断!因为 onTouch
的优先级是高于 onTouchEvent
的。
好了,本篇事件分发机制就介绍完了,祝大家学习愉快!