一、事件定义
定义:当用户触摸屏幕时,将产生的触摸行为(Touch事件)
事件类型
- MotionEvent.ACTION_DOWN 手指刚接触屏幕
- MotionEvent.ACTION_MOVE 手指在屏幕上滑动
- MotionEvent.ACTION_UP 手指从屏幕上松开
- MotionEvent.ACTION_CANCEL 非人为因素取消
二、事件序列
正常情况下一次手指触摸屏幕的行为会触发一系列事件
- 点击屏幕后立即松开,事件序列为DOWN -> UP
- 点击屏幕滑动一段距离后松开,事件序列为DOWN -> MOVE -> ... -> MOVE -> UP
事件序列流程图如下
三、事件分发对象
- Activity:控制生命周期&处理事件
- ViewGroup:一组View的集合
- View:所有UI组件的基类
四、事件分发主要方法
-
dispatchTouchEvent(MotionEvent ev)
:用来进行事件分发
-
onInterceptTouchEvent(MotionEvent ev)
:判断是否拦截事件(ViewGroup)
-
onTouchEvent(MotionEvent ev)
::处理触摸事件
五、事件分发源码分析
按照惯例,为了方便大家理解源码,我们先来看看整个事件分发的流程图
Activity的事件分发
Activity是最先得到用户触摸事件的,首先我们来看Activity#dispatchTouch
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
这里有个判断这个触摸事件是不是DOWN事件,如果是则调用onUserInteraction()
,这个方法是一个空方法,我们继续往下看,getWindow()
会返回一个PhoneWindow
对象,接着调用PhoneWindow#superDispatchTouchEvent,如果返回true则事件结束,否则表示事件没有被消费,则 调用Activity#onTouchEvent来处理事件,继续跟进PhoneWindow#superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
mDecor是顶层布局容器DecorView,继续跟进DecorView#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
可以看到DecorView中是直接调用了 super.dispatchTouchEvent(event)
,因为DecorView
继承自FrameLayout
,所以最后会调用到ViewGroup#sdispatchTouchEvent(event),我们来总结下Activity的事件分发:Activity->PhoneWindow->DecorView
ViewGroup的事件分发
现在事件已经从Activity分发到了ViewGroup,我们接着来看ViewGroup的事件分发
ViewGroup#dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
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();
}
// --------注释2--------
// Check for interception.
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.
intercepted = true;
}
...
// --------注释3 --------
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
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.
final ArrayList preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 对子View倒叙遍历
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
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;
}
// --------注释4 --------
if (!child.canReceivePointerEvents()
|| !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;
}
resetCancelNextUpFlag(child);
// --------注释5 --------
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
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();
// --------注释6 --------
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
// --------注释7 --------
// 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);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// --------注释8 --------
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
...
return handled;
}
看到注释1处,如果是一个DOWN事件,表明是一个新的事件的开始,会调用两个方法进行一些标记的清除,cancelAndClearTouchTargets(ev)
主要的作用是将全局变量mTouchTarget
置null,resetTouchState()
主要作用是将全局变量mGroupFlags
清除,接下来继续往下看注释2处的代码,这块代码主要是用来检测是否需要对事件进行拦截,必须是DOWNS事件或者mTouchTarget
不为null才会可能去执行onInterceptTouchEvent(ev)
,否则拦截标记intercepted
直接赋值为true,然后继续根据mGroupFlags
是不是设置为FLAG_DISALLOW_INTERCEPT
,是的话表明不拦截该事件,拦截标记intercepted
赋值为false,否则执行onInterceptTouchEvent(ev)
,当子View调用requestDisallowInterceptTouchEvent()
方法来请求父ViewGroup不拦截事件时会给mGroupFlags
赋值为FLAG_DISALLOW_INTERCEPT
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
在DOWN事件时,已经对mTuchTarget
置null,mGroupFlags
清除,导致在DOWN事件intercepted
赋值为true,表示拦截该事件,所以requestDisallowInterceptTouchEvent
只能作用DOWN事件之后的事件(MOVE、UP)。接着看注释3处,在DOWN事件时,如果newTouchTarget == null && childrenCount != 0
,则直接对子View进行倒叙遍历,继续往下看注释4处child.canReceivePointerEvents()
和isTransformedTouchPointInView()
,先看View#canReceivePointerEvents
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
这个方法表示View可见或者正在执行动画,接着看
MotionEvent#isTransformedTouchPointInView
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
这个方法主要作用是判断触摸区域是否在View的区域内 ,回到注释4处,这个条件判断表示如果当前View不可见并且没有在执行动画,或者触摸区域不在View的区域内,则直接continue,进行下次遍历。继续看到注释5处,如果找到了触摸的View,调用dispatchTransformedTouchEvent()
并将这个View传进去,跟进ViewGroup#dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
...
// Done.
transformedEvent.recycle();
return handled;
}
如果child
不为空,则调用child.dispatchTouchEvent(event)
将事件传递给child
,如果child
为null,则直接调用super.dispatchTouchEvent(event)
,此时如果 child
为View,则直接调用View的dispatchTouchEvent(event)
处理事件,如果child
为ViewGroup,则事件最终会来到ViewGroup的onTouchEvent()
方法 ,回到注释5处条件判断,如果dispatchTransformedTouchEvent()
返回true, 表示子View消费了事件,接着调用注释6处的addTouchTarget(child, idBitsToAssign)
,ViewGroup#addTouchTarget
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
这里对mFirstTouchTarget
赋值为消费了事件的子View的TouchTarget,回到调用该方法的注释6处,将消费了事件的子View的TouchTarget赋值给newTouchTarget
,并将alreadyDispatchedToNewTouchTarget
设为true
我们先来总结下到目前为止ViewGroup#dispatchTouchEvent所做的事情:在DOWN事件时,遍历所有子View,找到消费事件的子View,并将子View赋值给mFirstTouchTarget
,即mFirstTouchTarget
指向了消费事件的子View,如果没有子View消费事件,则 mFirstTouchTarget
依旧为null
继续看到ViewGroup#dispatchTouchEvent中注释7处,如果mFirstTouchTarget
为null,表明没有子View消费事件,则调用dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)
,并将第三个参数child传null,上面我们已经分析了,如果child
为null,则直接调用super.dispatchTouchEvent(event)
,此时如果 child
为View,则直接调用View的dispatchTouchEvent(event)
处理事件,如果child
为ViewGroup,则事件最终会来到ViewGroup的onTouchEvent()
方法
继续看注释8处,如果mFirstTouchTarget
不为null,表示DOWN事件已经找到了一个子View来消费事件,条件判断(alreadyDispatchedToNewTouchTarget && target == newTouchTarget)
当找到消费事件的子View时值为true,然后直接给handles
赋值为true,DOWN事件结束,如果上面条件判断为false,则else里面的代码是对MOVE和UP事件的处理,继续调用dispatchTransformedTouchEvent()
将MOVE和UP事件直接分发给消费了DOWN事件的 子View
我们来简单总结下ViewGroup#dispatchTouchEvent所做的全部事情:ViewGroup中可以通过onInterceptTouchEvent()
对事件进行拦截,返回true表示拦截,返回false表示不拦截,子View可以调用getParent().requestDisallowInterceptTouchEvent(true)
来请求父ViewGroup不拦截事件(只对MOVE和UP事件生效),接着遍历子View,找到消费了DOWN事件的子View,并将后续MOVE和UP事件直接分发给消费了DOWN事件的子View,若没有子View消费DOWN事件,则会调用View的dispatchTouchEvent()
,若返回true,则会调用此ViewGroup的onTouchEvent()
将事件交给自己的onTouchEvent()
处理,后续的MOVE和UP事件将不再向下分发,直接交给ViewGroup#onTouchEvent处理
View的事件分发
现在事件已经从ViewGroup分发到了View,我们接着来看View的事件分发
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;
// --------注释9 --------
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
...
return result;
}
从代码中可以看出,如果View正在被拖拽,则直接消费掉事件,mListenerInfo
是ListenerInfo的对象,在给View设置一些监听的时候贵初始化mListenerInfo
,看到注释9处的条件判断,View默认就是enable的,所以只要设置了OnTouchListener,会调用OnTouchListener#onTouch方法,返回true,则事件被消费,返回false,则继续执行onTouchEvent(event)
方法,onTouchEvent返回true,则事件被消费,继续跟进View#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();
// --------注释10 --------
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;
}
}
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;
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);
}
// --------注释12 --------
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();
}
}
}
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();
// --------注释11 --------
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;
}
// --------注释13 --------
return true;
}
return false;
}
看到注释10处,只要View是可点击的或者可长按的clickable就为true,并且View即使设置为DISABLED,也不会对clickable产生影响 ,依旧返回clickable,如果clickable为true,则说明事件被消费了,只不过对这个事件没有响应,看到注释13处直接返回clickable,表明如果View是可以点击的,那么直接消费掉事件,我们继续往下看在DOWN事件中给mHasPerformedLongPress
标记位设为false,表示目前还没有处理长按事件,接着继续判断当View是在可滑动容器时,注释11处发送一个延迟100ms的mPendingCheckForTap
任务来检查是否是长按事件,ViewConfiguration.getTapTimeout()
返回的是100ms,CheckForTap代码如下
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
如果View不是在可滑动容器里面,则直接调用checkForLongClick()
方法来检查长按事件,跟进View#checkForLongClick
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();
}
mPendingCheckForLongPress.setAnchor(x, y);
mPendingCheckForLongPress.rememberWindowAttachCount();
mPendingCheckForLongPress.rememberPressedState();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
注意这里mHasPerformedLongPress
也被赋值false,ViewConfiguration.getLongPressTimeout()
返回的是500ms,这段代码又发送了一个500ms的延迟任务mPendingCheckForLongPress
来表示长按事件,看到UP事件里面的注释12处,大家应该还记得之前在DOWN事件中和checkForLongClick()
方法中我们将mHasPerformedLongPress
赋值为false,如果在UP事件时mHasPerformedLongPress
依然为false,则表明没有长按事件,调用removeLongPressCallback()
移除长按事件回调,继续回到checkForLongClick()
,栏看下mPendingCheckForLongPress的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) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
public void setAnchor(float x, float y) {
mX = x;
mY = y;
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
public void rememberPressedState() {
mOriginalPressedState = isPressed();
}
}
可以看到run方法里面调用了performLongClick(mX, mY)
,当返回值为true时,将mHasPerformedLongPress
标记设为true,所以若从DOWN事件到UP时间超过了500ms,则认为事件是长按事件,否则是点击事件,继续跟进performLongClick方法发现最终会调用performLongClickInternal(float x, float y)
,并且返回performLongClickInternal(float x, float y)
的返回值,继续跟进
View#performLongClickInternal
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) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if ((mViewFlags & TOOLTIP) == TOOLTIP) {
if (!handled) {
handled = showLongClickTooltip((int) x, (int) y);
}
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}
可以看到这里调用了mOnLongClickListener.onLongClick
回调,就是我们代码中设置长按事件的回调,如果我们在onLongClick回调中返回true,则mHasPerformedLongPress
标记设为true,在onTouchEvent()
方法的UP事件中就会判定该事件为长按事件,来看到这段代码
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();
}
}
}
如果mHasPerformedLongPress
标记为true,则不会执行条件里面的
post(mPerformClick)
,跟进PerformClick
private final class PerformClick implements Runnable {
@Override
public void run() {
performClickInternal();
}
}
run方法中调用了performClickInternal()
方法,跟进performClickInternal()
发现继续调用了performClick()
,跟进View#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;
}
可以看到其中调用了mOnClickListener.onClick(this)
,就是我们在代码中设置的点击监听回调,到此事件分发就分析完毕了。
六、事件分发总结
Activity事件分发
- Activity#dispatchTouchEvent
- PhoneWindow#superDispatchTouchEvent
- DecorView#superDispatchTouchEvent
- ViewGroup#dispatchTouchEvent
- 若事件没有被消费,则最终调用Activity#onTouchEvent
ViewGroup事件分发
- ViewGroup#dispatchTouchEvent(对down事件特殊处理:cancelAndClearTouchTargets(),resetTouchState(),清除标记后down事件一定会执行onInterceptTouchEvent(),onInterceptTouchEvent()默认返回false,不拦截)
- 若没有拦截,ViewGroup#dispatchTansformedTouchEvent,for循环遍历子View,调用子view的dispatchTouchEvent,若没有子view消费事件,则直接调用super.dispatchTouchEvent(event) ,此时如果 child 为View,则直接调用View的dispatchTouchEvent(event)处理事件,如果child 为ViewGroup,则事件最终会来到ViewGroup的onTouchEvent()方法
- 若有子view消费事件,则继续将down事件后续的move和up事件分发给子view处理,ViewGroup不做处理
- 若没有view消费事件,则down事件后续的move和up事件不再向下分发,直接交给ViewGroup#onTouchEvent处理
- 子view可以调用getParent().requestDisalowInterceptTouchEvent()请求ViewGroup不拦截事件(只对MOVE和UP事件有效)
View事件分发
- View#dispatchTouchEvent(若view被设置了监听器,则会先调用监听器的 onTouch()方法,若onTouch()方法返回true,则事件被消费,不会调用onTouchEvent(), 若返回false,则会继续调用onTouchEvent() )
- onTouch()返回false,View#onTouchEvent(若view是可以被点击的(clickable == true),则直接返回true消费掉事件),在down事件里面会做一个长按事件的检测 ,在up事件中检测没有长按事件则移除长按事件回调并响应点击事件,如果设置了长按事件并且长按事件的onLongClick()返回为true,则不执行onClick(),若onLongClick()返回为false,则会继续执行onClick()事件
- 行先后顺序:dispatchTouchEvent-> onTouch -> onTouchEvent -> onLongClick() -> onClick()