事件分发其实就是对MotionEvent的分发过程,当我们手指在屏幕上触碰就会产生一系列的MotionEvent,常见的事件序列有下面两种:
- 手指在屏幕上单击 会产生 down->up两个MotionEvent事件。
- 手指在屏幕上不停滑动会产生 down->move>…>move->up(按下手指,些许滑动、抬起手指)。
一个事件序列是总以down事件开始,up事件结束。
事件的分发就是MotionEvent的传递过程,它从Activity开始,再分发给Window,最后分发给顶级View,顶级View通常都是一个ViewGroup,最后继续向下分发给具体的View,如果这个具体View没有消费该事件,那么就让该View的上级View处理,如果都不处理最后由Activity处理。
在事件分发过程有几个重要的方法:
- dispatchTouchEvent:这个方法由父ViewGroup调用,调用子View或者子ViewGroup的dispatchTouchEvent方法就将事件分发给子View了。
- onInterceptTouchEvent:父容器在将事件分发给子View之前会调用这个方法询问是否拦截事件,返回true代表拦截,false不拦截。如果事件被拦截就不会往下分发了,默认不拦截,也就是返回false。
- onTouchEvent:处理事件,返回true表示消费事件,false,不消费。
View的dispatchTouchEvent方法
首先看看View的dispatchTouchEvent方法。,当事件传到它的父容器,父容器就会遍历所有的子View然后找到合适的子View调用它的dispatchTouchEvent方法,将事件分发给它处理。
View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
//。。。省略部分代码
boolean result = false;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null//1
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//2
result = true;
}
if (!result && onTouchEvent(event)) {//3
result = true;
}
}
//。。。
return result;
}
首先在注释1处:li.mOnTouchListener != null
判断是否设置了OnTouchListener,如果设置了就调用它的onTouch方法(注释2),如果onTouch方法返回true那么就将result值设为true,如果onTouch方法返回flase,那么就不会进入{}中,result就为false。此时就会调用View内部的onTouchEvent方法。如果onTouchEvent方法true代表事件被消费result会被设为true,然后将result返回给父容器,父容器拿到这个值就知道子View是否消费了这个事件。从这个方法我们可以知道:
- 外部设置的OnTouchListene的onTouch方法优先于View自身的onTouchEvent执行。
- 父ViewGroup通过子View的dispatchTouchEvent将事件分发给子View,dispatchTouchEvent方法的返回值代表子View是否消费了该事件。true代表消费,false未消费。
验证:
自定义一个MyView重写它的onTouchEvent,很简单只是打印一句日志。
public class MyView extends View {
private Paint mPaint;
public MyView(Context context) {
this(context,null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint=new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,getMeasuredWidth()/2,mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TAG","MyView --> onTouchEvent");
return super.onTouchEvent(event);
}
}
然后再Activity中使用:给它在外部设置一个OnTouchListener,并且在onTouch方法中打印一句日志,onTouch返回false。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_event_dispatch);
myView=findViewById(R.id.myView);
myView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG","OnTouchListener --> onTouch");
return false;
}
});
}
当我们的手指在MyView上触摸。
很明显先调用了OnTouchListener的onTouch方法,并且由于onTouch返回false表示它不消费事件,事件会传递给MyView内部的onTouchEvent,因此onTouchEvent会被执行。那么如果onTouch返回true呢?
此时的onTouch方法会被执行两次,但是onTouchEvent不会被执行了。这种现象其实是正确的。通过前面的源码分析知道,首先onTouch返回true了,那么onTouchEvent就不会被执行,onTouch返回true表示事件被消费,那么当前View的dispatchTouchEvent也会返回true,这样父ViewGroup知道当前子View消费了down事件,当我们手指抬起,后续的up事件依然会分发给当前的子View(MyView)。如果我们的onTouch方法返回了false表示不消费事件,那么View自身的onTouchEvent会被执行,如果onTouchEvent同样返回false,那么dispatchTouchEvent就会返回false,这样父ViewGroup就知道这个子View(MyView)不消费down事件,后续的其他事件也都不会再分发给它了。
那么View的onTouchEvent方法到底做了什么呢?
View#onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
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) {
//即使View是DISABLED的,只要它可以是clickable默认就可以消耗事件
return clickable;
}
//重点
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_DOWN:
//....省略部分代码
//创建一个执行长按事件的任务
mHasPerformedLongPress = false;
checkForLongClick(0, x, y);
break;
case MotionEvent.ACTION_UP:
//手指抬起时
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//移除长按事件任务
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//调用onClick事件
performClick();
}
}
}
//............
break;
//......省略
}
return true;
}
return false;
}
首先只要View是clickable 的,即使它是不可用的默认也能够消耗事件,在注释重点部分的代码,省略了一些代码,并且为了更好的分析调整了switch中ACTION_DOWN和ACTION_UP顺序。当down事件到来时会先将mHasPerformedLongPress 设置为false(这点很重要),并调用checkForLongClick(0, x, y)方法创建一个执行长按事件的任务。
private void checkForLongClick(int delayOffset, float x, float y) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
//....
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
在checkForLongClick方法内部发送了一个延迟执行的任务CheckForLongPress,默认是500毫秒后执行。
private final class CheckForLongPress implements Runnable {
//。。。
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
}
CheckForLongPress 在500毫秒后会执行,并且调用performLongClick方法,这个方法会执行长按监听事件回调。如果performLongClick返回true,那么将mHasPerformedLongPress 设为true,这点很重要。performLongClick方法最终会调用performLongClickInternal方法
private boolean performLongClickInternal(float x, float y) {
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
//。。。
return handled;
}
很简单,如果这个View设置了OnLongClickListener那么就调用onLongClick,如果onLongClick返回true,performLongClick方法也会返回true。之后就是mHasPerformedLongPress 被设为true,如果onLongClick返回false代表不消费事件,mHasPerformedLongPress 依然还是false。
总结下在onTouchEvent方法中down事件到来后:首先将mHasPerformedLongPress 设为false,然后调用checkForLongClick(0, x, y)方法,创建一个执行长按事件的任务,这个任务默认会在500毫秒后执行,如果这个任务执行了,并且performLongClick返回true(OnLongClickListener.onLongClick返回true),此时mHasPerformedLongPress会被设为true。
在onTouchEvent方法中up事件到来后,又做了什么呢?
public boolean onTouchEvent(MotionEvent event) {
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) {
//即使View是DISABLED的,只要它可以是clickable默认就可以消耗事件
return clickable;
}
//重点
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
//....
case MotionEvent.ACTION_UP:
//手指抬起时
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//移除长按事件任务
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//调用onClick事件
performClick();
}
}
}
//............
break;
//......省略
}
return true;
}
return false;
}
当up事件到来,也就是手指抬起时,如果mHasPerformedLongPress 为false,那么会先调用 removeLongPressCallback()方法移除执行长按事件的任务,然后调用performClick方法,这个方法最终会OnClickListener的onClick方法。
mHasPerformedLongPress 取值的情况:
- 1、手指按下后,很快抬起没有超过500毫秒,此时mHasPerformedLongPress为false,抬起后(up)会先移除执行长按事件的任务,然后调用OnClickListener的onClick方法
- 2、手指按下后超过了500毫秒,会先执行CheckForLongPress 任务,此时OnLongClickListener的onLongClick会执行,如果onLongClick返回false,代表不消费事件,此时mHasPerformedLongPress为false,手指抬起时(up),OnClickListener的onClick方法依然有机会执行。
- 3、手指按下后超过了500毫秒,会先执行CheckForLongPress 任务,此时OnLongClickListener的onLongClick会执行,如果onLongClick返回true,代表消费事件,此时mHasPerformedLongPress设置为true,手指抬起时(up),OnClickListener的onClick方法不会执行。
再来看看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;
}
//。。。
return result;
}
很简单,如果设置了OnClickListener就调用OnClickListener的onClick方法,并且此时默认消费事件。这样事件分发给View,View如何处理就基本分析完了。
事件分发从哪里开始?
当我们手指在屏幕上触摸滑动时,事件会先到达Activity,可以看到Activity的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
//将事件分发给PhoneWindow
//如果返回true则说明事件被消费,
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果事件没有被消费,调用自己的onTouchEvent,由自己处理
return onTouchEvent(ev);
}
Activity#dispatchTouchEvent方法很简单,就是将事件分发给PhoneWindow,如果在后续的分发过程中事件被成功消费,则dispatchTouchEvent返回true,此时直接 return true,如果在后续的分发中事件没有被消费dispatchTouchEvent返回false,此时调用Activity#onTouchEvent方法,自己处理。
继续看getWindow().superDispatchTouchEvent(ev),Window的具体实现类是PhoneWindow,调用这个方法事件就被分发给PhoneWindow了。PhoneWindow#superDispatchTouchEvent:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可以看到PhoneWindow直接将事件分发给DecorView了,DecorView是顶级View,它是一个ViewGroup。这样MotionEvent就从Acticity到达ViewGroup了。
ViewGroup的dispatchTouchEvent
ViewGroup#dispatchTouchEvent:代码比较长先分段看下。
//首先检查是否需要拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//判断是否拦截,默认返回false也就是不拦截。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
//2
//如果不为down事件,并且mFirstTouchTarget为空,就直接拦截
//也就是说当down事件分发给子view,但是没有子view消费这个事件,
//那么当这个事件序列的其他事件到来时:如move,up事件到来,就不往下分发,自己处理。
intercepted = true;
}
这段代码就是为了检查是否需要拦截事件,如果这个事件不是ACTION_DOWN事件,并且mFirstTouchTarget为空就拦截该事件,将intercepted设为ture。需要知道,一个事件是以down事件开始的,如果down事件被下级view成功处理了mFirstTouchTarget就不为null,会指向这个子View,能走到注释2就代表下级view没有消费down事件,此时事件序列的后续事件move,up就直接拦截,自己处理。
如果成功进入1的if中,此时如果是ACTION_DOWN事件,disallowIntercept肯定是false,会调用onInterceptTouchEvent询问自己是否拦截事件,如果mFirstTouchTarget != null也会进入if中,不过此时disallowIntercept 可能为true,就不会调用onInterceptTouchEvent询问是否拦截了,intercepted 直接为false不拦截。disallowIntercept 的值取决于FLAG_DISALLOW_INTERCEPT,它受子View的影响,也就是说子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 设置这个标志位来影响上级View对除down事件的其他事件的拦截情况,如果子View不设置这个标志位还是会询问是否拦截的,如果设置了这个标志位那么disallowIntercept就为true,此时onInterceptTouchEvent就不会被调用了,intercepted直接为false。为什么这个标志位对down事件没用呢?很简单在down事件到来后会重置上个事件序列的状态以防对当前的事件序列产生影响。
// Handle an initial down.
//当down事件到来,重置状态 包括FLAG_DISALLOW_INTERCEPT和mFirstTouchTarget
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();
}
如果不拦截,紧接着就会遍历所有的子View找到合适的view去处理事件。
//如果不拦截,就遍历所有的子view
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {
//...
//判断子view是否能接收事件,不能则跳出当前循环,进行下次循环
//能接收事件的条件:子view是否可见,子view是否在播放动画,点击事件是否落在子view区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
//。。。
continue;
}
//...
//调用dispatchTransformedTouchEvent将事件分发给子view,如果子view正确处理则dispatchTransformedTouchEvent返回true
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//。。。。
//子消费了事件则会进入if语句内部,调用addTouchTarget方法给mFirstTouchTarget赋值,然后跳出循环,不需要再遍历子view了。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
} //end if (!canceled && !intercepted)
// 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);
}
这段代码就是循环遍历子View,找到可以处理事件的子View然后调用dispatchTransformedTouchEvent方法将事件分发给子View,如果子View消费了事件mFirstTouchTarget就会被赋值。那么dispatchTransformedTouchEvent干了什么呢?
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//.....
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//将事件分发给子View,
handled = child.dispatchTouchEvent(event);
}
return handled;
}
//....
可以看到,我们传入了child也就是子View,直接调用子View的dispatchTouchEvent将事件分发给它。
如果遍历了所有的子View都没人处理就自己处理。可以看到此时调用dispatchTransformedTouchEvent时,传入的子View时null,因此会调用自己超类的dispatchTouchEvent,自己去处理这个事件。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//如果遍历了所有子view后都没有子view处理事件,则mFirstTouchTarget为空,此时自己处理
//注意此时第三个参数为null不再是child了
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
完整代码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//.......
if (onFilterTouchEventForSecurity(ev)) {
// Handle an initial down.
//当down事件到来,重置状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
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) {
//判断是否拦截,默认返回false也就是不拦截。
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
//如果不为down事件,并且mFirstTouchTarget为空,就直接拦截
//也就是说当down事件分发给子view,但是没有子view适当的处理这个事件,
//那么当这个事件序列的其他事件到来时:如move,up事件带来,就不往下分发,自己处理。
intercepted = true;
}
//intercepted为false表示不拦截,会进入if语句内部
if (!canceled && !intercepted) {
//......
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
//如果不拦截,就遍历所有的子view
for (int i = childrenCount - 1; i >= 0; i--) {
//......
//判断子view是否能接收事件,不能则跳出当前循环,进行下次循环
//能接收事件的条件:子view是否可见,子view是否在播放动画,点击事件是否落在子view区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
//调用dispatchTransformedTouchEvent将事件分发给子view,如果子view正确处理则dispatchTransformedTouchEvent返回true
//正确处理:子view处理了该事件,并且将事件消费了。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//...
//子view正确的处理了事件则调用addTouchTarget方法给mFirstTouchTarget赋值,然后跳出循环,不需要再遍历子view了。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
//...
}
}
//......
}
}
// 如果事件被拦截或者没被拦截但是没有子View消费事件mFirstTouchTarget都会为null
if (mFirstTouchTarget == null) {
//注意此时第三个参数为null不再是child了
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
}
//....
return handled;
}
这样事件从Activity如何分发到具体的View就分析完了。