1、前言
事件分发可以说是 Android 众多知识点中最为基础且最重要的之一了,吃透这块知识点不仅能让你在解决滑动冲突时由仍有余,也能为你在面试过程中轻松过关斩将。
2、MontionEvent + 3个重要方法
MontionEvent
事件分发的起源点来自哪?
答:用户对手机屏幕进行指尖的触摸、亦或是轻轻的抚摸、又或是一顿狂风暴雨般的猛击。
Android 将我们对手机这一系列操作封装到了 MontionEvent 对象中
常见的几种:
事件类型 | 具体操作 |
---|---|
MontionEvent .ACTION_DOWN | 手指触摸屏幕(内部对应点击某个View) |
MontionEvent .ACTION_MOVE | 手指滑动屏幕(内部对应滑动某个View) |
MontionEvent .ACTION_UP | 手指离开屏幕的一瞬间(与DOWN对应) |
3个重要方法
接着我们先来看看事件分发中主要的三个方法,先看看结果再来分析过程,这段简单了解即可,
方法 | 作用 | 调用 | view中是否存在 | ViewGroup中是否存在 |
---|---|---|---|---|
dispatchTouchEvent | 分发点击事件 | 当点击事件传递到当前View时 | 存在 | 存在 |
onInterceptTouchEvent | 拦截点击事件 | ViewGroup 内部的 dispatchTouchEvent() 内部调用 | 不存在 | 存在 |
onTouchEvent | 处理点击事件 | dispatchTouchEvent()内部调用 | 存在 | 存在 |
三个方法都是 boolean 类型返回值的方法,true 即表示自己消费,false 则继续分发,一段伪代码简单描述上述的三个方法的关系:
//摘自艺术探索
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
3、Activity 分发
现在我们从源码的角度简单的走一遍分发的流程,当我们的 Activity 接收到用户的触摸屏操作时,便会调用 Activity 的 dispatchTouchEvent(), 如下
public boolean dispatchTouchEvent(MotionEvent ev) {
//可以忽略这一部分代码
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//当此activity在栈顶时,点击按home、back、menu等键都会触发此方法
onUserInteraction();
}
//1、点进去会走到Window 类中的 superDispatchTouchEvent
//2、然后 Window 的实现类只有PhoneWindow,类注释中有明确说明
//3、如下有贴出PhoneWindow 的superDispatchTouchEvent
//和 DecorView 的superDispatchTouchEvent,先去看这俩方法的注释再回来
if (getWindow().superDispatchTouchEvent(ev)) {
//4、欢迎回来.若superDispatchTouchEvent返回true,
//即ViewGroup或者View的dispatchTouchEvent返回true,则事件结束
return true;
}
//5、如果ViewGroup或者View都没有消费事件则会返回false,<--面试重点
//那么事件将会交给Activity的onTouchEvent处理。
return onTouchEvent(ev);
}
//走进#PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//mDecor 指的是 DecorView 的实例
//DecorView 其实就是 activity 窗口的根视图
return mDecor.superDispatchTouchEvent(event);
}
//走进#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
//这个 dispatchTouchEvent 其实就是调用的 ViewGroup 中的 dispatchTouchEvent
//这个 ViewGroup 就相当于每个界面的顶层View(根View)
//好了,可以回去了...
return super.dispatchTouchEvent(event);
}
该说的都在码里了,我干了,你随意...
这部分只是 Activity 中的分发,接着我们走进 ViewGroup 瞧瞧
4、ViewGroup 分发
接着我们来分析 ViewGroup 中的 dispatchTouchEvent(),因为代码比较多所以我们分段进行分析:
// 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;
}
上面的代码主要是描述当前 View 是否拦截点击事件这个逻辑。
第一个 if 判断中:
第一个判断很好理解,根据用户操作手指进行判断;
第二个mFirstTouchTarget: 这个对象结合后面的代码逻辑可知,在 ViewGroup 的子 view 处理成功时,mFirstTouchTarget 会被赋值并指向子 View ,所以反推可知此时的 mFirstTouchTarget == null;那么当事件来到 ACTION_MOVE 和 ACTION_UP 的时候该 if 语句将会不成立,这就导致 ViewGroup 的 onInterceptTouchEvent() 不会被调用,并且 ViewGroup 同一事件序列中的其他事件都会交给它处理;
disallowIntercept默认为false,在代码中我们可以通过requestDisallowInterceptTouchEvent() 来设置,一旦设置之后,ViewGroup将无法拦截除了 ACTION_DOWN 以外的点击事件,为什么说除 ACTION_DOWN 以外呢?因为在 ViewGroup 分发的时候,如果是 ACTION_DOWN 将会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,
以下是事件为 ACTION_DOWN 时重置 FLAG_DISALLOW_INTERCEPT 标记位的源码:
// 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();
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
根据源码分析可知:
当 ViewGroup 进行拦截事件时,只有事件为 ACTION_DOWN 时才会调用 onInterceptTouchEvent(),并且 FLAG_DISALLOW_INTERCEPT 这个标记位也可以来控制 ViewGroup 是否进行拦截,在处理滑动冲突的时候可以利用这一特性来解决。
以上分析的是 ViewGroup 拦截事件的源码,接着来看看当 ViewGroup 不对事件进行拦截时的情况会是如何:
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
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;
}
//dispatchTransformedTouchEvent 内部调用
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
总结一下:
首先会遍历 ViewGroup 所有的子 View ,然后判断子 View 是否有接收到点击事件,如果有则事件便会传递给它,dispatchTransformedTouchEvent 实际上就是调用了子 View 的 child.dispatchTouchEvent 方法,这样事件就从 ViewGroup 传递到了 View上,交由 View 去进行一轮新的分发,这里暂不讨论子 View 具体如何分发,如果 child.dispatchTouchEvent(event) 返回了 true ,那么 if 语句成立 ,mFirstTouchTarget便会被赋值,并且跳出 for 循环,如下所示:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
还有一种情况便是 child.dispatchTouchEvent(event) 返回了 false ,那么便会接着遍历分发(在还有下一个 View 的情况下),如果遍历完所有的子 View 都没有被处理,要么就是 ViewGroup 没有子 View ,要么就是子 View 内部自己做了处理,在 dispatchTouchEvent 或者 onTouchEvent 方法中返回了 false ;如果是这样的话那么 ViewGroup 便会自己去处理点击事件,如下所示:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
上述代码说的很清楚,当mFirstTouchTarget为null时,同样会调用 dispatchTransformedTouchEvent 方法,但是注意第三个参数(View child),传的是个 null,再看一遍:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
这个判断主要是用来处理当 ACTION_MOVE 和 ACTION_UP 事件到来时,改 if 语句就会不成立,这样就是导致 ViewGroup 的 onInterceptTouchEvent() 不会再被调用,并且同一事件序列中的其他事件都会默认交给它处理。
5、View 分发
View 的事件分发相比 ViewGroup 就相对简单一些了,先来看看它的 dispatchTouchEvent 方法:
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
......
return result;
从源码中可以看出,会先去判断当前 View 是否有设置 onTouchListener 监听事件,如果有的话,事件会交给 onTouchListener 中的 onTouch 处理,则不会传递到 onTouchEvent 方法,从这里可以说明, onTouchListener 的优先级要高于 onTouchEvent 方法,这样处理的目的就是为了方便用户自己去处理点击事件 。
接着看看 onTouchEvent 的实现,首先来看看 View 处于不可用状态 (指 TextView 、ImageView等)下点击事件的处理过程,如下:
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;
}
很显然,不可用状态下的 View 同样会消耗掉点击事件,即便它看起来是不可用的。
接着往下走,如果 View 有设置代理,那么还会执行 TouchDelegate 的 onTouchEvent 方法,这个机制跟 onTouchListener类似
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
接着往下走,来看看 onTouchEvent 对事件的具体处理
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
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) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
break;
...
return true;
}
从上面 if 语句就可以看出只要 View 的 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE(代理),有一个为 true ,便会消费掉事件,不管它的状态是不是可用状态,当 ACTION_UP 事件发生时,会触发 performClick 方法,如果 View 设置了 OnClickListener 监听事件,则会调用它的 onClick 方法
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;
6、总结
关于这个知识点可以从三个步骤进行理解,也是事件分发的流程
Activity --> ViewGroup -->View
Activity
事件分发首先从 Activity 开始进行分发,在 Activity 的 dispatchTouchEvent() 中会将事件分发到 window 上,而 window 是个抽象类,所以会把具体工作交给它的唯一实现类 PhoneWindow 上,而 PhoneWindow 又会将事件传递到 DecorView 上,而 DecorView 其实就相当于当前界面的底层容器,接着这个事件会来到容器的顶层 View ,一般来说这个顶层 View 就是 ViewGroup。
ViewGroup
事件首先会来到 dispatchTouchEvent 方法,如果 onInterceptTouchEvent 方法返回 true 表示对事件进行了拦截,那么该事件便会交给 ViewGroup 的 onTouchEvent 方法进行消费;
如果 onInterceptTouchEvent 方法返回 false 即表示没有对事件进行拦截,那么 ViewGroup 则会去遍历子View ,如果子 View 没有做特殊处理的情况下,事件便会顺利的传递到子 View ;如果子 View 在 dispatchTouchEvent 或者 onTouchEvent 方法中返回了 false ,那么该事件还是会交给 ViewGroup 处理,最终会传递到 Activity 的 onTouchEvent 方法进行消费掉;
View
View 的分发同样是从 dispatchTouchEvent 方法开始,但是 View 没有 onInterceptTouchEvent 方法,当 View 事件传递到 onTouchEvent 之前会先判断是否有设置 onTouchListener 监听,如果有的话事件便交给 onTouchListener 的 onTouch 处理,如果没有设置,则会交给 onTouchEvent 进行处理,当 View 有设置 onClickListener 监听时,事件最终便会传递到 onClickListener 的 onClick 方法。
所以在 View 中的优先级如下:
onTouchListener > onTouchEvent > onClickListener
参考:艺术探索