Android事件分发机制是android工程师必须掌握的基础知识,网上关于事件分发的文章有很多,这里我来阐述下自己对事件分发的看法。
何为事件?事件就是用户手指触摸屏幕时,产生的的一系列触摸行为。Android中将事件定义为MotionEvent,其中事件的类型又分为四种:
正常用户一次从触摸屏幕到离开屏幕会产生一系列事件,根据情况不同又可分为两种:
Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
//判断当前是否为ACTION_DOWN事件
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//调用了Window的superDispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果Window的superDispatchTouchEvent返回false,则继续执行onTouchEvent(ev),否则不会执行
return onTouchEvent(ev);
}
//该方法是个空实现,当activity处于栈顶时,触屏点击按home,back,menu键等都会触发此方法
public void onUserInteraction() {
}
Window是个抽象类,它的唯一子类是PhoneWindow。当调用PhoneWindow的superDispatchTouchEvent(ev)返回true时,Activity#dispatchTouchEvent方法结束并返回true,否则继续执行Activity#onTouchEvent(ev)。
Phonewindow#superDispatchTouchEvent(ev)
public boolean superDispatchTouchEvent(MotionEvent event) {
//mDecor是DecorView的实例对象,调用了DecorView#superDispatchTouchEvent
return mDecor.superDispatchTouchEvent(event);
}
DecorView#superDispatchTouchEvent(MotionEvent event)
public boolean superDispatchTouchEvent(MotionEvent event) {
//DecorView的父类是FrameLayout,故这里会调用父类(ViewGroup)的dispatchTouchEvent(event)
return super.dispatchTouchEvent(event);
}
通过上述源码分析可知实际上事件从Activty传入到ViewGroup中。
ViewGroup#dispatchTouchEvent(MotionEvent ev)
public boolean dispatchTouchEvent(MotionEvent ev) {
***
boolean handled = false//在事件分发的过程中如果某个View消费了事件,则会将handled 置为true,最后会将handled 返回。
if (actionMasked == MotionEvent.ACTION_DOWN) {
//当前事件为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.
//取消并且清空TouchTarget,TouchTarget是一个单项链表结构的数据类,其中保存了当前触摸的
View以及下一个TouchTarget的实例引用。此处会将mFirstTouchTarget置null。
cancelAndClearTouchTargets(ev);
//重置触摸状态 将FLAG_DISALLOW_INTERCEPT标记为取反。
resetTouchState();
}
// Check for interception.//检查是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//当事件为ACTION_DOWN时,disallowIntercept 一定为false,因为resetTouchState()
将FLAG_DISALLOW_INTERCEPT标记位进行了取反操作。
所以这里FLAG_DISALLOW_INTERCEPT标记位可以影响除了ACTION_DOWN事件以外
的事件,子类可以通过getParent.requestDisallowInterceptTouchEvent(boolean disallowIntercept)
改变FLAG_DISALLOW_INTERCEPT标记位,从而来决定是让父类否拦截事件。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//当为ACTION_DOWN时disallowIntercept为false,必定会调用onInterceptTouchEvent(ev),
该方法默认是返回false,也就是不拦截事件,子类(只能是ViewGroup,只有ViewGroup才能拦截事件)
可重写该方法。
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
//如果disallowIntercept为true则表示不拦截
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
//没有触摸目标且当前事件不为ACTION_DOWN,则继续拦截触摸事件
intercepted = true;
}
***
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {//没有拦截事件
***
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//这里会对ACTION_DOWN事件进行特殊处理
***
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
//这里需要解释下为什么会采用倒叙遍历子View,这是一个概率问题,一般外层View
消费事件的概率较内层View大,所以采用倒叙遍历的方式,这样可以减少遍历子View
的时间。
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
***
if (!canViewReceivePointerEvents(child) ||
!isTransformedTouchPointInView(x, y, child, null)) {
//这里对当前子View进行了条件判断,当view不可见、view在执行动画或者当前触摸
位置不在子View范围内时,则跳过该View继续遍历子View。
ev.setTargetAccessibilityFocus(false);
continue;
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//该方法会将当前时间分发给子View,稍后去看源码,接着往下走。
//当上述条件成立时,会执行下面代码。
***
newTouchTarget = addTouchTarget(child, idBitsToAssign);//将当前子View跟
Touchtarget对象绑定,并将绑定的Touchtarget对象赋值给mFirstTouchTarget,
这里也侧面说明了如果该子View能够消费事件,那么mFirstTouchTarget就指向
该View,否则mFirstTouchTarget为null。
可能这里大家心里会有疑问,问什么要将当前子View与Touchtarget绑定?原因是
为了后续事件能够快速响应,不需要在遍历全部子View。
alreadyDispatchedToNewTouchTarget = true;//该标记位标识已经将事件分发给了
新的View
break;//跳出子View遍历循环。
}
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//mFirstTouchTarget 为空则说明没有子View消费事件,会调用dispatchTransformedTouchEvent
并且第三个参数传入的为null
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else { //代表有子View能够消费事件
TouchTarget target = mFirstTouchTarget;
while (target != null) {
//alreadyDispatchedToNewTouchTarget 是在对ACTION_DOWN事件特殊处理时赋值为true,
也就是说当前事件为ACTION_DOWN时,handled会被赋值为true。
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {//这里也说明了如果子View消费了事件,ViewGroup不会对后续事件进行处理,全部交给
子View处理
***
'
//一个事件序列中ACTION_DOWN事件后的其他事件都会执行
dispatchTransformedTouchEvent,该方法在我们上述分析中出现过多次,
接下来我们就看看该方法对子View做了什么。
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
}
}
***
return handled
}
ViewGroup#dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
***
if (child == null) {
如果传入的子View不存在,则会调用父类(View)的dispatchTouchEvent(event)方法。
handled = super.dispatchTouchEvent(event);
} else {
//如果子View存在,则会调用子View的dispatchTouchEvent(event)。
handled = child.dispatchTouchEvent(event);
}
return handled;
}
至此ViewGroup事件的分发就结束了,这里总结下整个ViewGroup事件分发流程。
先通过伪代码来整理下思路:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
boolean intercepted = false;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//子View可通过getParent.requestDisallowInterceptTouchEvent请求父容器拦截事件
final boolean disallowIntercept = (mDroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//如果允许父容器拦截事件,会调用父容器onInterceptTouchEvent方法
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;//否则父容器不拦截事件
}
} else {
intercepted = true; //没有子View消费事件或者当前不是ACTION_DOWN事件,事件就不会继续传递下去
}
//判断是否拦截事件
if (intercepted ) {
//如果拦截,则ViewGroup调用自身onTouchEvent方法
consume = onTouchEvent(ev);
} else {
//如果不拦截,则调用子View的dispatchTouchEvent方法
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
再来张流程图,加深理解。
ViewGroup中的dispatchTouchEvent负责将事件进行分发,并不会消费事件,如果有子View则会将事件传递下去,我们来看下子View中是如何处理事件。
View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
***
boolean result = false;
ListenerInfo li = mListenerInfo;//mListenerInfo包含View的很多监听,有onLongClickListener,
onClickListener等
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//如果设置onTouchListener ,并且onTouchListener#onTouch返回true,则
View#dispatchTouchEvent返回true,表示子View消费事件。
result = true;
}
//如果未设置onTouchListener,则会调用View#’onTouchEvent,如果该方法返回true,
View#dispatchTouchEvent返回true,表示子View消费事件。
if (!result && onTouchEvent(event)) {
result = true;
}
***
return result;
}
View#dispatchTouchEvent中不会再对事件进行分发,这里子View会决定是否消费事件,如果调用了View.setOnTouchListener(onTouchListener listener)且onTouchListener#onTouch
返回true,那么此次事件消费,否则会调用 onTouchEvent(event)。如果设置了onTouchListener,会先执行onTouchListener#onTouch方法,根据其返回值决定是否调用onTouchEvent。也就是onTouchListener#onTouch要先于onTouchEvent执行。
View#onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
***
final int viewFlags = mViewFlags;
final int action = event.getAction();
//当前View是否为可点击的
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状态是DISABLED,那么只要是可点击的,依然能够消费事件。
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
//当接触到屏幕时
case MotionEvent.ACTION_DOWN:
***
mHasPerformedLongPress = false;//将该变量赋值为false,表示还未处理长按事件。
***
// Walk up the hierarchy to determine if we're inside a scrolling container.
//判断当前子View是否在滚动容器中
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
//对于滚动视图中的View,短时间内延时按下的回调,以免当前正处于滚动状态。
if (isInScrollingContainer) {//如果当前正处于滚动状态
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();//CheckForTap实现了runnable接口
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());//通过postDelayed
方法名称可知是延时一段时间执行runnable中run方法, ViewConfiguration.getTapTimeout()是
100毫秒。CheckForTap中run方法会执行checkForLongClick方法,检查长按事件。
} else {//如果当前子View没有处于正在滚动的容器中
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);//这里也会执行checkForLongClick
}
break;
//当离开屏幕时
case MotionEvent.ACTION_UP:
***
//PFLAG_PRESSED标志位在ACTION_DOWN中调用setPressed(true, x, y)设置,此处prepressed 为true
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
***
//mHasPerformedLongPress 如果此时为false,说明长按事件没有触发,那么解除长按事件延时执行
事件,这里可能大家会有点蒙,什么叫解除长按事件延时执行呢?后面会回大家揭晓,这里先跟着流
程走完。mIgnoreNextUpEvent默认是false,那么所有条件都满足。
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
//当只有处于按下状时,才会执行click行为,这里的click是不是就是View#onClickListener中的
onClick方法呢?我猜是的,接着往下走。
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();//PerformClick也实现了Runable,在其run方法中调用
了performClickInternal()。
}
if (!post(mPerformClick)) {
performClickInternal();//这里也调用了performClickInternal(),先说结果吧!
在performClickInternal中拖过一系列的调用最终会调用View#OnClickListener中的onClick方法,至此时间分发也就结束了。
}
}
}
}
break;
}
return true;//这里的返回值会返回至View#dispatchTouchEvent,View#dispatchTouchEvent的返回值会
返回值ViewGroup#dispatchTransformedTouchEvent,ViewGroup#dispatchTransformedTouchEvent返回值会返回至ViewGroup#dispatchTouchEvent,这样事件消费情况一级一级网上传,直到回到Activity#dispatchTouchEvent。
}
retrun false;
}
在View#onTouchEvent中首先会执行ACTION_DOWN(事件的开始肯定是从ACTION_DOWN开始),在ACTION_DOWN中会对长按事件进行检测,那么具体是怎么检测呢?带着问题看看之前分析所遗漏的checkForLongClick方法。
View#checkForLongClick
private void checkForLongClick(int delayOffset, float x, float y) {
//如果当前View是可长按的就会往下执行。
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
//将mHasPerformedLongPress 置为false,表示长按事件没有执行。
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();//
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);//这里会发送一个延时任务,也就是在
ACTION_UP中判断是否已经执行了长按事件,如果没有执行则解除延时任务,这是同一个任
务。这里延迟时间 ViewConfiguration.getLongPressTimeout()是500毫秒。
}
}
如果当前View可处理长按事件,那么发送一个500毫秒的延时任务,这个延时任务具体执行内容是什么呢?CheckForLongPress中的run方法会告诉我们答案。
private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;
private boolean mOriginalPressedState;
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
//1.会执行performLongClick方法,并将mHasPerformedLongPress
if (performLongClick(mX, mY)) {
置为true
mHasPerformedLongPress = true;
}
}
}
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
//2. 这里又会执行performLongClick,跟在ACTION_UP中的performClickInternal
final boolean handled = performLongClick();
有点相似啊!我猜这里面就会调用View#onLongClickListener中的onLongClick方法。
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
public boolean performLongClick() {
//3.performLongClickInternal
return performLongClickInternal(mLongClickX, mLongClickY);
}
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
//4.执行View#onLongClickListener中的onLongClick方法,并将该返回值返回
handled = li.mOnLongClickListener.onLongClick(View.this);
}
***
return handled;
}
直到此处,豁然明朗。在View#onTouchEvent方法中首先会执行ACTION_DOWN,在ACTION_DOWN中会对长按事件进行检测,并发送一个500毫秒的延时任务,在ACTION_UP中对mHasPerformedLongPress 判断是否执行了长按事。这里涉及到一个时间问题,如果ACTION_DOWN到ACTION_UP事件发生时间间隔不到500毫秒,长按事件还没执行,则会执行View#onClickListener中onClick。如果时间间隔大于500毫秒则会执行View#onLongClickListener中onLongClick,这里会出现两种情况:
1、onLongClick方法返回true时,表示消费了长按事件,则不会再执行View#onclickListener中onClick。
2、onLongClick方法返回false时,表示没有消费长按事件,会执行View#onclickListener中onClick。
通过上述分析可以得出结论:
1、如果View设置OnLongClickListener,从接触屏幕到离开屏幕至离开屏幕时间间隔不足500毫秒,不会执行OnLongClickListener#onLongClick。
2、如果View设置OnLongClickListener、OnClickListener,并且OnLongClickListener#onLongClick返回true,不会执行OnClickListener#onClick;如果返回false,则会执行OnClickListener#onClick。
View中是对事件进行消费而不是分发,如果View事件没有消费,则会交给上层View处理。如果当前View设置OnTouchListener,且OnTouchListener#onTouch返回true,则事件被消费不会继续传递。返回false则将事件传递至onTouchEvent,如果View设置OnLongClickListener、OnClickListener,如果OnLongClickListener#onLongClick返回true,OnClickListener#onClick不会执行,反之则会执行。综上可知OnTouchListener、OnLongClickListener、OnClickListener优先级层层降低,OnTouchListener优先级最高。个人语言表达能力实在有限,那么画张图帮大家梳理梳理。我还是你的小佩琪.jpg
Acttivity
事件分发的入口是在Acttivity#dispatchTouchEvent。通过系列传递,Activity会将事件传递至ViewGroup中,如果事件在ViewGroup或其子类中被消费,那么事件传递结束;否则,事件将会被Activity#onTouchEvent处理。
ViewGroup
ViewGroup中的事件是Activity传递而来,如果ViewGroup不对事件进行拦截,寻找当前ViewGroup中全部子View,将事件传递给当前手指触摸位置在其范围内的子View。如果拦截事件,当前序列事件的其他事件(MOVE,UP)都将不会再传递,全部交由ViewGroup#onTouchEvent处理,如果onTouchEvent消费事件,则事件传递结束,否则,将事件传回Activity#onTouchEvent。
View
事件传递至View#dispatchTouchEvent,直接View#onTouchEvent。如果View消费事件,那么事件传递结束;如果没有消费事件,View将事件传回至ViewGroup中,交由ViewGroup#onTouchEvent处理。
具体流程图如下:
相信看到这里大家对事件分发应该有了全新的认识。在下水平有限,若文中有错,欢迎纠正。
本片文章就到此结束啦!咱们下期见!