有了前一篇的概念及Demo的亲自体验,那么接下来我们从源码出发,知根知底,看一下究竟。
Android中事件分发顺序:Activity(Window) -> ViewGroup -> View
其中:
- super: 调用父类方法;
- true:消费事件,即事件不继续向下传递;
- false:不消费事件
所以,我们的重心也就是 Activity的分发机制,ViewGroup 的分发机制,View的分发机制
Activity的事件分发解析:
当一个点击事件发生时,事件最先传到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:
/**
* 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() {
}
可以看出:
- 该方法为null方法
- 当次Activity 在栈顶时,触屏点击按 home,back,menu键等都会触发此方法
- 所以onUserInteraction 主要用于屏幕保护。
关注点3:
-
Window类是抽象类,而且PhoneWindow 类是 Window 类唯一的实现类。
-
superDispatchTouchEvent(ev) 是抽象方法
-
我们通过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) { //DecorView 继承自 FragmentLayout //所以它的父类也是ViewGroup //所以super.dispatchTouchEvent(event)调用的其实也就是 ViewGroup的 dispatchTouchEvent return super.dispatchTouchEvent(event); }
所以: 执行getWindow().superDispatchTouchEvent(ev)实际上是执行了 ViewGroup.dispatchEvent(event)
再回头看开始的源码,
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);
}
由于一般事件列都是 DOWN,所以这里返回true,基本上都会进 getWindow().superDispatchTouchEvent(ev)判断,所以 执行 Activity.dispatchTouchEvent 其实也就是执行 ViewGroup.dispatchTouchEvent(event),这样事件就从Activity 传递到 ViewGroup.
总结:
当一个点击事件发生时,调用顺序如下:
- 最先传入Activity的 dispatchTouchEvent()进行事件分发
- 调用 window 类实现类 phoneWindow的 superDispatchTouchEvent()
- 调用 Decoorvier 的 superDispatchTouchEvent()
- 最终调用 Decoorvier 父类的 dispatchTouchEvent,也就是ViewGroup的 dispatchTouchEvent();
简单来说,也就是
当一个点击发生后,事件最先传到 Activity的 dispatchTouchevent进行事件分发,最终调用了ViewGroup的 dispathchTouchevent。这样事件就从Activity 传递到了ViewGroup.
ViewGroup 的事件分发解析:
dsispatchTouchEven() 源码分析:
//发生Action_DOWN事件或者已经发生过 ACTION_DOWN,并且将 mFirstTouchTarget赋值,才进入此区域,主要目的是拦截器。
//mFirstTouchTarget!=null也就是,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被复制并指向子元素。也就是说,当ViewGroup不拦截此事件并将此事件交给子元素处理时,mFirstTouchTarget!=null
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//disallowIntercept 是否禁用事件拦截器的功能(默认false)
//可以在子view通过调用 requestDisallowInterceptTouchEvent 方法对这个值进行修改,不让该 view 拦截事件
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.
//当没有触摸targets,且不是down事件时,开始持续拦截触摸
intercepted = true;
}
上面这段内容主要就是为判断是否拦截。如果当前事件为 MotionEcent.ACTION_DOWN或 mFirstTouchTarget!=null,则进入判断,判断是否拦截。如果不是以上两种情况,即已经是 MOVE或者 别的后续事件,并且之前的事件没有对象进行处理,则设置成 true,开始拦截所有事件。这也就解释了 如果子view 的onTouchEvent() 方法返回false,那么接下来的一系列事件都不会交给他处理。如果 ViewGroup 的 onInterceptTouchEvent()第一次执行为true,则 mFirstTouchTarget!=null,则也会使得接下来不会调用 onInterceptTouchEvent(),直接将拦截设置为 true。
当然,这里有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过 requestDisallowInterceptTouchEvent 方法来设置的,一般用于子 View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的其他点击事件。为什么说是除了 ACTION_DOWN 以外的其他事件呢?这是因为 ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置的这个 标记位 无效。因此,当面对 ACTION_DOWN 事件时,ViewGroup 总会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件。
当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View或ViewGroup进行处理。
//从最底层的父视图开始遍历
//找寻newTouchTarget,即上面的 mFirstTouchTarget
//如果已经存在找寻newTouchTarget,说明正在接受触摸事件,则跳出循环
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//如果当前视图无法获取用户焦点,则跳出本次循环
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;
}
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;
}
//重置取消或抬起标志位
//如果触摸位置再 child 的区域内,则把时间分发给 View 或 ViewGroup
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 获取TouchDown时间点
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// 获取TouchDown的Index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
//获取TouchDown 的x,y坐标
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//添加TouchTarget,则mFirstTouchTarget != null。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示以及分发给NewTouchTarget
alreadyDispatchedToNewTouchTarget = true;
break;
}
上面这段代码逻辑也很清晰,首先遍历 ViewGroup 的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接受点击事件主要由两点来衡量;子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。可以看到, dispatchTransformedTouchEvent 实际上调用的就是子元素的 dispatchTouchEvent 方法,在它的内部有如下一段内容,而在上面的代码中,child 传递的 不是 null,因此他会直接调用子元素的dispatchTouchEvent 方法,这样事件就交由子元素处理了。从而完成了一轮事件分发。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的 dispatchTouchEvent 返回 true,这时我们暂时不用考虑 事件在子元素内部是怎么分发的,那么 mFritsTouchTarget 就会被赋值同时跳出 for循环,如下所示:
//添加TouchTarget,则 mFirstTouchTarget!=null
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示以及分发给 NewTouchTarget
alreadyDispatchedToNewTouchTarget = true;
break;
其中 addTouchTarget(child, idBitsToAssign) 内部完成 mFirstTouchTarget 被赋值。如果 mFirstTouchTarget为空,将会让ViewGroup 默认拦截所有操作。如果遍历所有子 View或ViewGroup,都没有消费事件,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,就像我们最上面写的那个demo一样,是因为在onTOuchEvent中返回了false,这个时候,ViewGroup会自己处理点击事件。
总结:
-
Android事件分发是先传递到ViewGroup,再传递到View
-
在ViewGroup 中通过 onInterceptTouchEvent() 对事件传递进行拦截
-
- onInterceptTouchEvent 返回值true代表拦截
- false代表继续传递(默认false)
- 子view中如果将传递的时间消费掉,ViewGroup 中将无法接收到任何事件。
View事件分发解析:
同样,我们还是先分析下 dsispatchTouchEven()方法:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
...
//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;
}
从源码中可以发现:只有4个条件都为真,dispatchTouchEvent() 才返回true;否则执行 onTouchEvent(event)方法:
- li != null
- li.mOnTouchListener != null
- (mViewFlags & ENABLED_MASK) == ENABLED
- li.mOnTouchListener.onTouch(this, event))
**li != null **
ListenerInfo是View的一个静态类,包括了几个 Listener,比如TouchListener,FocusChangeListener等,一般情况下它均不为null,它不是我们今天讨论的核心,所以不用多关注它。
li.mOnTouchListener != null
mOnTouchListener 是由View设置的,比如 mButton.setTouchListener().所以如果View 设置了 Touch 监听,那么 mOnTouchListener 不为null.
//mOnTouchListener是在View类下setOnTouchListener方法里赋值的 public void setOnTouchListener(OnTouchListener l) { //即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空) mOnTouchListener = l; }
(mViewFlags & ENABLED_MASK) == ENABLED
该条件是判断当前点击的控件是否 enable,由于很多view默认是enable ,所以该条件默认为true
li.mOnTouchListener.onTouch(this, event))
判断TouchListener 的onTouch() 方法是否消耗了Touch 事件。返回值为 true表示消费该事件,false表示未消费。
如果上面全部满足,那么result 为true,否则为 false,调用 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) {
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;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//如果该控件是可以点击就会进入到下两行的switch判断中
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
//经过种种判断,会执行到关注点1的performeCLick() 方法
//请往下看关注点1
if ((mPrivateFlags & PFLAG_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 (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
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)) {
//关注点1
performClickInternal();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
//如果该控件是可以点击的,就一定返回true
return true;
}
//如果该控件不可以点击,就一定返回false
return false;
}
关注点1:
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;
}
-
只要li.mOnClickListener!=null与li!=null,就回去调用 onClick方法。
-
那么,mOnclickListener 又是在哪里赋值的呢?
-
public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } mOnClickListener = l; }
当我们通过调用 setOnclickListener 方法来给控件注册一个点击事件时,就会给 mOnclickListener赋值,即回调onClick();
总结:
-
onTouch()的执行高于onClick(),这一点从最上面的Demo都可以看出。
-
如果在回调onTouch() 里返回false,就会让 dispatchTouchEvent 方法返回false,那么就会执行 onTouchEvent();如果回调了 setOnclickListener() 来给控件注册点击事件的话,最后会在 performClick() 方法里回调 onClick()。
onTouch() 返回false,未消费该事件->执行onTouchEvent()->执行 onClick
如果在回调 onTOuch()里返回 true,就会让 dispatchTouchEvent 方法返回 true,那么将不会执行 onTouchEvent(),即onClick(),也不会执行;
onTouch()返回true(该事件被onTouch()消费掉)=dispatchTouchEvent()返回true,即不会再向下传递,也就不会执行onTouchEvent(),不会执行 Onclick()
onTouch() 和 onTouchEvent() 的区别:
-
这两个方法都是在View 的dispatchTouchEvent 中调用得,但onTouch 优先于 onTouchEvent 执行。
-
如果在onTouch 方法中返回 true 将事件消费掉,onTouchEvent() 将不会在执行。
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; }
-
因此如果有一个控件是 非 enable 的,那么给它注册 onTouch 事件将永远得不到执行。对于这一类控件,如果我们想要监听它的 touch 事件,就必须通过在该控件中重写 onTOuchEvent 方法来实现。
到了这里,我们的事件分发源码解析也就差不多结束了,如果有什么问题,也欢迎与我讨论。
更多Android开发知识请访问—— Android开发日常笔记,欢迎Star,你的小小点赞,是对我的莫大鼓励。
参阅:
- GcsSloop
- Android开发艺术探索