1. View
的事件分发机制
当点击屏幕时,就会产生点击事件,这个事件被封装成了一个类:MotionEvent
。而当这个MotionEvent
产生后,系统就会将这个MotionEvent
传递给View
的层级,MotionEvent
在View
中的层级传递过程就是点击事件分发。
MotionEvent
对象产生一系列事件,它有四种状态:
MotionEvent.ACTION_DOWN
:手指按下屏幕的瞬间(一切事件的开始)MotionEvent.ACTION_MOVE
:手指在屏幕上移动MotionEvent.ACTION_UP
:手指离开屏幕瞬间MotionEvent.ACTION_CANCEL
:取消手势,一般由程序产生,不会由用户产生
Android
中的行为onClick
、onLongClick
、onScroll
、onFling
等等,都是由许多个Touch
事件构成的(一个ACTION_DOWN
, n
个ACTION_MOVE
,1
个ACTION_UP
)。
motion [ˈmoʊʃn] 动作;移动;手势;请求;意向;议案
Android
的UI
界面由Activity
、ViewGroup
、View
及其派生类组成,事件也就在Activity
、ViewGroup
、View
中进行传递。
事件分发过程由dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
协作完成:
dispatchTouchEvent
:分发事件,当点击事件能够传递给当前View
时,该方法就会被调用onInterceptTouchEvent
:事件拦截,只存在于ViewGroup
中,普通的View
没有此方法。在ViewGroup
的dispatchTouchEvent
内部调用onTouchEvent
:处理点击事件,在dispatchTouchEvent
中调用
dispatch [dɪˈspætʃ] 派遣,发送;迅速处理,快速办妥;杀死,处决 intercept [ˌɪntərˈsept] 拦截;截断;窃听
当屏幕被触摸,首先会通过硬件产生触摸事件传入内核,然后走到Framework
层,最后经过一系列事件处理到达ViewRootImpl.processPointerEvent(...)
方法,而 ViewRootImpl.processPointerEvent(...)
方法中的mView
就是DecorView
。
当点击事件传递到当前的Activity
时,具体的事件处理工作都是交由Activity
中的PhoneWindow
完成的,然后PhoneWindow
再把事件处理工作交给DecorView
(DecorView
一般就是当前界面的底层容器,即setContentView(...)
所设置的View
的父容器),之后再由DecorView
将事件处理工作交给ViewGroup
。
首先来看 ViewRootImpl.processPointerEvent(...)
方法:
// /frameworks/base/core/java/android/view/ViewRootImpl.java
View mView;
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
boolean handled = mView.dispatchPointerEvent(event); // 1
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
mUnbufferedInputDispatch = true;
if (mConsumeBatchedInputScheduled) {
scheduleConsumeBatchedInputImmediately();
}
}
return handled ? FINISH_HANDLED : FORWARD;
}
注释 1
处调用了 View.dispatchPointerEvent(...)
方法,以下是相关源码:
// /frameworks/base/core/java/android/view/View.java
@UnsupportedAppUsage
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event); // 1
} else {
return dispatchGenericMotionEvent(event);
}
}
以下是 ViewGroup.dispatchTouchEvent(...)
的源代码:
// /frameworks/base/core/java/android/view/ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // 1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 2
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;
}
...
}
...
return handled;
}
首先会判断事件是否是为 ACTION_DOWN
事件,如果是,则进行初始化。这里为什么要进行初始化呢?这是因为一个完整的事件序列是以 DOWN
开始,以 UP
结束的。所以,如果是 DOWN
事件,说明这是一个新的事件序列,故而需要初始化之前的状态。在注释 1
处,如果条件满足,则执行下面的代码,mFirstTouchTarget
的意义是,当前 ViewGroup
是否拦截了此事件,如果拦截了,mFirstTouchTarget == null
,如果没有拦截则交给子 View
来处理,mFirstTouchTarget != null
。
假设当前的 ViewGroup
拦截了此事件,mFirstTouchTarget == null
为 false
,如果这时触发 ACTION_DOWN
事件,则会执行 onInterceptTouchEvent(ev)
方法;如果触发的是 ACTION_MOVE
、ACTION_UP
事件,则不再执行 onInterceptTouchEvent(ev)
,而是直接设置 intercepted = true
,此后的一个时间序列均由这个 ViewGroup
处理。
在注释 2
处出现了 FLAG_DISALLOW_INTERCEPT
标志位,它主要是禁止 ViewGroup
拦截除了 DOWN
之外的事件,一般通过子 View.requestDisallowInterceptTouchEvent(...)
来设置。
所以,总结一下就是,当 ViewGroup
要拦截事件的时候,那么后续的事件序列都交给它处理,而不用再调用 onInterceptTouchEvent(...)
方法了。所以 onInterceptTouchEvent(...)
方法并不是每次事件都会调用的。
以下是 ViewGroup.onInterceptTouchEvent(...)
方法:
// /frameworks/base/core/java/android/view/ViewGroup.java
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;
}
如果想要 ViewGroup
拦截事件,那么应该在自定义的 ViewGroup
中重写这个方法,接下来查看 ViewGroup.dispatchTouchEvent(...)
方法剩下的部分源码:
// /frameworks/base/core/java/android/view/ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) { // 1
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 (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) { // 2
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)) { // 3
// 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);
}
...
}
注释 1
处是 for
循环,首先遍历 ViewGroup
的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到点击事件,则交由子元素来处理。需要注意的是这个 for
循环是倒序便利的,即从最上层的子 View
开始往内层遍历。接着看注释 2
处的代码,其意思是判断触摸点的卫士是否在子 View
的范围或者子 View
是否在播放动画,如果均不符合则执行 continue
语句,表示这个子 View
不符合条件,开始便利下一个子 View
。接下来查看注释 3
处的 ViewGroup.dispatchTransformedTouchEvent(...)
方法做了什么,代码如下所示:
// /frameworks/base/core/java/android/view/ViewGroup.java
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;
}
...
}
如果有子 View
,则调用子 View.dispatchTouchEvent(...)
方法,如果没有子 View
,则调用 super.dispatchTouchEvent(...)
方法。ViewGroup
是继承自 View
的,以下是 View.dispatchTouchEvent(...)
方法:
// /frameworks/base/core/java/android/view/View.java
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)) { // 1
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
当 li.mOnTouchListener.onTouch(...)
返回 true
,表示该事件被消费,就不会执行 View.onTouch(...)
方法,否则就会执行该 View.onTouch(...)
方法。可以看出 li.mOnTouchListener.onTouch(...)
方法的优先级要高于 View.onTouchEvent(...)
方法。下面接着看 View.onTouchEvent(...)
方法:
// /frameworks/base/core/java/android/view/View.java
public boolean onTouchEvent(MotionEvent event) {
. ...
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 1
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
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 (!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();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
...
}
return true;
}
return false;
}
从注释 1
处可以知道,只要 View
的 CLICKABLE
和 LONG_CLICKABLE
有一个为 true
,那么 View.onTouch(...)
就会返回 true
消耗这个事件。CLICKABLE
和 LONG_CLICKABLE
代表 View
可以被点击和长按点击,可以通过 View.setClickable(...)
和 View.setLongClickable(...)
方法来设置,也可以通过 View.setOnClickListener(...)
和 View.setOnLongClickListener(...)
方法来设置,它们会自动将 View
设置为 CLICKABLDE
和 LONG_CLICKABLE
。接着在 ACTION_UP
事件中会调用 View.performClickInternal()
方法,源码如下所示:
// /frameworks/base/core/java/android/view/View.java
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) { // 1
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
从上面的代码注释 1
处可以看出,如果 View
设置了点击事件 OnClickListener
,那么它的 onClick
方法就会被执行。
ViewRootImpl.DecorView.dispatchPointerEvent
- > ViewRootImpl.DecorView.dispatchTouchEvent
-> Activity.dispatchTouchEvent()
-> PhoneWindow.superDispatchTouchEvent
-> DecorView.superDispatchTouchEvent
-> ViewGroup.dispatchTouchEvent()
。 如果ViewGroup.dispatchTouchEvent()
返回false
则调用onTouchEvent
方法,true
表示事件被消费掉了。
但是为什么DecorView
就走了两次?—— 主要原因就是解耦
ViewRootImpl
并不知道有Activity
的存在,它只是持有DecorView
,所以传给了DecorView
;DecorView
知道Activity
的存在,所以传给了Activity
;Activity
不知道DecorView
的存在,它只是持有PhoneWindow
,所以就形成了这样一段调用链;
Android
事件响应机制是先分发(先由外部的View
接收,然后依次传递给其内层的最小View
)再处理(从最小View
单元(事件源)开始依次向外层传递)的形式实现的。其复杂性表现在:可以控制每层事件是否继续传递(分发和拦截协同实现),以及事件的具体消费(事件分发也具有事件消费能力)。
Activity
只有dispatchTouchEvent
和onTouchEvent
方法。以下是Activity
的dispatchTouchEvent
方法和onTouchEvent
方法:
public class Activity extends ContextThemeWrapper implements Window.Callback, KeyEvent.Callback {
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
}
以下是View
的dispatchTouchEvent
方法和onTouchEvent
方法:
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
...
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
public boolean onTouchEvent(MotionEvent event) { }
}
以下是ViewGroup
的dispatchTouchEvent
方法和onInterceptTouchEvent
方法:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public boolean dispatchTouchEvent(MotionEvent ev) {
intercepted = onInterceptTouchEvent(ev);
}
public boolean onInterceptTouchEvent(MotionEvent ev) { }
}
View
没有onInterceptTouchEvent
方法,一旦有点击事件传递給它,它就会处理。
说明:
Activity
的dispatchTouchEvent
只有return super.dispatchTouchEvent(ev)
才往下走,返回true
或者false
事件就被消费了(终止传递);- 如果事件不被中断,整个事件流向就是一个
U
型图。 如果没有对控件里面的方法进行重写或更改返回值,而直接用super
调用父类的默认实现,那么整个事件流向应该是从Activity -> ViewGroup -> View
从上往下调用dispatchTouchEvent
方法,一直到叶子节点(View
)的时候,再由View -> ViewGroup -> Activity
从下往上调用onTouchEvent
方法; - 对于
dispatchTouchEvent
、onTouchEvent
、onInterceptTouchEvent
,
ViewGroup
和View
的这些方法的默认实现就是会让整个事件按照U
型完整走完,所以return super.xxxxxx()
就会让事件依照U
型的方向的完整走完整个事件流动路径,中间不做任何改动,不回溯、不终止,每个环节都走到; dispatchTouchEvent
和onTouchEvent
一旦return true
,事件就停止传递了,没有谁能再收到这个事件;return false
的时候,事件都回传给父控件的onTouchEvent
处理;- 对于
dispatchTouchEvent
返回false
的含义应该是:事件停止往子View
传递和分发,同时开始往父控件回溯(父控件的onTouchEvent
开始从下往上回传直到某个onTouchEvent return true
), 事件分发机制就像递归,return false
的意义就是递归停止然后开始回溯; - 对于
onTouchEvent return false
就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动; - 对于
onInterceptTouchEvent
,intercept
的意思就拦截,每个ViewGroup
每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理),如果要自己处理那就在onInterceptTouchEvent
方法中return true
就会交给自己的onTouchEvent
的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View
也需要这个事件,所以onInterceptTouchEvent
拦截器return super.onInterceptTouchEvent()
和return false
是一样的,是不会拦截的,事件会继续往子View
的dispatchTouchEvent
传递; ViewGroup
的dispatchTouchEvent
,return true
是终结传递,return false
是回溯到父View
的onTouchEvent
,然后ViewGroup
怎样通过dispatchTouchEvent
方法能把事件分发到自己的onTouchEvent
处理呢,return true
和false
都不行,那么只能通过Interceptor
把事件拦截下来给自己的onTouchEvent
,所以ViewGroup dispatchTouchEvent
方法的super
默认实现就是去调用onInterceptTouchEvent
;- 对于
View
的dispatchTouchEvent return super.dispatchTouchEvent()
的时候呢事件会传到哪里呢,很遗憾View
没有拦截器。但是同样的道理return true
是终结,return false
是回溯会父类的onTouchEvent
,怎样把事件分发给自己的onTouchEvent
处理呢,那只能return super.dispatchTouchEvent
,View
类的dispatchTouchEvent
方法默认实现就是能帮你调用View
自己的onTouchEvent
方法的;
总结:
- 对于
dispatchTouchEvent
和onTouchEvent
,return true
是终结事件传递,retrun false
是回溯到父View
的onTouchEvent
方法; ViewGroup
想把自己分发给自己的onTouchEvent
,需要拦截器onInterceptTouchEvent
方法return true
把事件拦截下来;ViewGroup
的拦截器onInterceptTouchEvent
默认是不拦截的,所以return super.onInterceptTouchEvent = return false
;View
没有拦截器,为了让View
可以把事件分发给自己的onTouchEvent
,View
的dispatchTouchEvent
默认实现(super
)就是把事件分发给自己的onTouchEvent
;
2 验证View
事件分发机制
以下是MyView
的代码,继承View
:
class MyView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
Log.e("CAH", "MyView: dispatchTouchEvent")
return super.dispatchTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("CAH", "MyView: onTouchEvent")
return super.onTouchEvent(event)
}
}
MyViewGroup01
和MyViewGroup02
是一样的代码,这里以MyViewGroup01
为例,继承ViewGroup
:
class MyViewGroup01 @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e("CAH", "MyViewGroup01: dispatchTouchEvent")
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Log.e("CAH", "MyViewGroup01: onInterceptTouchEvent")
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.e("CAH", "MyViewGroup01: onTouchEvent")
return super.onTouchEvent(event)
}
}
MyView
和MyViewGroup
布局文件:
<com.example.kotlintest.MyViewGroup02 xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/holo_green_light">
<com.example.kotlintest.MyViewGroup01
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_purple">
<com.example.kotlintest.MyView
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@android:color/holo_red_light" />
</com.example.kotlintest.MyViewGroup01>
</com.example.kotlintest.MyViewGroup02>
运行:
点击MyView
(红色部分):先接收事件的是父容器(MyViewGroup02
),继续分发,而事件的分发过程分为两个步骤:分发过程和处理过程。其正常的分发结果为:
// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent
// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent
2.1 dispatchTouchEvent
(分发事件)
如果在MyViewGroup01
的dispatchTouchEvent
方法中返回true
,表示需要在MyViewGroup01
消费了整个事件,即不会再分发,也不会再处理。dispatchTouchEvent
方法中返回true
的打印信息:
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
如果在MyViewGroup01
的dispatchTouchEvent
方法中返回false
,表示在MyViewGroup01
点击事件在本层不再继续进行分发,并交由上层控件的onTouchEvent
方法进行消费。dispatchTouchEvent
方法中返回false
的打印信息:
// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// 处理过程
// CAH: MyViewGroup02: onTouchEvent
2.2 onInterceptTouchEvent
(拦截事件)
如果在MyViewGroup01
的onInterceptTouchEvent
方法中返回true
,表示需要在MyViewGroup01
拦截这个点击事件,不再继续往下分发,即MyView
不再执行dispatchTouchEvent
方法。但是只是分发结束了而已,接着开始处理事件。下面是onInterceptTouchEvent
方法中返回true
的打印信息:
// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// 处理过程
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent
如果在MyViewGroup01
的onInterceptTouchEvent
方法中返回false
,表示需要在MyViewGroup01
不会拦截这个点击事件,继续往下分发。下面是onInterceptTouchEvent
方法中返回false
的打印信息:
// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent
// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent
2.3 onTouchEvent
(消费事件)
如果MyViewGroup01
的onTouchEvent
方法中返回true
,表示MyViewGroup01
可以将该事件直接消费掉了,即分发结束后,处理事件的时候,直接处理到MyViewGroup01
就可以结束了。下面是onTouchEvent
方法中返回true
的打印信息:
// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent
// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent
如果MyViewGroup01
的onTouchEvent
方法中返回false
,表示MyViewGroup01
不可以将该事件直接消费掉,即事件继续往上处理。下面是onTouchEvent
方法中返回false
的打印信息:
// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent
// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent
3 对于View.onTouchEvent
,OnClickListerner.onClick
和OnTouchListener.onTouch
的优先级
View.dispatchTouchEvent
-> OnTouchListener.onTouch
-> View.onTouchEvent
-> View.performClick
-> OnClickListener.onClick
以下是View
的源码:
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
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;
}
}
return result;
}
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!post(mPerformClick)) {
performClick();
}
break;
}
}
public boolean performClick() {
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
static class ListenerInfo {
private OnTouchListener mOnTouchListener;
public OnClickListener mOnClickListener;
}
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
public interface OnClickListener {
void onClick(View v);
}
}
onTouch
方法是View
的OnTouchListener
接口中定义的方法;onClick
方法是View
的OnClickListener
接口中定义的方法;
- 在
View.dispatchTouchEvent
方法中,OnTouchListener.onTouch
方法优先级比View.onTouchEvent
方法的优先级高,会先触发; - 如果
OnTouchListener.onTouch
方法返回false
会接着触发View.onTouchEvent
方法;返回true
,则View.onTouchEvent
方法不会被调用; OnClickListerner.onClick
方法是在View.onTouchEvent
的MotionEvent.ACTION_UP
事件通过View.performClick
方法触发的;
因此,OnTouchListener.onTouch
方法如果返回true
,则不会执行View.onTouchEvent
方法,也就更不会执行OnClickListener.onClick
方法,如果返回false
,则两个都会执行。
4 事件序列
事件序列:同一个事件序列是指从手指触摸屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中产生的一系列事件,这个事件以DOWN
事件开始,中间含有不定数量的MOVE
事件,最终以UP
事件结束
在ViewGroup.dispatchTouchEvent
中消费ACTION_DOWN
事件,ACTION_UP
事件是怎么传递?
正常情况下,一个事件序列只能被一个View
拦截且消耗,因为一旦某个View
拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且它的onInterceptTouchEvent
不会再被调用。 因此同一个事件序列中的事件不能分别由两个View
同时处理,但是通过特殊的手段可以做到,比如一个View
将本该字自己理的事件通过onTouchEvent
强行传递给其他View
处理。
Activity.dispatchTouchEvent()
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent()
-> view1.onTouchEvent()
-> ViewGroup1.onTouchEvent()
-> Activity.dispatchTouchEvent()
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onTouchEvent()
如果Activity
、ViewGroup
和View
都不消费ACTION_DOWN
,那么ACTION_UP
事件是怎么传递的?
某个View
一旦开始处理事件,如果它不消耗ACTION_DOWN
事件,那么同一时间序列中的其它事件也不会交给它来处理,事件会重新交给它的父元素去处理,即父元素的onTouchEvent
会被调用。 如果View
不消耗除ACTION_DOWN
以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent
并不会被调用,并且当前View
可持续收到后续的事件,最终这些消失的点击事件会传递给Activity
处理。
ACTION_DOWN
:Activity.dispatchTouchEvent()
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent()
-> view1.onTouchEvent()
-> ViewGroup1.onTouchEvent()
-> Activity.onTouchEvent()
;
ACTION_MOVE
:Activity.dispatchTouchEvent()
-> Activity.onTouchEvent()
-> 消费
ACTION_CANCEL
什么时候触发?
- 如果在父
View
中拦截ACTION_UP
或ACTION_MOVE
,在父视图拦截消息的瞬间,就指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL
事件; - 如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现
ACTION_CANCEL
;
同时对父View
和子View
设置点击方法,优先响应哪个?
优先响应子View
, 如果先响应父View
,那么子View
将永远无法响应。 父View
要优先响应事件,必须先调用onInterceptTouchEven
对事件进行拦截,那么事件不会再往下传递,直接交给父View
的onTouchEvent
处理。
以下是ViewGroup
的dispatchTouchEvent()
源码:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN) { // 1.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 2
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 2.1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 2.2
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
// 3
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
事件,如果是,则进行初始化,这事因为一个完整的事件序列是以DOWN
开始,以UP
结束的。
FLAG_DISALLOW_INTERCEPT
标志位,它主要是禁止ViewGroup
拦截除了DOWN
之外的事件,一般通过子View
的requestDisallowInterceptTouchEvent
来设置。
requestDisallowInterceptTouchEvent
的调用时机或者点击事件被拦截,但是想传到下面的View
,如何操作?
重写子类的requestDisallowInterceptTouchEvent()
方法,返回true
就不会执行父类的onInterceptTouchEvent()
,可将点击事件传到下面的View,
剥夺了父View
对除了ACTION_DOWN
以外的事件的处理权。
当ViewGroup
要拦截事件的时候,那么后续的事件都交给它处理,而不用调用onInterceptTouchEvent
方法了。所以onInterceptTouchEvent
方法并不是每次事件都会调用。
参考
https://blog.csdn.net/qq_32534441/article/details/103634329
https://www.jianshu.com/p/e99b5e8bd67b
Android | 理解 ViewRootImpl