文章目录
- 分发顺序
- Activity
- ViewGroup
- View
- 协作方法
- 整体流程
- 注意
- Activity事件分发
- ViewGroup事件分发
- View点击事件
- 总结
分发顺序
Activity->ViewGroup->View
Activity
- 分发事件:
Activity
通过dispatchTouchEvent
方法分发事件,首先尝试将事件传递给Window
及其根视图(DecorView
)。 - 消费事件:如果根视图及其子视图没有消费事件,
Activity
会调用自己的onTouchEvent
方法处理事件。因此,Activity
具备兜底消费事件的能力。
ViewGroup
- 分发事件:
ViewGroup
通过dispatchTouchEvent
方法将事件分发给子视图(或进一步嵌套的子视图)。 - 拦截事件:
ViewGroup
拥有onInterceptTouchEvent
方法,可以决定是否拦截事件。拦截事件后,事件不会再传递给子视图,而是直接由ViewGroup
自己处理。 - 消费事件:当
ViewGroup
需要处理自己捕获的事件时,最终会调用其onTouchEvent
方法来消费事件。因此,ViewGroup
可以在适当情况下选择消费事件。
View
- 消费事件:
View
只能消费事件,而没有分发和拦截事件的能力。当dispatchTouchEvent
将事件传递给View
时,View
只能选择在onTouchEvent
中处理和消费该事件,或者将事件交回父视图。
Activity
:负责整体的事件分发和兜底消费。ViewGroup
:负责在视图层级中分发事件,具备拦截和消费事件的灵活性。View
:仅具备事件消费能力。
协作方法
-
分发事件 (
dispatchTouchEvent
)dispatchTouchEvent(MotionEvent event)
方法是事件分发的入口。- 每当事件产生时(如点击、滑动),系统会将该事件封装成一个
MotionEvent
对象,并通过dispatchTouchEvent
方法传递给根视图(通常是Activity
中的DecorView
)。 - 在
dispatchTouchEvent
中,事件会根据层级逐层传递给子视图,直到找到可以处理事件的视图为止。 - 若
dispatchTouchEvent
返回true
,则事件处理停止;若返回false
,则事件会继续传递。
-
拦截事件 (
onInterceptTouchEvent
)onInterceptTouchEvent(MotionEvent event)
主要用于ViewGroup
及其子类。ViewGroup
可以选择是否拦截事件并防止它传递给子视图。- 如果
onInterceptTouchEvent
返回true
,则事件会直接交由ViewGroup
自己处理,子视图将无法接收到事件;如果返回false
,则事件会继续传递到子视图。 - 常见场景是滑动容器(如
ScrollView
)在检测到用户滑动手势时,会选择拦截触摸事件,使得事件不再传递给子视图。
-
处理事件 (
onTouchEvent
)onTouchEvent(MotionEvent event)
方法是实际处理事件的地方。- 视图可以在该方法中根据
MotionEvent
的类型(如ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
等)进行具体的操作(如点击处理、滑动等)。 - 如果
onTouchEvent
返回true
,表示视图已处理该事件;如果返回false
,则事件会继续向上层视图传递,直到被某个视图消费或到达根视图为止。
整体流程
- 事件从
Activity
的dispatchTouchEvent
开始。 - 事件传递给根视图(通常是一个
ViewGroup
),然后通过dispatchTouchEvent
传递到子视图。 - 在
ViewGroup
中调用onInterceptTouchEvent
判断是否拦截事件。 - 如果
onInterceptTouchEvent
返回false
,则事件继续向下传递;否则由ViewGroup
的onTouchEvent
处理。 - 最终,事件在目标视图的
onTouchEvent
中被消费。
注意
- 事件的消费:当某个视图返回
true
,表示事件被消费,后续事件(如ACTION_MOVE
、ACTION_UP
)会继续传递给该视图。 - 父视图与子视图的冲突:父视图可以通过拦截事件来管理事件的流向,避免子视图误处理事件。
requestDisallowInterceptTouchEvent(boolean disallowIntercept)
:子视图可以请求父视图不要拦截事件,适用于处理特殊的事件需求(如嵌套滑动)。
Activity事件分发
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
getWindow().superDispatchTouchEvent(ev)
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
Activity#dispatchTouchEvent() ->
PhoneWindow#superDispatchTouchEvent() ->
DecorView#superDispatchTouchEvent() ->
ViewGroup#dispatchTouchEvent()
getWindow()
返回当前Activity
的Window
对象(Window
对象的唯一实现类是PhoneWindow
类),调用其superDispatchTouchEvent
方法来进一步分发事件DecorView#superDispatchTouchEvent()
方法内部会将事件传递给根视图(一般是DecorView
),并由该视图将事件沿视图层次分发下去,此方法调用父类ViewGroup#dispatchTouchEvent()
ViewGroup
中dispatchTouchEvent
ViewGroup.dispatchTouchEvent
返回true
表示事件已经在ViewGroup
或其子视图中被消费,不再向上传递。ViewGroup.dispatchTouchEvent
返回false
表示事件未被处理,最终会由Activity
兜底处理。
onTouchEvent(ev)
return onTouchEvent(ev);
- 如果
superDispatchTouchEvent(ev)
返回false
,即所有的视图和组件都未处理该事件,dispatchTouchEvent
会将事件传递给Activity
自身的onTouchEvent
方法。 onTouchEvent
是Activity
处理事件的最后一步,通常用于处理默认的触摸行为。
ViewGroup事件分发
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 如果是 ACTION_DOWN 或者存在 mFirstTouchTarget(表示当前视图或子视图已经接收了一个触摸事件),则可以继续检查是否拦截。
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果拦截事件(disallowIntercept 为 false),调用onInterceptTouchEvent,该事件交给viewGroup处理
// 不拦截事件则设置intercepted为false,后续继续向下传递给子视图
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//...
}
-
ViewGroup.onInterceptTouchEvent()
返回false:不拦截(默认)
返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
// 从后往前遍历子视图
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// 检查子视图是否可以接收指针事件以及触摸点是否在其边界内
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue; // 跳过不接收事件的子视图
}
// 获取当前子视图的触摸目标
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 子视图已经在处理触摸事件,更新指针ID位
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 = x;
mLastTouchDownY = y;
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break; // 退出循环
}
}
-
遍历子视图:
- 首先,代码会遍历
ViewGroup
的所有子视图,从后向前(通常是为了优先处理最上层的视图),以确保能够找到可以接收触摸事件的视图。
- 首先,代码会遍历
-
判断是否能够接收点击事件:
- 判断子视图是否能够接收点击事件主要考虑两个条件:
- 动画状态:如果子视图正在播放动画,它可能不希望接收触摸事件。在这种情况下,该视图将被跳过。
- 触摸坐标:需要检查触摸事件的坐标是否落在子视图的区域内。通过计算触摸点与子视图边界的关系来判断。
- 判断子视图是否能够接收点击事件主要考虑两个条件:
-
传递触摸事件:
- 如果找到一个满足条件的子视图,该视图将接收触摸事件。此时,会调用
dispatchTransformedTouchEvent
方法,实际上是调用了子视图的dispatchTouchEvent
方法,将触摸事件传递给它进行处理。
- 如果找到一个满足条件的子视图,该视图将接收触摸事件。此时,会调用
如果子元素view返回了true,表示被子元素消耗了,那么此时就会跳出循环
View点击事件
View事件分发机制从dispatchTouchEvent()开始
public boolean dispatchTouchEvent(MotionEvent event) {
if ( (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener != null &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
// 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
// 1. (mViewFlags & ENABLED_MASK) == ENABLED
// 2. mOnTouchListener != null
// 3. mOnTouchListener.onTouch(this, event)
// 下面对这3个条件逐个分析
/**
* 条件1:(mViewFlags & ENABLED_MASK) == ENABLED
* 说明:
* 1. 该条件是判断当前点击的控件是否enable
* 2. 由于很多View默认enable,故该条件恒定为true(除非手动设置为false)
*/
/**
* 条件2:mOnTouchListener != null
* 说明:
* 1. mOnTouchListener变量在View.setOnTouchListener()里赋值
* 2. 即只要给控件注册了Touch事件,mOnTouchListener就一定被赋值(即不为空)
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
/**
* 条件3:mOnTouchListener.onTouch(this, event)
* 说明:
* 1. 即回调控件注册Touch事件时的onTouch();
* 2. 需手动复写设置,具体如下(以按钮Button为例)
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
// 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
// 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)
// onTouchEvent()源码分析 -> 分析1
}
});
/**
* 分析1:onTouchEvent()
*/
public boolean onTouchEvent(MotionEvent event) {
... // 仅展示关键代码
// 若该控件可点击,则进入switch判断中
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
// 根据当前事件类型进行判断处理
switch (event.getAction()) {
// a. 事件类型=抬起View(主要分析)
case MotionEvent.ACTION_UP:
performClick();
// ->>分析2
break;
// b. 事件类型=按下View
case MotionEvent.ACTION_DOWN:
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
// c. 事件类型=结束事件
case MotionEvent.ACTION_CANCEL:
refreshDrawableState();
removeTapCallback();
break;
// d. 事件类型=滑动View
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
// 若该控件可点击,就一定返回true
return true;
}
// 若该控件不可点击,就一定返回false
return false;
}
/**
* 分析2:performClick()
*/
public boolean performClick() {
if (mOnClickListener != null) {
// 只要通过setOnClickListener()为控件View注册1个点击事件
// 那么就会给mOnClickListener变量赋值(即不为空)
// 则会往下回调onClick() & performClick()返回true
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
总结
感谢您的阅读
如有错误烦请指正
参考:
- Android 事件分发机制详解(上)
- Android 事件分发机制详解(下)_montouchlistener-CSDN博客