事件分发机制是View的一个核心知识点,而且也是一个难点。本文将详细介绍和总结事件分发机制的知识,以便更好更深入的理解。
1.基础知识
1.1 事件分发的对象
事件分发的对象实际上就是事件,即MotionEvent对象(当用户触摸屏幕时,将产生点击事件(Touch事件),Touch事件的相关细节(包括发生触摸的位置、时间、历史记录和手势动作等)被封装为MotionEvent对象)
主要发生的Touch事件有如下四种:
MotionEvent.ACTION_DOWN :按下View(所有事件的开始)
MotionEvent.ACTION_UP :抬起View(与DOWN事件对应)
MotionEvent.ACTION_MOVE :滑动View
MotionEvent.ACTION_CANCEL :非人为原因结束本次事件
事件列:从手指接触屏幕到手指离开屏幕,这个过程中产生的一系列事件
任何事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件,如下图:
1.2 事件分发的本质
将点击事件(MotionEvent)向某个View进行传递并最终得到处理,也就是说,当一个点击事件发生之后,系统需要将这个点击事件传递给一个具体的View去处理,这个事件传递的过程就是分发过程。
1.3 事件传递规则
当一个事件产生之后,传递的顺序是:Activity(Window) –> ViewGroup –> View
2.事件分发机制的流程介绍
2.1概念浅析(场景举例)
对于事件分发机制,其实可以用一个生活中很常见的场景来对比理解:假设在你的公司,有一个总经理,级别最高,他下面有一个部长,级别次之;最底层就是干活的你,没有级别。现在董事长交给总经理一项任务,总经理将这项任务布置给部长,部长又把任务安排给了你。而当你好不容易干完活了,你就把任务交给部长,部长觉得任务完成的不错,于是就签了他的名字交给总经理,总经理看了也觉得不错,就签了名交给董事会。这样,一个任务就顺利完成了。
2.2流程详解
点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent(),onInterceptTouchEvent()和onTouchEvent()
这三个方法的关系可以用如下一段伪代码来表示:
// 点击事件产生后,会直接调用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {
//代表是否消耗事件
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
//如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件
//则该点击事件则会交给当前View进行处理
//即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
//如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件
//则该点击事件则会继续传递给它的子元素
//子元素的dispatchTouchEvent()就会被调用,重复上述过程
//直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
return consume;
}
上述伪代码已经将三者的关系表现得淋漓尽致,通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法会被调用,如此反复直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouchListener中的onTouch方法会被回调。这是事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用,如果返回true,俺么onTouchEvent方法将不会被调用。由此可见,给View设置onTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法就会被调用。可以看出,平时我们用的onClickListener,其优先级最低,即处于事件传递的尾端。
2.3 传递机制的结论
(1)某个View一旦决定拦截事件,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给他的话),并且它的onInterceptTouchEvent不会再被调用。也就是说,当一个View决定拦截一个事件后,那么系统会把同一个事件序列中的其他事件都交给他来处理,因此就不需要再调用这个View的onInterceptTouchEvent去询问它是否要拦截了
(2)正常情况下,一个事件序列只能被一个View拦截并消耗,这条的原因可以参考上一条,因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个时间序列中的事件不能分别友两个View同时处理,但是通过特殊手段也可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理
(3)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。
(4)如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并而且当前View可以持续收到后续的事件,最终这些消失的点击事件会传(5)ViewGroup默认不拦截任何事件,Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false
(6)View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
(7)View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false
(8)View的enable属性不影响onTouchEvent的默认返回值。哪怕是一个View是disable状态,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true
(9)onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件
(10)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再有父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以再子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
递给Activity处理
3.事件分发的源码解析
3.1Activity的事件分发
当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent进行事件分发,实际上是有Activity的Window来完成的。
Activity的dispatchTouchEvent源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
//关注点1
//一般事件列开始都是DOWN,所以这里基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//关注点2
onUserInteraction();
}
//关注点3
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
关注点1:一般事件序列都是DOWN开始的,所以这里返回为true,执行onUserInteraction( );
关注点2:看一下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() {
}
源码中的注释很长,但是代码实际上只有一行,从源码中我们可以看出,此方法为空方法,主要作用是用于屏保。代码注释中也可以知道,当此activity位于栈顶的时候,屏幕点击home、back、menu等键都会触发此方法。
关注点3:Window类是一个抽象类,PhoneWindow是它的唯一实现类;superDispatchTouchEvent(ev)是抽象方法,返回的window对象;
通过PhoneWindow类看一下superDispatchTouchEvent的作用
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
//mDecor是DecorView的实例//DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
}
接下来看一下mDecor.superDispatchTouchEvent(event)的源码
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
//DecorView继承自FrameLayout//那么它的父类就是ViewGroup
而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()
}
通过源码以及源码中的增加的注释,我们可以知道,执行getWindow( ).superDispatchTouchEvent(ev)实际上是执行了ViewGroup.dispatchTouchEvent(event)
结合最初的代码可以知道,一般事件序列都是从DOWN开始,所以基本都会执行getWindow( ).superDispatchTouchEvent(ev)的判断,也就是说,执行Activity.dispatchTouchEvent(ev)实际上就是执行了ViewGroup.dispatchTouchEvent(event),通过这种方式,事件就从Activity传递到了ViewGroup中
总结:
当一个点击事件发生时,调用顺序如下:
(1)事件最先传到Activity的dispatchTouchEvent进行事件分发
(2)调用Window类的实现类PhoneWindow中的superDispatchTouchEvent方法
(3)调用DecorView的superDispatchTouchEvent方法
(4)最终会调用DecorView父类的dispatchTouchEvent,也就是ViewGroup的dispatchTouchEvent方法
3.2 ViewGroup的事件分发机制
ViewGroup的dispatchTouchEvent()源码分析:
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
//看这个If判断语句//第一个判断值disallowIntercept:是否禁用事件拦截的功能(默认是false)
// 可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。
// 第二个判断值: !onInterceptTouchEvent(ev):对onInterceptTouchEvent()返回值取反
//如果我们在onInterceptTouchEvent()中返回false,就会让第二个值为true,从而进入到条件判断的内部
// 如果我们在onInterceptTouchEvent()中返回true,就会让第二个值为false,从而跳出了这个条件判断。
// 关于onInterceptTouchEvent()请看下面分析(关注点1)
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
//通过for循环,遍历了当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
//判断当前遍历的View是不是正在点击的View
//如果是,则进入条件判断内部
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
//关注点2
//条件判断的内部调用了该View的dispatchTouchEvent()方法(具体请看下面的View事件分发机制)
//实现了点击事件从ViewGroup到View的传递
if (child.dispatchTouchEvent(ev)) {
//调用子View的dispatchTouchEvent后是有返回值的
//如果这个控件是可点击的话,那么点击该控件时,dispatchTouchEvent的返回值必定是true
//因此会导致条件判断成立
mMotionTarget = child;
//于是给ViewGroup的dispatchTouchEvent方法直接返回了true,这样就导致后面的代码无法执行,直接跳出
//即把ViewGroup的touch事件拦截掉
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
//关注点3
// 没有任何View接收事件的情况,即点击空白处情况
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
//调用ViewGroup的父类View的dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch()、onTouchEvent()
// 实现了点击事件从ViewGroup到View的传递
return super.dispatchTouchEvent(ev);
}
//之后的代码在一般情况下是走不到的了,我们也就不再继续往下分析。
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
关注点1:(onInterceptTouchEvent源码分析)
ViewGroup每次在做事件分发的时候,需要调用onInterceptTouchEvent判断是否拦截事件,源码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
返回false 表示不拦截,默认为false,允许事件继续向子View传递
返回true 表示拦截,需要手动设置,即自己处理该事件,执行自己的onTouchEvent,事件不会继续向下传递
关注点2:
当点击了某个控件,调用过程如下:
(1)调用该View所在布局(ViewGroup)的dispatchTouchEvent
(2)在布局的dispatchTouchEvent中找到被点击的View
(3)再去调用该View的dispatchTouchEvent
这个过程实现了点击事件从ViewGroup到View的传递
总结:
Android的事件分发机制是先传递到ViewGroup,再由ViewGroup传递到View。
在ViewGroup中通过onInterceptTouchEvent对事件传递进行拦截,onInterceptTouchEvent方法返回true表示拦截事件,即不允许事件继续向子View传递;onInterceptTouchEvent方法返回false表示不拦截事件,即允许事件继续向子View传递,默认是返回false的;子View如果将传递的事件消费掉,ViewGroup中将无法接收任何事件。
3.3 View的事件分发机制
View的dispatchTouchEvent源码分析:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
代码中可以看出需要满足三个判断条件才会返回true,否则都是执行onTouchEvent(event)
第一个条件:mOnTouchEventListener == null
//mOnTouchListener是在View类下setOnTouchListener方法里赋值的
public void setOnTouchListener(OnTouchListener l) {
//即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
mOnTouchListener = l;
}
第二个条件:(mViewFlags & ENABLED_MASK) == ENABLED
这个条件是判断当前点击的空间是否enable,由于大部分View默认都是enable的,因此该条件恒定为true
第三个条件:mOnTouchListener.onTouch(this, event)
这个条件是设置回调控件的onTouch方法
//手动调用设置
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
如果在onTouch方法中返回了true,就会让上述三个条件全部成立,从而整个方法都会返回true;如果onTouch方法里面返回了false,就会去执行onTouchEvent方法。
接下来继续看onTouchEvent方法的源码:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//如果该控件是可以点击的就会进入到下两行的switch判断中去;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//如果当前的事件是抬起手指,则会进入到MotionEvent.ACTION_UP这个case当中。
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
// 在经过种种判断之后,会执行到关注点1的performClick()方法。
//请往下看关注点1
if ((mPrivateFlags & 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 (!mHasPerformedLongPress) {
// 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)) {
//关注点1
//请往下看performClick()的源码分析
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
//如果该控件是可以点击的,就一定会返回true
return true;
}
//如果该控件是不可以点击的,就一定会返回false
return false;
}
关注点1:
performClick()的源码分析:
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
只要mOnClickListener不为null,就回去调用onClick方法,mOnClickListener的赋值见下面的方法
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
mOnClickListener = l;
}
也就是所,当我们通过setOnClickListener方法来给控件注册一个点击事件的时候,就会给mOnclickListener赋值,也就会回调onClick方法
总结:
(1)onTouch的执行优先级高于onClick方法
(2)每当控件被点击时,如果回调onTouch里返回了false,就会让dispatchTouchEvent方法返回false,那么就会执行onTouchEvent方法;如果回调onTouch返回了true,就会让dispatchTouchEvent方法里返回true,那么将不会执行onTouchEvent
onTouch()返回false(该事件没被onTouch()消费掉) = dispatchTouchEvent()返回false(继续向下传递) = 执行onTouchEvent() = 执行OnClick()
onTouch()返回true(该事件被onTouch()消费掉) = dispatchTouchEvent()返回true(不会再继续向下传递) = 不会执行onTouchEvent() = 不会执行OnClick()