Android touch 事件的分发是 Android 工程师必备技能之一。关于事件分发主要有几个方向可以展开深入分析:
- touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService;
- WMS 是如何通过 ViewRootImpl 将事件传递到目标窗口;
- touch 事件到达 DecorView 后,是如何一步步传递到内部的子 View 中的。
其中与上层软件开发息息相关的就是第 3 条,也是本文的重点。
Touch事件
我们知道一次完整的Touch事件序列为:
ACTION_DOWN ----> ACTION_MOVE ----> ACTION_UP / ACTION_CANCEL(人为取消的情况)
而对于Touch事件的分发,不管是View还是ViewGroup都和一下的三个方法有关系:
- dispatchTouchEvent():事件分发
- onInterceptTouchEvent():事件拦截(只有ViewGroup才有该方法)
- onTouchEvent():事件消费
Touch事件相关方法
- 1、public boolean dispatchTouchEvent(MotionEvent ev):
事件分发方法,分发Event所调用 - 2、public boolean onInterceptTouchEvent(MotionEvent
ev):事件拦截方法,拦截Event所调用 - 3、public boolean onTouchEvent(MotionEvent
event):事件响应方法,处理Event所调用
拥有上述事件的类
- 1、Activity类(Activity及其各种继承子类)
dispatchTouchEvent()、onTouchEvent() - 2、ViewGroup类(LinearLayout、FrameLayout、ListView等…)
dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent() - 3、View类(Button、TextView等…)
dispatchTouchEvent()、onTouchEvent()
PS:需要特别注意一点就是ViewGroup中额外拥有onInterceptTouchEvent()方法,其他两个方法为这三种类所共同拥有。
方法的简单用途解析
我们可以发现这三个方法的返回值都为boolean类型,其实它们就是通过返回值来决定下一步的传递处理方向。
- 1、dispatchTouchEvent() ——用来分发事件所用
该方法会将根元素的事件自上而下依次分发到内层子元素中,直到被终止或者到达最里层元素,该方法也是采用一种隧道方式来分发。在其中会调用onInterceptTouchEvent()和onTouchEvent(),一般不会去重写。
返回false则不拦截继续往下分发,如果返回true则拦截住该事件不在向下层元素分发,在dispatchTouchEvent()方法中默认返回false。 - 2、onInterceptTouchEvent() ——用来拦截事件所用
该方法在ViewGroup源代码中实现就是返回false不拦截事件,Touch事件就会往下传递给其子View。
如果我们重写该方法并且将其返回true,该事件将会被拦截,并且被当前ViewGroup处理,调用该类的onTouchEvent()方法。 - 3、onTouchEvent() ——用来处理事件
返回true则表示该View能处理该事件,事件将终止向上传递(传递给其父View)
返回false表示不能处理,则把事件传递给其父View的onTouchEvent()方法来处理
当一个点击事件发生时,从Activity的事件分发开始(Activity.dispatchTouchEvent())
ViewGroup 事件分发示意图
整个touch事件的传递过程为: Activity.dispatchTouchEvent() -> PhoneWindow.superDispatchTouchEvent() -> DecorView.superDispatchTouchEvent() -> ViewGroup.dispatchTouchEvent() -> View.dispatchTouchEvent()
而消费过程则相反: View.onTouchEvent() -> ViewGroup.onTouchEvent() -> DecorView.onTouchEvent() -> Activity.onTouchEvent()
ViewGroup中包含多个子view时会将touch事件分配给包含在点击位置处的子view
ViewGroup和子view同时注册了监听器OnClickListener 监听事件由子view进行消费
在一次完整的touch事件(ACTION_DOWN -> ACTION_MOVE -> ACTION_UP)传递过程中,touch事件应该被
同一个view进行消费,全部接受或者全部拒绝
只要接受ACTION_DOWN事件就意味着接受所有的事件,拒绝接受ACTION_DOWN 则不会接受后续的内容
如果当前正在处理的touch事件被上层的view拦截,会接收到一个ACTION_CANCEL,后续事件不会再传递过来
父容器拿到触摸事件,默认不拦截(onInterceptTouchEvent() return false),分发给孩子(dispatchTransformedTouchEvent()),
看孩子是否消费, 孩子不消费,事件又传回父容器(onTouchEvent()),看父容器是否消费。
- 父容器拿到触摸事件,如果ACTION_DWON事件传递下去没有孩子消费,那么后续的事件就不会传了。(没有消费mFirstTouchTarget == null)
- 父容器拿到触摸事件,默认不拦截,分发给孩子,看孩子是否消费,孩子消费,事件传递结束。
- 父容器拿到触摸事件,拦截,事件不会分发给孩子,交给自己是否消费(调用父容器自己的onTouchEvent)
父容器拦截touch事件要分为两种:
- 1.拦截ACTION_DOWN 直接导致mFirstTouchTarget为null 那么直接调用ViewGroup的父类即View类中的dispatchTouchEvent()
方法即dispatchTouchEvent() -> onTouch() ->
onTouchEvent()此时会将ViewGroup当做一个普通的View进行处理 - 2.拦截ACTION_DOWN之后的ACTION_MOVE ACTION_UP事件如果拦截这些事件中的一个事件会将该事件转换成一个
ACTION_CANCEL给消费了这个事件的子view 因此本次的touch事件是不会
传递给拦截了touch事件的viewgroup的而是
当下次touch事件到来时才会传递给该viewgroup的onTouchEvent()方法来处理并且会将mFirstTouchTarget置为null
当下次
touch事件到来时由于mFirstTouchTatget为Null会直接调用自己的onTouchEvent()方法
而不会在传递个子view了
另外还有就是关于setOnTouchListener与 onTouchEvent的关系:
setOnTouchListener的onTouch方法和onTouchEvent()方法都会在我们View的dispatchTouchEvent()方法里面执行
不过onTouch()方法会优先于我们的onTouchEvent()方法而且如果OnTouchListener不为空直接执行onTouch()方法
并且直接返回true就不会在调用onTouchEvent()方法了
- 先后关系:onTouch先于onTouchEvent执行
- 如果onTouch返回true,表示消费,onTouchEvent就不会执行。
View 的事件分发示意图
从View的touch事件传递流程得出以下几点:
- 1.OnTouchListener()的onTouch()方法是优先于View的OnTouchEvent()方法执行的如果OnTouchListener的onTouch()方法
返回了true表示消费了touch事件那么后续View的onTouchEvent()方法也就不会再执行了那么View的onClick()
onLongClick() 等方法也就不会再接着执行了(onClick()
onLongClick()等方法都是在onTouchEvnet()方法中进行执行的) - 2.如果View是未激活的即处于DISABLED状态但是是可点击的(CLICKABLE LONG_CLICKABLE CONTEXT_CLICKALE)那么view也会 消费掉touch事件但是不会响应OnClickListener的onClick()方法
onLongClickListener的onLongCLick()方法等 - 3.只要是View可点击的并且处于ENABLED状态那么就一定返回true即一定会消费touch事件
- 4.在View的onTouchEvent()方法中处理我们常见的点击事件如:ACTION_DOWN 中处理长点击onLongClick() ACTION_UP中处理点击onClick()等
- 5.onTouch() onTouchEvent()中事件是否被消费了由方法的返回值来决定 而不是由我们是否在方法中使用了 touch事件MotionEvent来决定的
- 6.View的事件调度顺序是dispatchTouchEvent() -> onTouchListener() -> onTouchEvent() -> onLongCLick() -> onClick()
- 7.View是没有onIterceptTouchEvent()即没有拦截touch事件的方法的,ViewGroup才有
- 8.View的onTouchEvent()方法中只要view是clickable(CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE)的那么 就会消费这次touch事件(从ACTION_DOWN 到 ACTION_UP结束)
在安卓中有些控件默认是clickable的比如Button checkbox 等 而TextView LinearLayout等是non
clickable的
onTouch和onTouchEvent有什么区别,又该如何使用?
View中dispatchTouchEvent方法的源码:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
onTouch和onTouchEvent执行顺序
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;
}
- 从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
- 另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。
OnClick OnLongClick等对外的监听是在哪里处理的?
OnClick事件是先ACTION_DOWN之后再ACTION_UP,所以必定要在onTouchEvent()处理。同理,OnLongClick是在保持ACTION_DOWN一段时间后发生,因此也要在onTouchEvent()中处理。看看源码,发现果然是在这里:
//以下源码均为忽略了不想关部分,只保留了重点
public boolean onTouchEvent(MotionEvent event) {
//...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 处理click
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// a short period in case this is a scroll.
if (isInScrollingContainer) {
//...
} else {
// 处理longclick
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
//...
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
//...
break;
}
return true;
}
return false;
}
执行onClick
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
执行OnLongClick
// 处理longclick
setPressed(true, x, y);
checkForLongClick(0, x, y);
================================================
private void checkForLongClick(long delay, float x, float y, int classification) {
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();
mPendingCheckForLongPress.setClassification(classification);
postDelayed(mPendingCheckForLongPress, delay);
}
}
===================================
private final class CheckForLongPress implements Runnable {
@Override
public void run() {
if ((mOriginalPressedState == isPressed()) && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
recordGestureClassification(mClassification);
// 执行onLongClick
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}
}
====================================
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}
====================
private boolean performLongClickInternal(float x, float y) {
boolean handled = false;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null) {
// 执行onLongClick
handled = li.mOnLongClickListener.onLongClick(View.this);
}
return handled;
}
何时 触发CANCEL 事件
看ViewGroup的dispatchTouchEvent方法
上图红框中表明已经有子 View 捕获了 touch 事件,但是蓝色框中的 intercepted boolean 变量又是 true。这种情况下,事件主导权会重新回到父视图 ViewGroup 中,并传递给子 View 的分发事件中传入一个 cancelChild == true。看一下 dispatchTransformedTouchEvent 方法的部分源码如下:
因为之前传入参数 cancel 为 true,并且 child 不为 null,最终这个事件会被包装为一个 ACTION_CANCEL 事件传给 child。
什么情况下会触发这段逻辑呢?
总结一下就是:当父视图的 onInterceptTouchEvent 先返回 false,然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件),关键步骤就是在接下来的 MOVE 的过程中,父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。
实际上有个很经典的例子可以用来演示这种情况:
当在 Scrollview 中添加自定义 View 时,ScrollView 默认在 DOWN 事件中并不会进行拦截,事件会被传递给 ScrollView 内的子控件。只有当手指进行滑动并到达一定的距离之后,onInterceptTouchEvent 方法返回 true,并触发 ScrollView 的滚动效果。当 ScrollView 进行滚动的瞬间,内部的子 View 会接收到一个 CANCEL 事件,并丢失touch焦点。
比如以下代码:
CaptureTouchView 是一个自定义的 View,其源码如下:
CaptureTouchView 的 onTouchEvent 返回 true,表示它会将接收到的 touch 事件进行捕获消费。上述代码执行后,当手指点击屏幕时 DOWN 事件会被传递给 CaptureTouchView,手指滑动屏幕将 ScrollView 上下滚动,刚开始 MOVE 事件还是由 CaptureTouchView 来消费处理,但是当 ScrollView 开始滚动时,CaptureTouchView 会接收一个 CANCEL 事件,并不再接收后续的 touch 事件。具体打印 log 如下:
因此,我们平时自定义View时,尤其是有可能被ScrollView或者ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则有可能引起UI显示异常。
参考文章
Android事件分发机制学习笔记面试:讲讲 Android 的事件分发机制Android事件分发机制详解:史上最全面、最易懂