View触摸事件分发机制
View的事件传递,首先从Activity开始,
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
当用户点击屏幕的时候,首先触发的是Activity的 dispatchTouchEvent
方法,分发的源头从这里开始。在这里可以看到会将事件分发给window,如果window的superDispatchTouchEvent
方法返回的是false则会给activity的onTouchEvent
方法处理。
然后查看window的superDispatchTouchEvent
方法,我们知道window是一个抽象类,而唯一实现了这个抽象类的是PhoneWindow
,在PhoneWindow
里面
//PhoneWindow内
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView内
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
这个mDecor是一个DecorView
,DecorView
是一个ViewGroup,也是界面显示的view树的根布局,在superDispatchTouchEvent
方法内调用了dispatchTouchEvent
从这里开始向下各个view分发事件。
事件的分发离不开dispatchTouchEvent
、onTouchEvent
方法,ViewGroup比View还多一个onInterceptTouchEvent
。其中dispatchTouchEvent
负责事件的分发,onTouchEvent
负责事件的消费ViewGruop多的onInterceptTouchEvent
事件负责事件的拦截。一次完成的触摸事件又分为MotionEvent.ACTION_DOWN
、MotionEvent.ACTION_MOVE
、MotionEvent.ACTION_UP
(多点触控相关不做分析)。下面开始详细分析。
接上面的代码,首先从ViewGroup的dispatchTouchEvent
方法分析
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 当事件为 ACTION_DOWN时候 说明是一次新的事件,清除mFirstTouchTarget
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 判断事件是否被拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//当事件为ACTION_DOWN 或者mFirstTouchTarget不为空 调用onInterceptTouchEvent()
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 如果事件不是canceled 并且没有被拦截 开始分发给下面子view 并且是ACTION_DOWN
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 childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断子view是否在点击区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//查找这个子child是否在mFirstTouchTarget内
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;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
//newTouchTarget为空 且mFirstTouchTarget不为空
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// mFirstTouchTarget为空 说明没有子view消费
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;
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.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
}
return handled;
}
代码较多,删除了一些其他如多点触控、光标、无障碍等相关代码。逐行分析开始
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
每次点击屏幕的时候 ACTION_DOWN
代表开启一次新的事件,会调用cancelAndClearTouchTargets
方法清除上次相关,这里的mFirstTouchTarget代表着消费了上次ACTION_DOWN
事件的view结构树。
/**
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
//省略
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
在cancelAndClearTouchTargets
方法会遍历mFirstTouchTarget执行dispatchTransformedTouchEvent
然后将其置空,在dispatchTransformedTouchEvent
方法内主要是判断是子view是否消费了事件。省略一些代码后
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//如果cancel为true 或者MotionEvent.ACTION_CANCEL 取消这次事件
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//当child为空 调用父类的dispatchTouchEvent方法
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//否则分发给子view
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;
}
上面调用 dispatchTransformedTouchEventcancel
cancel为true说明将此事件取消所以先修改event的事件event.setAction(MotionEvent.ACTION_CANCEL);
然后传给子view。
回到dispatchTouchEvent
方法,清除上次事件之后会判断是否要拦截,还是向下分发
// 判断事件是否被拦截 intercepted为false的时候会向下往子view分发
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//当事件为ACTION_DOWN 或者mFirstTouchTarget不为空 调用onInterceptTouchEvent()
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
如果ACTION_DOWN
事件未消费,后面的MOVE和UP事件mFirstTouchTarget会为空,intercepted会默认为true,不会在向下分发。也就是说DOWN事件子view没有消费,后面的MOVE和UP就不会分发给子view了,如果是DOWN事件或者其他事件且mFirstTouchTarget不会空,会根据onInterceptTouchEvent
的返回值来判断是否分发,onInterceptTouchEvent
默认是返回false。
下面开始向子view分发,newTouchTarget代表本次消费事件的view
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 如果事件不是canceled 并且没有被拦截 并且是ACTION_DOWN 开始分发给下面子view
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 childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断子view是否在点击区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//加入到mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
在进行分发的时候在此判断了是否为ACTION_DOWN
事件,然后遍历所有的子view,查找需要消费此次事件的view,并且赋值给newTouchTarget。首先判断是否在点击区域,然后调用dispatchTransformedTouchEvent
判断这个view是否要消费此次事件,dispatchTransformedTouchEvent
内代码前面已经看过了,当返回true说明这个view要消费此次事件,然后调用addTouchTarget
方法,添加至mFirstTouchTarget
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
到这里基本就已经完成了查找,newTouchTarget就是消费此次事件的view,继续往下看
// mFirstTouchTarget为空 说明ACTION_DOWN事件肯定没有子view消费 直接交给父view
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;
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;
}
}
当mFirstTouchTarget为空的时候,说明ACTION_DOWN就没有子view消费这个时候调用dispatchTransformedTouchEvent
并且child参数传空,前面已经看过,child为空会调用父类的dispatchTouchEvent
方法。
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
}
mFirstTouchTarget不为空,然后再将事件传递给mFirstTouchTarget看它是否会消费
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
这里的alreadyDispatchedToNewTouchTarget
是在ACTION_DOWN
事件,并且有子view消费事件才为true,newTouchTarget是消费本次事件的View,如果alreadyDispatchedToNewTouchTarget && target == newTouchTarget
那说明是DOWN事件,且有子view消费本次事件,那说明不需要调用dispatchTransformedTouchEvent
来判断子view是否消费了。
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;
}
else的情况下就调用dispatchTransformedTouchEvent
来判断是否消费。
dispatchTouchEvent
事件基本就分析完了,文字表述有点乱,理份简化的流程图
看完ViewGroup的dispatchTouchEvent
方法在来看View的,在viewGroup里面dispatchTouchEvent
主要负责事件是否向下分发,而在View,已经没有子view可分发了,这个时候判断是否要自己消费掉。看代码,
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
final int actionMasked = event.getActionMasked();
//如果是ACTION_DOWN事件 停止滑动
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
//mOnTouchListener不为空 事件传给mOnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果mOnTouchListener.onTouch返回的是false 给onTouchEvent处理
if (!result && onTouchEvent(event)) {
result = true;
}
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
首先是判断是否有mOnTouchListener事件的监听,如果有的话看mOnTouchListener.onTouch的返回值是否为true,为true表示已经消费,不需要后续处理。为false调用onTouchEvent,再根据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 (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);
}
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();
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;
}
return true;
}
return false;
}
具体来看下onTouchEvent
里面的操作,首先是检查clickable
是否可以点击,如果当前view是可以点击状态,进入switch来判断点击、滑动、长按等事件,并且直接返回ture,否则返回false。就是说如果当前的view是可以点击状态的不管有没有设置点击长按等触摸事件,都会返回true。
举个例子,在一个LinearLayout内嵌套一个Button,给LinearLayout设置点击事件,不设置Button的。在没有其他情况干预下,点击button,由于button默认是可以点击状态,所以会消费掉此次事件,就算LinearLayout设置了点击事件也不会触发了。
在switch里面按照事件传递的顺序来分析,首先看 ACTION_DOWN
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
//clickable为false了 不再是可点击状态,调用checkForLongClick方法
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();
// 是否在滚动布局中
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//不在滚动容器中
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
在点击状态被改之后和不在滚动容器中调用了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);
}
}
在checkForLongClick
中检查是否可以长按,然后postDelayed
发送了一条消息,先看下postDelayed
public boolean postDelayed(Runnable action, long delayMillis) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.postDelayed(action, delayMillis);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().postDelayed(action, delayMillis);
return true;
}
这里可以getRunQueue拿到的是HandlerActionQueue
,HandlerActionQueue
是一个对列,用于存放等待更新的界面操作,更详细不在这里说了。
然后看下CheckForLongPress
是干嘛的
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();
}
}
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
public boolean performLongClick() {
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) {
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;
}
CheckForLongPress
的run方法调用了performLongClick
,最后调用到performLongClickInternal
方法,在performLongClickInternal
方法里面找到了熟悉的onLongClick,
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
这里就看到了长按事件的调用,当手指按下屏幕之后会将长按操作加入至对列表,但是这个操作是延迟执行的,延迟时间为可以查看代码发现默认是500毫秒,就是说按下屏幕500毫秒以后会调用长按方事件的监听
ViewConfiguration.getLongPressTimeout()
private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;
/**
* @return the duration in milliseconds before a press turns into
* a long press
*/
public static int getLongPressTimeout() {
return AppGlobals.getIntCoreSetting(Settings.Secure.LONG_PRESS_TIMEOUT,
DEFAULT_LONG_PRESS_TIMEOUT);
}
这是第一种情况,下面看下当在滚动布局中的情况。
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
}
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);
}
}
这里应该是为了等待滑动暂停,所以延迟一段时间再做长按事件判断处理。
接下来看ACTION_MOVE
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;
在ACTION_MOVE
里并没有什么很多操作,只是将刚刚两个长按事件的延迟消息给移除了。触摸屏幕已经开始滑动了,肯定就说明长按事件不需要了。
ACTION_UP
事件
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);
}
//长按事件是否消费了此次事件
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) {
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;
过滤掉其他代码,只关注这几行
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) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//将点击事件加入对列
if (!post(mPerformClick)) {
// 如果加入失败就直接执行
performClickInternal();
}
}
}
mHasPerformedLongPress
代表长按事件mOnLongClickListener.onLongClick返回的值,默认为false,在mHasPerformedLongPress为false才会进这个if然后和前面长按事件一样的操作,只是这里没有延迟。
代码基本看完,做个总结:
1、当手指按下屏幕的时候,首先调用Activity的dispatchTouchEvent
事件,然后将事件分发给Window的superDispatchTouchEvent
,然后给DecorView的dispatchTouchEvent
,开始分发给各个布局组件,
2、在ViewGroup的dispatchTouchEvent
里如果是Down事件,先清空上次的Target ,然后调用onInterceptTouchEvent
,如果返回false,分发给下面的子View,如果子view的dispatchTouchEvent
方法true说明这个子view要消费这个事件,记住这个子view,赋值给Target。如果没有子view消费这个事件将会调用返回onTouchEvent方法的返回值。
3、如果是非down事件,将不遍历子view分发,而是先判断Target是否为空,为空说明down事件没有子view消费,直接返回onTouchEvent
方法的返回值。就是说在子view没有在消费down事件,在后续的MOVE和UP事件就不会收到分发消息,同时在后续的MOVE和UP事件onInterceptTouchEvent
无法作出拦截,
4、如果target不为空调用onInterceptTouchEvent
方法,为true说明自身拦截了up或move事件,表示消费了此次事件。返回true,同事将事件改为ACTION_CANCE
L并且告诉子view,代表此次事件已经结束。onInterceptTouchEvent
为false的话直接将事件交给子view调用子view的dispatchTouchEvent方法。
5、在View的dispatchTouchEvent
方法内,如果onTouchListener
不为空且onTouch返回fals才会调用onTouchEvent
方法,在内onTouchEvent
对事件进行处理,判断是长按还是点击还是滑动。长按事件在down里面,当收到down事件后向消息队列发送一条消息默认500毫秒后调用长按事件的方法,如果500毫秒内收到了move或者up事件将这条消息移除。点击事件在up事件内,和长按事件一样是通过发送消息,当长按事件onLongClick返回true时点击事件将不会被触发。
个人学习笔记,如果错误欢迎大佬们指出。