基础概念
- 触摸事件:手指触摸屏幕时生成的事件,即
MotionEvent
。常见的触摸事件有:ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
以及ACTION_CANCEL
,当存在多个手指触摸屏幕时,还会触发ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件。 - 焦点事件:
ACTION_DOWN
和ACTION_POINTER_DOWN
属于焦点事件,通过MotionEvent
中的PointerId
进行描述; - 触摸事件序列:从手指触摸屏幕开始到手指离开屏幕结束的过程中触发的一系列事件,通常以
ACTION_DOWN
事件开始、ACTION_UP
事件结束,中间有不定数量的ACTION_MOVE
、ACTION_POINTER_DOWN
或者ACTION_POINTER_UP
事件的一系列事件。 - 滑动冲突:
View
树中相邻的层级上均存在可滑动的View
,当用户触摸屏幕时触发了ACTION_MOVE
事件导致有多个View
可以处理的情况。 - 事件分发机制:触摸屏幕产生的事件
MotionEvent
在整个View
树上分发处理的逻辑,理解事件分发机制的实现原理才能知道如何解决View
滑动冲突问题。
源码分析
事件分发流程
当用户开始触摸手机屏幕时,经过传感器等一系列硬件处理,最终生成触摸事件并由SystemServer
进程的InputMangerService
服务通过Socket
发送到目标应用进程,经过多个的InputStage
分发之后触摸事件被传递到DecorView#dispatchPointerEvent
方法,dispatchPointerEvent
方法内部调用了dispatchTouchEvent
方法,通过PhoneWindow#getCallback
方法拿到目标Activity
,最终调用Activity#dispatchTouchEvent
方法进行处理。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
}
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// mWindow是PhoneWindow,cb是Activity
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
}
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
ContentCaptureManager.ContentCaptureClient {
/**
* 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.
*
* @see #onTouchEvent(MotionEvent)
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 通过Window进行分发最终交由根布局DecorView进行处理,如果处理成功则将事件从队列中移除,否则交由onTouchEvent继续处理;
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 如果整个View树都没有处理触摸事件,则由Activity的onTouchEvent处理
return onTouchEvent(ev);
}
/**
* 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;
}
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, IBinder assistToken,
IBinder shareableActivityToken) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mActivityInfo = info;
mWindow = new PhoneWindow(this, window, activityConfigCallback);
// 省略其他代码
}
}
接着,触摸事件会被分发给目标Activity
的PhoneWindow
(在Activity
实例创建之后调用attach
方法中进行创建)进行处理。经过PhoneWindow#superDispatchTouchEvent
方法将触摸事件交由DecorView.superDispatchTouchEvent
。因为DecorView
继承自FrameLayout
,而FrameLayout
继承自ViewGroup
,所以触摸事件最终由ViewGroup.dispatchTouchEvent
开始分发处理。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
// 省略其他代码
}
}
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}
至此,触摸事件已经被分发到View
树的根节点,开始在View
树上进行遍历(深度遍历)和分发处理,总结流程图如下:
为了侧重分析触摸事件的分发流程,下面主要围绕关键代码进行分析,和事件分发流程关联不大的部分直接略过;
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
// First touch target in the linked list of touch targets.
// 记录当前父View下第一个处理触摸事件的View对象,内部通过链表的形式进行维护
@UnsupportedAppUsage
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// 过滤不安全的触摸事件
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// STEP 1:ACTION_DOWN事件标识一个事件序列的开始,需要重置之前事件处理过程中的标记以及中间状态;
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev); // 清空之前事件序列处理过程中的所有TouchTarget
resetTouchState(); // 重置之前事件序列处理过程中的触摸状态标记
}
// 检查是否需要拦截
final boolean intercepted;
// STEP 2:根据事件类型以及是否存在前序事件被子View消费来判断是否调用onInterceptTouchEvent方法让当前ViewGroup尝试拦截,如果父容器不拦截则直接分发给子View处理(如果没有子View能处理触摸事件则交由当前ViewGroup处理),否则直接由当前ViewGroup进行处理;
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 {
// 前序事件一直都是当前ViewGroup处理,所以当前ViewGroup继续处理本次事件
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 isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0 && !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
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);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 获取当前触摸事件在当前ViewGroup中的相对坐标 x, y 值
final float x = ev.getXDispatchLocation(actionIndex);
final float y = ev.getYDispatchLocation(actionIndex);
// Find a child that can receive the event.
// 从前到后遍历子View来寻找能够接收本次事件的子View
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// STEP 3:遍历并分发触摸事件给所有的子View进行处理
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
...
// 判断触摸事件是否可以被子View响应,canReceivePointerEvents方法根据View可见性和动画进行判断,isTransformedTouchPointInView判断触摸事件是否落在View的区域内(兼容了属性动画)
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 判断子View是否消费过之前的触摸事件,如果消费过之前的触摸事件,则直接结束遍历,进入下面的处理流程,会将事件再次分发给这个子View
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// STEP 4:将触摸事件交给子View进行处理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
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 = x;
mLastTouchDownY = y;
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();
}
// 如果遍历完子View之后触摸事件没有被消费,则交给之前最早消费触摸事件的View进行处理
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// STEP 5:如果没有子View能够处理触摸事件,则交给当前ViewGroup进行处理
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;
// 之前遍历子View的过程中已经消费过触摸事件的子View
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.
// 如果取消或者ACTION_UP事件发生则清空所有触摸状态
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;
}
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // 清除子View对父View的拦截标志
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
}
从DecorView#superDispatchTouchEvent
方法梳理出来的主要流程:
首先,ViewGroup#dispatchTouchEvent
会先判断是否需要由ViewGroup
拦截事件,如果触摸事件没有被DecorView
拦截的情况下,则会将触摸事件分发给所有的子View
,根据子View
的类型进行进一步的分发:如果子View
是ViewGroup
那么会重复上面的过程,否则会通过View#dispatchTouchEvent
方法来处理触摸事件,如果子View
不消费触摸事件,则分发给下一个子View
进行处理,直到触摸事件被某个View
处理或者整个View
树都没有处理,整个事件分发流程就结束了。
下面针对这个流程中的主要步骤进行分析:
STEP 1:
首先,如果当前触摸事件是ACTION_DOWN
事件,那么会重置ViewGroup
的标识位,所以子View
无法禁止其所在的ViewGroup
对ACTION_DOWN
事件的拦截动作的,因此ACTION_DOWN
事件一定会先被ViewGroup#onInterceptTouchEvent
进行处理;
// Handle an initial down.
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();
}
STEP 2:
根据触摸事件的类型以及前序触摸事件是否有对应的处理者来决定是否由当前ViewGroup
进行拦截:
- 如果当前触摸事件是
ACTION_DOWN
,则disallowIntercept
变量的值一定为false
,因此直接调用ViewGroup#onInterceptTouchEvent
方法判断是否拦截触摸事件; - 如果当前触摸事件不是
ACTION_DOWN
,但是本次事件序列中的前置事件已经被某个子View
消费,即mFirstTouchTarget
不为null
,那么判断是否存在子View
调用ViewGroup#requestDisallowInterceptTouchEvent
禁止当前ViewGroup
拦截触摸事件,禁止的情况下当前ViewGroup
无法拦截,否则调用当前ViewGroup#onInterceptTouchEvent
方法判断是否拦截; - 如果当前触摸事件不是
ACTION_DOWN
,并且之前的触摸事件没有被当前ViewGroup
的任意一个子View
消费过,则当前触摸事件直接被当前ViewGroup
进行拦截,因此如果子View
没有消费ACTION_DOWN
,那么同一个事件序列中的后续触摸事件默认交由当前ViewGroup
进行处理;
// 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;
}
总结如下:
- 对于
ACTION_DOWN
类型的触摸事件,一定会调用ViewGroup#onInterceptTouchEvent
方法尝试拦截; - 对于非
ACTION_DOWN
类型的触摸事件,如果前序事件没有被子View
消费过,一定会被ViewGroup
拦截,即不会分发给子View
; - 对于非
ACTION_DOWN
类型的触摸事件,如果前序事件有被子View
消费过,那么会判断是否存在子View
禁止ViewGroup
拦截事件,如果没有就会调用ViewGroup#onInterceptTouchEvent
方法尝试拦截,否则不会拦截,直接分发给子View
进行处理;
STEP 3&4:
根据当前ViewGroup
是否拦截触摸事件(变量intercepted
的取值)来决定是否遍历子View
来处理本次触摸事件,如果当前ViewGroup
拦截触摸事件则不会遍历子View
来分发处理触摸事件,直接判断mFirstTouchTarget
是否为null
,如果mFirstTouchTarget
为null
,说明目前没有子View
处理过本次触摸事件序列中的触摸事件,则直接交由当前ViewGroup
调用View#dispatchTouchEvent
进行处理。否则从mFirstTouchTarget
开始遍历之前处理过触摸事件的子View
处理触摸事件;
否则,先遍历子View
对本次触摸事件进行处理,如果触摸事件落在了某个子View
的区域内,那么就调用ViewGroup#getTouchTarget
来查找当前子View
是否在之前处理过触摸事件,如果之前已经处理过触摸事件,则更新其处理过的pointerIdBits
,否则调用ViewGroup.dispatchTransformedTouchEvent
方法来间接调用View#dispatchTouchEvent
处理本次触摸事件。
STEP 5:如果遍历所有子View
之后此触摸事件没有被处理,则交由当前ViewGroup
进行处理,最终调用到当前ViewGroup
的View#dispatchTouchEvent
方法来处理本次触摸事件;
整理流程如下图所示:
注意点:
ViewGroup
负责拦截和分发触摸事件,View
负责处理触摸事件;
事件处理流程
触摸事件经过ViewGroup
分发之后,非容器类的View
就可以对落在自身区域中的触摸事件进行具体的处理,如果最终成功消费了触摸事件,那么这次触摸事件的分发处理流程就结束了。
接着看下非容器类的View
是如何处理触摸事件的。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
/**
*
* @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) {
// ...
boolean result = false;
// ...
// 1. 先对触摸事件进行安全性校验,如果通过了才会进入后续的处理流程;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 2. 如果View设置了OnTouchListener对象,则优先将触摸事件交给其进行处理;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 3. 如果没有设置OnTouchListener或者OnTouchListener的onTouch调用后返回false,那么就调用onTouchEvent方法进行处理。
if (!result && onTouchEvent(event)) {
result = true;
}
}
// ...
return result;
}
}
从源码可以看出View.dispatchTouchEvent
进行了以下处理:
- 如果子
View
设置了OnTouchListener
则调用其onTouch
方法进行处理; - 如果没有设置
OnTouchListener
或onTouch
方法返回false
,则交由View.onTouchEvent
进行处理;
由此可见,触摸事件会先被交由OnTouchListener
进行处理,如果没有被OnTouchListener
处理,才会走到onTouchEvent
方法。下面看下onTouchEvent
方法的内部实现。
/**
* Implement this method to handle touch screen motion events.
* <p>
* 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:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @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();
// 是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 是否enable
if ((viewFlags & ENABLED_MASK) == DISABLED && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false); // 取消按压态
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// 注意:即使View是disabled的,只要其是可点击的,那么触摸事件就会被直接消费,只是没有做任何响应。
return clickable;
}
// 如果有代理则交由代理处理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果可点击或者设置了TOOLTIP标记,则尝试进行处理,否则直接不处理
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// ACTION_UP:如果View变为不可点击则移除callback并结束处理流程
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// ...
// ACTION_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) {
// 通过抛任务来保证先展示可见状态,然后执行点击动作触发的任务
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
// 重置点击相关的状态
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
// ...
mHasPerformedLongPress = false;
// ACTION_DOWN:如果View不可点击则通过post一个延迟任务(默认400ms)来检测是否为长按行为
if (!clickable) {
checkForLongClick(ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
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.
// ACTION_DOWN:如果所在的容器是可滑动的,则延迟处理本次事件(默认100ms)
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
// ACTION_DOWN:如果所在的容器是不可滑动的,则设置按压态并post一个延迟任务(默认400ms)来检测是否为长按行为
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
// ACTION_CANCEL:如果View可点击,则清除按压态
if (clickable) {
setPressed(false);
}
// ACTION_CANCEL:移除所有callback
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
// ACTION_MOVE:如果移动到了View范围以外,则移除所有callback
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(0 /* send immediately */, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
通过上述源码分析,梳理整个触摸事件序列的处理流程如下:
- 如果
View
是可点击的但是被disabled
了,则直接返回true
,否则继续执行; - 如果设置了
TouchDelegate
则调用其onTouchEvent
方法进行处理,如果onTouchEvent
方法返回true
,则直接返回true
; - 否则,判断
View
是不是可点击的或者设置了TOOLTIP
标记,如果不是可点击的且没有设置TOOLTIP
标记,直接返回false
;否则根据事件类型进行对应的处理,但是最终返回的值为true
; - 根据触摸事件类型进行不同的处理:
ACTION_DOWN
事件:如果View不可点击则通过post一个延迟任务(默认400ms
)来检测是否为长按行为,否则,判断所在的容器是不是正在滑动,是的话则延迟处理本次事件(默认100ms
),否则设置按压状态并post一个延迟任务(默认400ms)来检测是否为长按行为;
ACTION_UP
事件:如果View
不可点击则移除所有callback
并结束处理流程,否则移除长按检测callback
,并按照点击行为来执行相关的点击任务;
ACTION_MOVE
事件:如果触摸事件的坐标已经离开了View
的范围,那么移除所有callback
,即不会执行View的任何逻辑,因为已经不在响应区域了;
ACTION_CANCEL
事件:移除所有callback
;
由此可见,View
的点击行为是通过ACTION_DOWN
和ACTION_UP
事件都被同一个View
消费来触发执行的,View
的长按行为是通过在ACTION_DOWN
事件发生之后向消息队列抛延迟任务来实现的。
思路借鉴
从Android
事件分发机制的源码实现中存在一些比较不错的思路,可以在日常开发借鉴学习。
一、模版方法模式:在分发触摸事件给子View之前,加入钩子来实现触摸事件的拦截;
// 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;
}
二、职责分离:ViewGroup
负责拦截和分发触摸事件,View
负责处理触摸事件;
/**
* 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;
// 省略其余代码
// 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;
}
三、缓存加速:TouchTarget
记录上次触摸事件的处理者,加速后续触摸事件的分发处理;
- 遍历子
View
分发触摸事件,记录处理触摸事件的子View
为TouchTarget
;
/**
* 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;
}
- 新的事件序列开始或者发生取消事件时,清空之前的
TouchTarget
记录;
/**
* 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;
}
}
四、动画处理
通过对触摸事件进行转换处理,同时兼容子View的动画,保证点击事件在动画区域得到响应;
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}