Android 中的触摸事件是一个老生常谈的话题了,在 Android 面试中也经常问到,比较考察基础以及解决问题的能力,最近在巩固基础知识,希望通过本文对这个问题有更加深入的理解。
事件类型
在事件分发的过程中,MotionEvent 对象记录时间的触摸事件,例如点击、移动。
一次完整的 MotionEvent,包含用户触摸屏幕到离开屏幕。
事件 |
发生次数 |
描述 |
ACTION_DOWN |
1 |
手指触摸时触发 |
ACTION_MOVE |
1 ~ N |
手指在屏幕上滑动时触发 |
ACTION_UP |
1 |
手指离开屏幕时触发 |
ACTION_CANCEL |
0 ~ 1 |
事件被上层拦截时触发 |
核心方法
时间的分发流程涉及到三个核心方法 :
dispatchTouchEvent(MotionEvent event)
事件分发
onInterceptTouchEvent(MotionEvent ev)
事件拦截,只有 ViewGroup 有
onTouchEvent(MotionEvent event)
事件响应
事件分发涉及到的类包含 View、ViewGroup 和 Activity 等,他们都能处理触摸事件的分发,但是其方法有些区别。
方法 |
View |
ViewGroup |
Activity |
dispatchTouchEvent() |
√ |
√ |
√ |
onInterceptTouchEvent() |
× |
√ |
× |
onTouchEvent() |
√ |
√ |
√ |
分发流程
当 UI 主线程收到底层上报的 input 事件,会调用 InputEventReceiver.dispachInputEvent()
方法。
整个事件分发流程如下图:
DecorView.dispatchTouchEvent()
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); }
|
Activity 实现了 Window.Callback
接口,所以事件会分发到 Activity.dispatchTouchEvent
。
Activity.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
|
PhoneWindow.superDispatchTouchEvent()
public boolean superDispatchTouchEvent(KeyEvent event) { return mDecor.superDispatcTouchEvent(event); }
|
PhoneWindow 的顶层 View 是 DecorView,接着又会调用 DecorView 的 superDispatcTouchEvent()
方法,在 DecorView 中又调用了父类的 dispatchTouchEvent()
方法。
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
|
DecorView 的父类为 ViewGroup,接着会调用 ViewGroup 的 dispatchTouchEvent()
方法。
ViewGroup.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); }
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); } else { intercepted = false; } } else { intercepted = true; } ...
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(); final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);
if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }
newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; }
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { 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; } ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); }
if (newTouchTarget == null && mFirstTouchTarget != null) { newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } }
if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { 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; } }
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; }
|
其中 onInterceptTouchEvent()
方法可以实当前 ViewGroup 拦截事件,阻断事件分发流程。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled;
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; }
final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) { return false; }
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); }
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); }
transformedEvent.recycle(); return handled; }
|
该方法是 ViewGroup 真正处理事件的地方,分发子 View 来处理事件,过滤掉不相干的 pointer ids。当子视图为 null 时,MotionEvent 将会发送给该 ViewGroup。最终调用 View.dispatchTouchEvent()
方法来分发事件。
View.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent event) { ...
final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { stopNestedScroll(); }
if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } }
if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); }
if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); }
return result; }
|
先由 OnTouchListener.onTouch()
处理事件,如果返回了 true
,表示消费了事件,则不会调用 onTouchEvent()
方法。
若不拦截,继续调用 onTouchEvent()
方法:
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); }
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); }
if (prepressed) { setPressed(true, x, y); }
if (!mHasPerformedLongPress) { removeLongPressCallback();
if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } }
if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); }
if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { mUnsetPressedState.run(); }
removeTapCallback(); } break;
case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) { break; }
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); } break;
case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); break;
case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y);
if (!pointInView(x, y, mTouchSlop)) { removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { removeLongPressCallback(); setPressed(false); } } break; }
return true; } return false; }
|
总结
整个事件流程如下图:
onInterceptTouchEvent()
返回值 true
表示事件拦截, onTouch()
或 onTouchEvent()
返回值 true
表示事件消费。
触摸事件先交由 Activity.dispatchTouchEvent()
,再一层层往下分发,当中间的 ViewGroup 都不拦截时,进入最底层的 View 后,开始由最底层的 onTouchEvent()
方法来处理,如果一直不消费,则最后返回到Activity.OnTouchEvent()
。
ViewGroup 才有 onInterceptTouchEvent()
拦截方法。在分发过程中,中间任何一层 ViewGroup 都可以直接拦截,则不再往下分发,而是交由发生拦截操作的ViewGroup的 onTouchEvent()
来处理。
子 View 可调用 requestDisallowInterceptTouchEvent()
方法,来设置 disallowIntercept = true
,从而阻止父 ViewGroup 的 onInterceptTouchEvent()
拦截操作。
onTouchEvent()
由下往上冒泡时,当中间任何一层的 onTouchEvent
消费该事件,则不再往上传递,表示事件已处理。
如果 View 没有消费 ACTION_DOWN 事件,则之后的 ACTION_MOVE 等事件都不会再接收。
只要 View.onTouchEvent()
是可点击或可长按,则消费该事件。
onTouch()
优先于 onTouchEvent()
执行,上面流程图中省略,onTouch()
的位置在 onTouchEvent()
前面。当 onTouch()
返回 true
,则不执行 onTouchEvent()
,否则会执行 onTouchEvent()
。onTouch()
只有 View 设置了OnTouchListener
,且是 enable
的才执行该方法。
面试题
事件一定会经过 Activity 吗?
不是的。我们的程序界面的顶层 ViewGroup,也就是 DecorView 中注册了 Activity 这个 Callback,所以当程序的主界面接收到事件之后会先交给 Activity。 但是,如果是另外的控件树,如 Dialog、PopupWindow 等事件流是不会经过 Activity 的。只有自己界面的事件才会经 Activity。
Activity的分发方法中调用了 onUserInteraction()
方法,你能说说这个方法有什么作用吗?
这个方法在 Activity 接收到 ACTION_DOWN 事件的时候会被调用,本身是个空方法,需要开发者自己去重写。 通过官方的注释可以知道,这个方法会在我们以任意的方式开始与 Activity 进行交互的时候被调用。
比较常见的场景就是屏保:当我们一段时间没有操作会显示一张图片,当我们开始与 Activity 交互的时候可在这个方法中取消屏保;另外还有没有操作自动隐藏工具栏,可以在这个方法中让工具栏重新显示。
ViewGroup 是如何分发事件的?
ViewGroup 处理事件信息分为三个步骤:拦截、寻找子控件、派发事件。
事件分发中有一个重要的规则:一个触控点的一个事件序列只能给一个 View 处理,除非异常情况。所以如果 ViewGroup 消费了 ACTION_DOWN 事件,那么子 View 将无法收到任何事件。
ViewGroup 第一步会判读这个事件是否需要分发给子 View,如果是则调用 onInterceptTouchEvent()
方法判断是否要进行拦截。 第二步是如果这个事件是 ACTION_DOWN 事件,那么需要为他寻找一个消费此事件的子控件,如果找到则为他创建一个 TouchTarget。 第三步是派发事件,如果存在 TouchTarget,说明找到了消费事件序列的子 View,直接分发给他,如果没有则交给自己处理。
你前面讲到“一个触控点的一个事件序列只能给一个 View 处理,除非异常情况,这里有什么异常情况呢?如果发生异常情况该如何处理?
这里的异常情况主要有两点:
- 被 ViewGroup 拦截
- 出现界面跳转等其他情况
当事件流中断时,ViewGroup 会发送一个 ACTION_CANCEL 事件给到 View,此时需要做一些状态的恢复工作,如终止动画,恢复 View 大小等等。
事件类型有哪些?
- ACTION_DOWN:手指接触时产生的事件。
- ACTION_MOVE:手指在屏幕上滑动时产生此事件。
- ACTION_UP:手指抬起时产生此事件。
- ACTION_CANCEL:取消事件。
- ACTION_POINTER_DOWN:当已经有一个手指按下的情况下,另一个手指按下会产生该事件。
- ACTION_POINTER_UP:多个手指同时按下的情况下,抬起其中一个手指会产生该事件。
你知道 ViewGroup 是如何将多个手指产生的事件准确分发给不同的子 View 吗?
这个问题的关键在于 MotionEvent 以及 ViewGroup 内部的 TouchTarget。
每个 MotionEvent 中都包含了当前屏幕所有触控点的信息,他的内部用了一个数组来存储不同的触控 id 所对应的坐标数值。
当一个子 View 消费了 ACTION_DOWN 事件之后,ViewGroup 会为该 View 创建一个 TouchTarget,这个TouchTarget 就包含了该 View 的实例与触控 id。这里的触控 id 可以是多个,也就是一个 View 可接受多个触控点的事件序列。
当一个 MotionEvent 到来之时,ViewGroup 会将其中的触控点信息拆开,再分别发送给感兴趣的子 View。从而达到精准发送触控点信息的目的。
那 View 支持处理多指信息吗?
View 默认是不支持的。他在获取触控点信息的时候并没有传入触控点索引,也就是获取的是 MotionEvent 内部数组中的第一个触控点的信息。多指需要我们自己去重写方法支持他。
View 是如何处理触摸事件的?
首先,他会判断是否存在 onTouchListener()
,存在则会调用他的 onTouch()
方法来处理事件。如果该方法返回 true
那么就分发结束直接返回。而如果该监听器为 null
或者 onTouch()
方法返回了 false
,则会调用 onTouchEvent()
方法来处理事件。
onTouchEvent()
方法中支持了两种监听器:onClickListener ()
和 onLongClickListener()
。View 会根据不同的触摸情况来调用这两个监听器。同时进入到 onTouchEvent()
方法中,无论该 View 是否是 enable
,只要是 clickable
,他的分发方法都是返回 true
。
总结一下就是:先调用 onTouchListener()
,再调用 onClickListener()
和 onLongClickListener()
。
在实际中有运用到事件分发吗?
第一个需求是要设计一个按钮块,按下的时候会缩小高度变低同时变得半透明,放开的时候又会回弹。这个时候就可以在这个按钮的 onTouchEvent()
方法中判断事件类型:down 则开启按下动画,up 则开启释放动画。同时注意接收到 cancel 事件的时候要恢复状态。
第二个是滑动冲突。
解决滑动冲突的核心思路就是把滑动事件根据具体的情况分发给 ViewGroup 或者内部 View。主要的方法有外部拦截法和内部拦截法。
- 外部拦截法的思路就是在 ViewGroup 中判断滑动的情况,对符合自身滑动的事件进行拦截,对不符合的事件不拦截,给到内部 View。
- 内部拦截法的思路要求 ViewGroup 拦截除了 down 事件以外的所有事件,然后再内部 View 中判断滑动的情况,对符合自身滑动情况的时间设置禁止拦截标志,对不符合自身滑动情况的事件则取消标志让 ViewGroup 进行拦截。
那外部和内部拦截法该如何选择呢?
在一般的情况下,外部拦截法不需要对子 View 进行方法重写,比内部拦截法更加简单,推荐使用外部拦截法。
但如果需要在子 View 判断更多的触摸情况时,则使用内部拦截法可更加方法子 View 处理情况。
参考