愿每次回忆,对生活都不感到负疚。 — 郭小川
写在前面
用户通过触摸屏幕与手机交互,所以触摸事件成为了交互中相对重要的因素。比如我们想要点外卖,就要通过手指触摸屏幕打开外卖软件,选择喜爱的菜品下单等待外卖小哥送餐,然而在等待过程中,我们还可以查看当前外卖的状态,这些行为我们都需要触摸屏幕来完成。那么问题来了,应用软件是如何知道手指触摸的是哪一块区域呢?
接下来让我们带着问题来分析触摸事件是如何处理的。
触摸事件
当触摸事件产生时,最先传递给Activity,那么我们就从Activity开始剖析。
1.Activity分发事件
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
/**
* 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)) { // 1
return true;
}
return onTouchEvent(ev); // 2
}
}
从注释1处可以看到getWindow().superDispatchTouchEvent(ev)如果返回true,dispatchTouchEvent(MotionEvent ev)就直接返回true,代表该事件已经被处理,Activity不处理该事件,否则会调用注释2处的onTouchEvent(ev)方法处理事件,继承自Activity的子类可以重写onTouchEvent(ev)方法处理事件。
下面我们来看一下getWindow():
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
.......
}
/**
* Retrieve the current {@link android.view.Window} for the activity.
* This can be used to directly access parts of the Window API that
* are not available through Activity/Screen.
*
* @return Window The current window, or null if the activity is not
* visual.
*/
public Window getWindow() {
return mWindow;
}
}
从以上代码可以看出getWindow()返回的是mWindow,mWindow在attach()方法中通过new PhoneWindow(this, window, activityConfigCallback)创建的,Window是抽象类,PhoneWindow是Window的实现类,所以getWindow().superDispatchTouchEvent(ev)实则是调用的PhoneWindow的superDispatchTouchEvent(ev)方法。
2.PhoneWindow分发事件
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
public PhoneWindow(Context context, Window preservedWindow,
ActivityConfigCallback activityConfigCallback) {
this(context);
// Only main activity windows use decor context, all the other windows depend on whatever
// context that was given to them.
mUseDecorContext = true;
if (preservedWindow != null) {
mDecor = (DecorView) preservedWindow.getDecorView(); // 1
mElevation = preservedWindow.getElevation();
mLoadElevation = false;
mForceDecorInstall = true;
// If we're preserving window, carry over the app token from the preserved
// window, as we'll be skipping the addView in handleResumeActivity(), and
// the token will not be updated as for a new window.
getAttributes().token = preservedWindow.getAttributes().token;
}
// Even though the device doesn't support picture-in-picture mode,
// an user can force using it through developer options.
boolean forceResizable = Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
mSupportsPictureInPicture = forceResizable || context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE);
mActivityConfigCallback = activityConfigCallback;
}
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
}
从superDispatchTouchEvent(MotionEvent event)中可以看到,调用了mDecor.superDispatchTouchEvent(event),mDecor是在PhoneWindow的构造函数中通过preservedWindow.getDecorView()初始化的,详见注释1。
3.DecorView分发事件
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}
从superDispatchTouchEvent(MotionEvent event)中可以看到,调用了super.dispatchTouchEvent(event),DecorView继承自FrameLayout,FrameLayout又继承自ViewGroup,所以super.dispatchTouchEvent(event)实则是调用了ViewGroup的dispatchTouchEvent(event)方法。
4.ViewGroup分发事件
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) { // 1
/**
* 当手指按下时重置所有状态,为之后的除了down之外的一系列事件做准备
*/
cancelAndClearTouchTargets(ev);
resetTouchState();
}
/**
* 当触摸事件为down或者触摸事件不为down且mFirstTouchTarget不为空的情况下,
* 去检查是否需要拦截事件。
*/
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // 2
/**
* 此处判断当前父控件是否不干预拦截事件,默认为false,
* 会调用onInterceptTouchEvent(ev)判断是否拦截事件。
* 一旦disallowIntercept等于true,就会直接走else,不拦截事件,
* 也就不会调用onInterceptTouchEvent(ev)判断是否拦截事件。
*
* 子类可以通过requestDisallowInterceptTouchEvent()方法设置mGroupFlags。
*
* onInterceptTouchEvent(ev)默认返回false不拦截事件,
* 继承自ViewGroup的子类可以重写该方法决定是否拦截事件,
* 如果子控件调用了父控件的requestDisallowInterceptTouchEvent()方法,
* 父控件就不会调用onInterceptTouchEvent(ev)方法了,
* 此时的onInterceptTouchEvent(ev)是无效的。
*
* 如果拦截事件,intercepted会置为true,
* 否则不拦截事件,intercepted会置为false。
*/
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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
/**
* 检查事件是否已经cancel
*/
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL; // 3
// 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) { // 4
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
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);
/**
* newTouchTarget为null并且有子控件就会去遍历子控件
*/
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) { // 5
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;
for (int i = childrenCount - 1; i >= 0; i--) { // 6
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;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) { // 7
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);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 8
// 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();
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();
}
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;
}
}
}
// 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;
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();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
}
从注释1处可以看到当触摸事件为down(手指按下)时会重置所有状态,为之后的一系列事件做准备。在注释2处可以看到当触摸事件为down或者触摸事件不为down且mFirstTouchTarget不为null的情况下去检查是否需要拦截事件。注释3处检查事件是否已经cancel。注释4处可以看到,事件没有被取消也没有被拦截就会走到注释5处。注释5处newTouchTarget为null并且有子控件就会去遍历子控件分发事件。注释6处可以看到for循环遍历子控件,判断子控件是否能够接收到触摸事件,如果子控件能够接收到触摸事件,则交由子控件来处理。注释7处会判断当前触摸事件是否在子控件范围内或者子控件是否在播放动画,如果触摸事件没有在子控件范围内或者子控件正在播放动画,则会开始遍历下一个子控件,否则会调用注释8处的dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)方法,该方法做了什么之后再讲。最后来看返回值handled,如果handled返回true表示该事件已经得到处理。
下面看一下dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits)方法:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) { // 1
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
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);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
}
这段代码我们只看第一个if,从注释1处可以看到如果子控件为null,则会调用super.dispatchTouchEvent(event),否则会调用child.dispatchTouchEvent(event),也就是向子控件分发事件,最后将结果handled返回。因为ViewGroup继承自View,
所以super.dispatchTouchEvent(event)实则是调用的View的dispatchTouchEvent(event)。
5.View分发事件
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
/**
* ListenrInfo是一个用来存放事件监听器的静态类。
* mOnTouchLisenter就是我们调用View的setOnTouchListener(listener)方法设置的监听器。
* 如果mOnTouchListener不为null并且当前控件是enabled状态,
* 就会调用mOnTouchListener.onTouch(this, event)方法,如果返回true,
* 表示这个事件已经被处理了,就不会调用onTouchEvent(event)去处理事件,
* 由此证明了OnTouchListener的onTouch()比onTouchEvent(event)优先级高。
*/
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) { // 1
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
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
}
从注释1处可以看到,如果mOnTouchListener不为null并且当前控件是enabled状态,就会调用mOnTouchListener.onTouch(this, event)方法,如果结果返回true,表示这个事件已经被处理了,就不会调用onTouchEvent(event)去处理事件,由此证明了OnTouchListener的onTouch()比onTouchEvent(event)优先级高。如果mOnTouchListener.onTouch(this, event)方法结果返回false就会调用onTouchEvent(evnet)方法处理事件。
6.View处理事件
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
/**
* Implement this method to handle touch screen motion events.
*
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
*
* - obeying click sound preferences
*
- dispatching OnClickListener calls
*
- handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
*
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
/**
* CLICKABLE代表点击
* LONG_CLICKABLE代表长按点击
* 可以通过setClickable()和setLongClickable()设置,
* 在调用setOnClickListener()和setOnLongClickListener()时会自动设置为CLICKABLE和LONG_CLICKABLE
* 只要CLICKABLE和LONG_CLICKABLE有一个成立,也就是setOnClickListener或者setOnLongClickListener了,
* clickable就会为true。
*/
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 1
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;
}
}
/**
* clickable为true就会处理点击事件或者长按事件
*/
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { // 2
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);
}
/**
* 在up事件发生时,也就是手指抬起时,
* 如果mHasPerformedLongPress的值为false并且mIgnoreNextUpEvent的值也为false,
* 就会调用performClickInternal()处理点击事件。
*/
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 3
// 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(); // 4
}
}
}
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); // 5
}
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;
}
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;
}
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);
}
}
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;
}
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();
}
}
}
注释1,2,3处都加了解释,可以在代码中直接看。
这里主要介绍点击事件和长按事件是如何处理的:
首先我们来看注解5处,当down事件产生时,就会去检查长按事件调用checkForLongClick(0, x, y),传入参数x,y的坐标,在该方法内部会调用postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset)方法,mPendingCheckForLongPress是Runnable的实现类,ViewConfiguration.getLongPressTimeout() - delayOffset表示长按事件触发的时间。接着来看mPendingCheckForLongPress内部run()方法中调用了performLongClick(x,y)方法,performLongClick(x,y)内部调用了无参数的performLongClick()方法,无参数的performLongClick()内部又调用了performLongClickInternal(x, y)方法,performLongClickInternal(x, y)内部就会去判断是否设置了长按事件监听器,如果设置了长按事件监听器,则调用mOnLongClickListener.onLongClick(View.this)并返回结果赋值给handled,performLongClickInternal(x, y)方法最终会将handled的值返回给performLongClick(x,y)方法,如果返回结果为true,表示该事件已经被长按处理,mHasPerformedLongPress会赋值为true,这个值在up事件产生时会用到。
长按事件调用流程:checkForLongClick(0,x,y) >>> postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset) >>> performLongClick(x,y) >>> performLongClickInternal(x, y) >>> mOnLongClickListener.onLongClick(View.this)
现在来看注解4处,当up事件产生时,如果mHasPerformedLongPress的值为false并且mIgnoreNextUpEvent的值也为false, 就会调用performClickInternal()处理点击事件。performClickInternal()内部会调用performClick()方法,performClick()内部回去判断是否设置了点击事件监听器,如果设置了点击事件监听器,则调用mOnClickListener.onClick(this)方法处理,并将处理结果返回。
短按事件调用流程:performClickInternal() >>> performClick() >>> mOnClickListener.onClick(this)
总结
方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent() | 有 | 有 | 有 |
onInterceptTouchEvnet() | 无 | 有 | 无 |
onTouchEvnet() | 有 | 有 | 有 |
onInterceptTouchEvent()和onTouchEvnet()都在dispatchTouchEvent()中调用。
这里使用一个例子来了解触摸事件的处理流程:我所在的项目来了一个需求,项目经理肯定不会去动代码呀,所以项目经理把这个需求分发给了我的Leader,而我的Leader还有别的事情要忙,所以这个需求又给了我,我看了一眼这个需求不合理,我又把这个需求还给了Leader,Leader听我说完后也认同需求不合理,所以这个需求又回到了项目经理手里,最终这个需求没有处理。
在这个例子里,项目经理就是Activity,Leader手底下不止我一个开发,所以Leader就是ViewGroup,我就是View。从项目经理分发需求给Leader,Leader将需求又分发给我,我又将需求打回给Leader,Leader将需求打回给项目经理,中间传递的过程就是dispatchTouchEvent(),谁都不处理就会返回false;如果需求在分发给Leader时,Leader觉得他就可以处理,就会在dispatchTouchEvnet()中调用onInterceptToucheEvent()拦截事件,调用Leader的onTouchEvnet()处理事件,并在dispatchTouchEvent()返回true告诉项目经理可以做;如果我处理了这个需求,就会调用我的onTouchEvent()处理事件,在dispatchTouchEvent()返回true告诉Leader我可以做。
通俗点说吧,触摸事件产生会一层一层的向下分发,谁有能力谁就拦截该事件并处理,如果都不能处理,触摸事件还会一层一层的向上传递,谁有能力谁就拦截该事件并处理,否则该事件就不会得到处理。