事件体系简介
既然是View的事件分发,那么首先我们应该知道什么是Android体系中的事件,它在code世界中又是如何定义的,下面我们简单介绍一些MotionEvent的知识。
MotionEvent
Android体系中将所有的输入事件都放到了MotionEvent中,实际上MotionEvent已经发展到十分复杂,由于本文的重点不在这里,所以就单点触控事件进行介绍。
单点触控
单点触控中有以下几个事件:
- ACTION_DOWN 手指初次接触到屏幕上时触发,简单地说就是手指按下触摸到屏幕的时候
- ACTION_UP 手指离开屏幕时触发,及手指抬起离开屏幕时
- ACTION_MOVE 手指在屏幕上滑动时触发,当手指在屏幕上连续一段距离后被识别为滑动,滑动在一次事件序列中可能会被触发多次,并不唯一
- ACTION_CANCEL 当事件传递下来后又被上层收回时触发
- ACTION_OUTSIDE 手指触控的区域不在控件上方时触发
一次单点触控流程可以大概描述为手指按下(ACTION_DOWN)——>手指在屏幕上滑动(ACTION_MOVE)——>最后手指离开屏幕(ACTION_UP),这是用户一般使用的简化过程。了解过这些基础事件后,我们再看几个MotionEvent常用API:
- getAction() 获取上述事件类型
- getX() 获取触摸点相对于View的X轴坐标
- getY() 获取触摸点相对于View的Y轴坐标
- getRewX() 获取触摸点相对屏幕的X轴坐标
- getRewY() 获取触摸点相对屏幕的Y轴坐标
上面介绍的知识中首先要强调的是ACTION_MOVE在一次事件流程中可能触发的次数不唯一,其次就要介绍一下比较冷门的两个事件ACTION_CANCEL,ACTION_OUTSIDE。
- ACTION_CANCEL
ACTION_CANCEL顾名思义取消事件,既然叫做取消那么就肯定存在先获取然后被收回这样一个过程,举个例子:你收到了一个会议消息,然后你又被告知这个会议被取消了;对应于View事件分发就是指事件序列分发到了某个环节,但他的上层又收回了这个事件序列,这时这个环节就会收到上层给他的ACTION_CANCEL事件.
- ACTION_OUTSIDE
对于这个事件可能大家会觉得不可思议,明明触摸事件的发生点都不在该控件的所在区域,看起来不相关的两者是如何联系到一起的呢。实际上这种情况的确存在而且也具有它的意义。比如我们常用的Dialog,是不是会有这样的情况,我们点击它本身的关闭或取消按钮,而是点了一下它周围的其他区域,Dialog依然能够关闭;这种情况很多弹窗上出现的很多,就是这个ACTION_OUTSIDE事件的作用。
对于Dialog是如何收到这个ACTION_OUTSIDE事件的,这主要是通过WindowManager的布局参数的flags设置为FLAG_WATCH_OUTSIDE_TOUCH,这样Daialog就具有了观察并处理超出本身范围的事件的能力
正文
Android事件分发从当前Activity开始,在到该Activity持有的Window,又由Window将事件传递给DecorView,最后又由DecorView再将事件分发给子View;如果子View并不消费该事件,那么该事件序列又会被交给该View的父容器,再到DecorView,再到Window,最后又回到了Activity。这意味着如果一直没有View消费该事件序列,那么事件会回到Activity并被处理掉。
当不考虑事件序列中途被拦截并消费,只考虑事件分发到View的情况,比如一个Button或者一个TextView。若该View并没有消费该事件的话,那么最多就会出现上述流程
Activity的事件分发
Activity的事件分发在其dispatchTouchEvent方法里进行了处理,我们看源码:
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION.DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent(ev)){
return true;
}
return onTouchEvent(ev);
}
这里我们可以看到Activity直接将事件交给了Window处理,如果Window的superDispatchTouchEvent方法返回false,那么Activity就会调用他的onTouchEvent方法,这个里如何具体实现我们后面再看。那么事件已经到了Window了。
Window的事件分发
我们知道Window是抽象类,唯一实现类是PhoneWindow,又因为该superDispatchTouchEvent是抽象方法,那么我们只有去PhoneWindow里去看该方法的实现,看源码:
public boolean superDispatchTouchEvent(MotionEvent ev){
return mDecor.dispatchTouchEvent(ev);
}
相比于Activity将事件传到Window来说,Window的做法则要更加简单粗暴,这里直接传递到了DecorView,而这个DecorView就是我们一般说的顶级View,我们在layout包里写的xml文件就是它的子View。一般来说这个DecorView都是一个ViewGroup,所以事件序列来到了ViewGroup,我们继续看。
ViewGroup的事件分发
ViewGroup大家应该已经比熟悉了,一般来说ViewGroup的很多逻辑
处理都跟其自身和其子View息息相关,就像View的measure、draw和layout方法类似,这里也很相似,首先我们来看看几个比较重要的方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用于事件的分发。当事件传递到了该View后,该方法一定会被调用,上面关于Activity等的事件分发时,大家应该都看到了这个方法。该方法的返回值由其自身的onTouchEvent()方法和子View的dispatchTouchEvent方法共同决定。
public boolean onInterceptTouchEvent(MotionEvent ev)
此方法会在上述方法内部被调用(View为ViewGroup时),具体的View不会调用该方法。此方法用于判断是否拦截某个事件,返回结果表示是否拦截当前事件。当同一个事件序列中的某个事件被拦截后,此方法不会被再次调用。
public boolean onTouchEvent(MotionEvent ev)
用于处理点击事件,返回结果用于判断是否消费事件,如果不消费,那么此view不会再收到该事件序列的其他事件。
下面我们结合ViewGroup的源码来分析它的事件分发机制,首先看dispatchTouchEvent方法,源码内容很长,我们一段一段的分析:
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down. 注释1
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.注释2
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.注释3
intercepted = true;
}
这里定义了一个名为handled的布尔值,根据命名我们可以猜测为是否处理的,显然事件还没有被处理,这也跟我们说的viewGroup的dispatchEvent方法默认不拦截事件相应证,然后再看注释1处,如果当前事件是ACTION_DOWN(下文简称down事件)的话,会调用两个方法,又看命名,第一个方法应该是清除并取消触摸的目标,第二个方法应该是重置触摸状态,那么到底与我们的猜测是否一致呢,通过查看这两个方法的源码和注释,与我们的猜测完全一致:
/**
* Cancels and clears all touch targets.
* 取消并清除所有触摸目标
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
//mFirstTouchTarget 一个成员变量,初始为null,如果事件被子view处理完后
//这个值将被赋值并指向子view,
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
//遍历单链表
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
//清除触摸目标 进行了单链表的删除操作
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
再看另一个方法:
/**
* Resets all touch state in preparation for a new cycle.
* 重置所有状态以进入新周期
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
再次回,到dispatchTouchEvent方法上来,看注释2处又做了什么,这里有定义了一个布尔值,表示是否拦截事件,然后的判断内容是如果事件down或者mFirstTouchTarget不为空的话则执行onInterceptTouchEvent方法,是不是down事件很好判断,前面说到mFirstTouchTarget在子view处理了事件后会被赋值,而想要交给viewgroup处理时,显然它等于null。所以onInterceptTouchEvent方法只会执行一次在这里就可以看出,并且当down事件被拦截后,之后的整个事件序列都会交给他处理(注释3)。但是在调用onIntercepTouchEvent方法之前还有一个叫disallowIntercept的布尔值,用于判断是否禁止拦截,主要受FLAG_DISALLOW_INTERCEPT这个标志位的影响,因为这个标志位子view可以通过一定方法来改变,使viewgroup不拦截该事件并将它传递到子view上,这听起来似乎可以用来解决滑动冲突,但是这得要求这个事件不是down事件,上文已经分析过了,因为此时会进行一系列重置工作,包括标记位。如果down事件并没有拦截呢,那么这个时候就要将事件传递给子View了(这可能会有点长):
//获得所有子view的数量
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
//创建了一个用于向子view分发touch事件的list
final ArrayList preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍历子view
for (int i = childrenCount - 1; i >= 0; i--) {
//获取子view的脚标
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//通过脚标拿到子view对象
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;
}
//如果view在播放动画或者点击事件的坐标不在该view区域内 则遍历下一个
//否则交由它处理
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//从单链表中拿到这个值为child的TouchTarget对象
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);
//调用dispatchTransformedTouchEvent方法,用于判断子view是否拦截事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
//记录时间
mLastTouchDownTime = ev.getDownTime();
//记录子view的脚标
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();
//为新触摸目标赋值 将child添加进链表并返回值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//已经分发给新的触摸目标设置为true
alreadyDispatchedToNewTouchTarget = true;
//跳出循环,因为处理事件的view已经有了
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
//清空存放view的list
if (preorderedList != null) preorderedList.clear();
}
上面这一大片代码主要完成了遍历子View,然后判断子view是否能够接受到事件,如果能则将事件分发给子view,如果子view拦截该事件,就记录下它然后设置为newToucthTarget。判断子view是否拦截是dispatchTransformedTouchEvent这个方法来完成的,我们看看是怎么实现的:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
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;
}
···
}
这里只截取了最主要的一段代码,可以看到如果子view不为空,就调用子view的dispatchTouchEvent方法,否则调用父类View的dispatchTouchEvent方法,最后返回结果。如果子view拦截了事件返回true,那么viewgroup就会记录下它,并为newTouchTarget赋值,然后跳出循环不再遍历,这一点参见上面代码中的注释。到这里还会出现另一种情况,那就是子View的dispatchTouchEvent都返回了false,整个遍历过程都没有找到view来处理事件怎么办呢,显然这时候只能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);
}
这里又调用了dispatchTransformedTouchEvent方法,需要注意的是传入的child参数为null,根据上面的分析,这里会将事件交给View来处理,这就是Viewgroup的处理逻辑,下面再看view的事件处理过程是怎样的。
View的事件分发
view相比ViewGroup没有onInterceptTouchEvent方法,一点有事件传递到它,那么它的onTouchEvent方法就一定会被调用。view的onTouchEvent方法默认会消费事件,除非它是不可点击的,clickable和longClickable同时为false,所有view的longClickable默认都为false,,clickable就各不相同了,比如Button的clickable默认为true,TextView默认为false;但是当我们为TextView设置点击事件监听的时候,它依然能够消费事件,这主要是因为setOnClickListener的时候,会将clickable设置为true。下面来看看view的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
...
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;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
从上往下可以看出,如果OnTouchListener不为null,并且是可以点击的则执行它的onTouch方法,如果onTouch返回true,那么事件处理就结束了,就不会执行下面的onTouchEvent方法,显然onTouchListener优先级高于onTouchEvent。然后我们再看onTouchEvent:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {//注释1
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;
}
//注释2
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
}
看到注释1处,当view处于不可用状态下,依然能处理点击事件,就像源码注释写的那样,这些view并不会响应,但依然消费了触摸事件。注释2处是view的代理来处理点击事件,处理机制与OnTouchListener相似,就不分析了。现在来看看在onTouchEvent中对具体的事件是如何处理的:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
//如果根本就是不可点击 那么设置的相关callback都会被移除
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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)) {
performClickInternal();
}
}
}
...
}
...
break;
}
...
return true;
}
从上面的代码可以看出只要CLICKABLE和LONG_CLICKABLE有一个为true,即为可点击状态,那么它就会消费这个事件。点击事件的触发在action_up这个事件,这个事件里可以看出主要调用了performClickInternal方法,其实这个方法也主要是调用的performClick方法,如果view设置了onClickListener,那么就会调用它的onClick方法:
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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;
}
一开始我们就说过,在设置clickListener的时候,clickable会被设置为true,所以当我们为View设置OnClickListener或OnClickListener的时候,只要事件传递到它这里来,那么就会执行我们设置的逻辑。到这里View的事件分发也就讲完了,对事件分发的清楚认识对于滑动冲突的处理和自定义view都有极大的帮助。