Android 的事件分发机制主要包括以下几个步骤:
-
事件生成:用户在设备上进行触摸、滑动等操作时,系统会生成相应的事件,如触摸事件(
MotionEvent
)。 -
事件发送:生成的事件会被发送到当前活动(Activity)或视图(View)树的根节点。
-
事件分发:
-
Activity:首先,事件会被传递给活动的
dispatchTouchEvent()
方法。这个方法决定如何将事件进一步分发。 -
ViewGroup:如果当前活动包含
ViewGroup
(如LinearLayout
、RelativeLayout
等),dispatchTouchEvent()
会先调用ViewGroup
的onInterceptTouchEvent()
方法。如果返回true
,则ViewGroup
会处理事件;如果返回false
,则将事件传递给子视图。 -
View:对于普通的视图(View),会调用其
onTouchEvent()
方法来处理事件。
-
-
事件处理:
-
onTouchEvent()
:当视图接收到事件后,会根据事件的类型(如按下、移动、抬起等)在此方法中处理相应的逻辑。 -
事件处理过程可能涉及多个视图,尤其是在有嵌套的视图结构中。
-
-
事件消费:如果某个视图处理了事件(返回
true
),后续的视图将不会再接收到这个事件。如果没有视图消费事件,事件将向上传递,直到达到活动。 -
最终结果:处理完成后,结果可能会影响用户界面的状态或行为。
注意事项:
-
onInterceptTouchEvent()
:在ViewGroup
中使用,决定是否拦截子视图的事件。 -
事件的传递顺序:从上到下(Activity → ViewGroup → View),处理顺序是从下到上(View → ViewGroup → Activity)。
好的,针对你提到的几个基础认知点,以下是更深入的解析:
事件分发的由来:
由于 Android 中的视图是树形结构的,多个视图可能重叠并同时响应用户的触摸事件。
这种情况下,如何选择一个特定的视图来处理事件就变得尤为重要。事件分发机制的目的是在复杂的视图层次中,准确、高效地找到合适的视图来响应用户的交互。
事件的定义:
触摸事件是用户与设备交互时产生的信号,通常由一系列连续的触摸状态构成。
事件列从 ACTION_DOWN
开始(手指触摸屏幕),
中间可能有多个 ACTION_MOVE
事件(手指移动),
最后以 ACTION_UP
结束(手指离开屏幕)。
这种事件序列使得应用能够理解用户的意图,比如滑动或点击。
事件分发的本质:
事件分发的本质在于动态选择并传递触摸事件到合适的视图,同时根据用户的交互来决定事件的处理逻辑。以下是更深入的讲解:
-
决策过程:当触摸事件发生时,
Activity
首先接收事件,执行dispatchTouchEvent()
方法。在这个方法中,系统根据事件的类型和当前视图的状态(如是否可见、是否可点击)决定是否继续向下传递事件。 -
拦截机制:
ViewGroup
可以通过重写onInterceptTouchEvent()
方法来决定是否拦截事件。如果返回true
,则该ViewGroup
将直接处理事件,子视图将不再接收到此事件。这一机制允许ViewGroup
在需要时优先处理特定的触摸交互,比如滚动或拖动。 -
事件传递链:如果事件没有被拦截,它将继续向下传递到子视图。每个视图都有自己的
onTouchEvent()
方法,用于处理具体的事件。如果一个视图能处理该事件(如点击),它会返回true
,表示事件已被消费,后续视图将不会再接收到该事件。这种机制使得触摸事件的处理能够精准到具体的视图。 -
状态管理:视图的状态(如按下、抬起、移动等)影响事件的处理。视图在
onTouchEvent()
中根据当前状态执行不同的逻辑,如更新视觉反馈、触发动画或改变内部状态。这种动态响应使得用户体验更加流畅和自然。 -
事件队列与优化:Android 还利用事件队列来优化事件处理。通过合并和优化事件,系统能够减少不必要的处理,提高性能。这在复杂交互或高频触摸操作中尤为重要。
事件传递的对象:
事件在 Android 中主要在 Activity
、ViewGroup
和 View
之间传递。
Activity
作为应用的入口,首先接收事件;ViewGroup
可能根据需要拦截事件,而具体的 View
则负责执行实际的事件处理。这种多层次的传递机制保证了事件处理的灵活性和精确性。
事件分发顺序
-
Activity:当用户触摸屏幕时,事件首先被发送到当前的
Activity
。Activity
会调用其dispatchTouchEvent()
方法,决定事件的后续处理。 -
ViewGroup:如果
Activity
的dispatchTouchEvent()
方法未拦截事件,事件将传递到ViewGroup
。ViewGroup
会执行onInterceptTouchEvent()
方法,判断是否拦截该事件。如果返回true
,则事件会在ViewGroup
中处理;如果返回false
,事件会继续传递到子视图。 -
View:最终,事件将传递到具体的
View
,在View
中调用onTouchEvent()
方法来处理事件。视图可以根据事件的类型(如点击、滑动等)执行相应的逻辑。
事件分发过程中的方法
-
dispatchTouchEvent():
-
作用:负责分发触摸事件。
-
调用时刻:当
Activity
或ViewGroup
收到触摸事件时首先调用。它决定事件是否继续传递给子视图或直接处理。
-
-
onInterceptTouchEvent():
-
作用:用于判断
ViewGroup
是否拦截事件。 -
调用时刻:在
ViewGroup
的dispatchTouchEvent()
内部调用。通常用于处理复杂的触摸交互,比如滑动或拖动。
-
-
onTouchEvent():
-
作用:处理具体的触摸事件。
-
调用时刻:在
dispatchTouchEvent()
内部调用。用于执行视图的响应逻辑,比如状态更新、动画触发等。
-
Activity事件分发机制
事件分发机制,首先会将点击事件传递到Activity中,具体是执行 dispatchTouchEvent() 进行事件分发。
/**
* 源码分析:Activity.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 仅贴出核心代码
// ->>分析1
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
// 若getWindow().superDispatchTouchEvent(ev)的返回true
// 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
// 否则:继续往下调用Activity.onTouchEvent
}
// ->>分析3
return onTouchEvent(ev);
}
/**
* 分析1:getWindow().superDispatchTouchEvent(ev)
* 说明:
* a. getWindow() = 获取Window类的对象
* b. Window类是抽象类,其唯一实现类 = PhoneWindow类
* c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
*/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
// mDecor = 顶层View(DecorView)的实例对象
// ->> 分析2
}
/**
* 分析2:mDecor.superDispatchTouchEvent(event)
* 定义:属于顶层View(DecorView)
* 说明:
* a. DecorView类是PhoneWindow类的一个内部类
* b. DecorView继承自FrameLayout,是所有界面的父类
* c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
// 调用父类的方法 = ViewGroup的dispatchTouchEvent()
// 即将事件传递到ViewGroup去处理,详细请看后续章节分析的ViewGroup的事件分发机制
}
// 回到最初的分析2入口处
/**
* 分析3:Activity.onTouchEvent()
* 调用场景:当一个点击事件未被Activity下任何一个View接收/处理时,就会调用该方法
*/
public boolean onTouchEvent(MotionEvent event) {
// ->> 分析5
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
// 即 只有在点击事件在Window边界外才会返回true,一般情况都返回false,分析完毕
}
/**
* 分析4:mWindow.shouldCloseOnTouch(this, event)
* 作用:主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
*/
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
// 返回true:说明事件在边界外,即 消费事件
return true;
}
// 返回false:在边界内,即未消费(默认)
return false;
}
这段源码分析详细讲解了 Activity.dispatchTouchEvent()
的实现和事件分发的过程。以下是对每个分析点的进一步阐释:
分析1:dispatchTouchEvent()
-
核心逻辑:当触摸事件发生时,
Activity.dispatchTouchEvent()
首先调用getWindow().superDispatchTouchEvent(ev)
。如果返回true
,表示该事件已经被处理,事件传递过程结束;如果返回false
,则继续调用onTouchEvent(ev)
进行后续处理。
分析2:superDispatchTouchEvent()
-
窗口与视图:
getWindow()
获取Window
对象,通常是PhoneWindow
的实例。superDispatchTouchEvent(ev)
将事件传递给DecorView
,它是应用界面的顶层视图,负责接收和分发事件。 -
层级关系:
DecorView
继承自FrameLayout
,是所有界面的根视图,最终调用的是ViewGroup
的dispatchTouchEvent()
方法,进入视图分发的下一阶段。
分析3:onTouchEvent()
-
回调机制:当触摸事件没有被任何子视图处理时,
Activity
将调用onTouchEvent()
。这通常用于处理未消费的事件,例如判断用户点击是否在窗口边界之外。 -
返回值逻辑:
onTouchEvent()
的返回值主要依赖于mWindow.shouldCloseOnTouch(this, event)
,该方法判断点击是否在窗口外。
分析4:shouldCloseOnTouch()
-
边界判断:此方法的作用是判断用户点击是否在窗口的边界外,特别是在
ACTION_DOWN
事件时。如果点击发生在边界外且设置为关闭窗口,则返回true
,表示事件已被消费;否则,返回false
,表示事件未被消费。
ViewGroup事件分发机制
从上面Activity的事件分发机制可知,在 Activity.dispatchTouchEvent()
实现了将事件从Activity -> ViewGroup
的传递,ViewGroup的事件分发机制从 dispatchTouchEvent()
开始。
/**
* 源码分析:ViewGroup.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 仅贴出关键代码
...
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// 分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
// 判断值1-disallowIntercept:是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
// 判断值2-!onInterceptTouchEvent(ev) :对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false,即不拦截事件,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true,即拦截事件,从而跳出了该条件判断
// c. 关于onInterceptTouchEvent() ->>分析1
// 分析2
// 1. 通过for循环,遍历当前ViewGroup下的所有子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
// 2. 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 3. 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面章节介绍的View事件分发机制)
if (child.dispatchTouchEvent(ev)) {
// 调用子View的dispatchTouchEvent后是有返回值的
// 若该控件可点击,那么点击时dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
// 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
// 即该子View把ViewGroup的点击事件消费掉了
mMotionTarget = child;
return true;
}
}
}
}
}
}
...
return super.dispatchTouchEvent(ev);
// 若无任何View接收事件(如点击空白处)/ViewGroup本身拦截了事件(复写了onInterceptTouchEvent()返回true)
// 会调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
// 因此会执行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己处理该事件,事件不会往下传递
// 具体请参考View事件分发机制中的View.dispatchTouchEvent()
...
}
/**
* 分析1:ViewGroup.onInterceptTouchEvent()
* 作用:是否拦截事件
* 说明:
* a. 返回false:不拦截(默认)
* b. 返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 默认不拦截
return false;
}
// 回到调用原处
这段源码分析清楚地描述了 ViewGroup.dispatchTouchEvent()
方法的实现过程,以及如何判断和处理触摸事件。以下是对每个分析点的进一步解释:
分析1:dispatchTouchEvent()
-
事件拦截判断:在每次事件分发时,
ViewGroup
会首先调用onInterceptTouchEvent(ev)
来询问是否拦截事件。这个决策是基于两个条件:-
disallowIntercept
:如果为true
,表示不允许拦截。 -
!onInterceptTouchEvent(ev)
:如果返回false
,表示不拦截事件,继续执行事件传递逻辑。
-
事件传递过程
-
遍历子视图:如果未拦截,
ViewGroup
将通过for
循环遍历其所有子视图。 -
可见性检查:在循环中,首先检查子视图的可见性。只有可见或有动画效果的视图会继续处理。
-
命中检测:使用
getHitRect(frame)
获取子视图的矩形区域,判断用户点击的坐标是否在子视图内。如果点击在子视图内,将事件位置调整为相对于子视图的坐标,并调用该子视图的dispatchTouchEvent(ev)
方法处理事件。
分析2:子视图的事件处理
-
返回值逻辑:如果子视图处理了事件(返回
true
),ViewGroup.dispatchTouchEvent()
也会返回true
,表示事件被消费,事件不会继续向下传递。
分析3:无视图处理的情况
-
调用父类方法:如果没有任何子视图处理事件,或者
ViewGroup
自身拦截了事件,将调用其父类View
的dispatchTouchEvent()
方法。这可能导致View
自身处理事件,执行点击逻辑(如调用performClick()
)。
onInterceptTouchEvent()
-
默认行为:
onInterceptTouchEvent()
默认返回false
,即不拦截事件。开发者可以重写此方法以自定义拦截逻辑,返回true
时会停止事件的进一步传递。
Android事件分发传递到Acitivity后,总是先传递到 ViewGroup 、再传递到 View 。流程总结如下:(假设已经经过了Acitivity事件分发传递并传递到 ViewGroup )
View事件分发机制
从上面 ViewGroup 事件分发机制知道,View事件分发机制从 dispatchTouchEvent() 开始
/**
* 源码分析: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;
}
这段源码分析清晰地描述了 View.dispatchTouchEvent()
和 View.onTouchEvent()
方法的工作原理,以及它们如何处理触摸事件。以下是对每个关键点的进一步解析:
dispatchTouchEvent()
-
返回条件:
dispatchTouchEvent()
会在三个条件同时满足时返回true
,表示事件已被消费:-
条件1:
(mViewFlags & ENABLED_MASK) == ENABLED
,判断视图是否启用。一般情况下,这个条件为真,除非手动设置为禁用。 -
条件2:
mOnTouchListener != null
,确保视图注册了触摸事件监听器。 -
条件3:
mOnTouchListener.onTouch(this, event)
,调用监听器的onTouch()
方法并检查返回值。如果返回true
,事件被消费,方法结束;如果返回false
,将调用onTouchEvent(event)
继续处理。
-
onTouchEvent()
-
事件处理逻辑:
onTouchEvent()
负责处理实际的触摸事件,首先检查视图的可点击性。如果视图可点击,会进入switch
语句根据事件类型进行处理:-
ACTION_UP:抬起手指时调用
performClick()
,触发点击事件。 -
ACTION_DOWN:按下时可能设置延迟检查点击状态。
-
ACTION_CANCEL:处理取消事件,重置状态。
-
ACTION_MOVE:处理移动事件,检查是否超过触摸阈值。
-
performClick()
-
点击事件触发:
performClick()
方法会检查是否注册了点击事件监听器mOnClickListener
。如果存在,调用onClick()
方法执行点击逻辑,并返回true
表示点击事件已成功处理。
点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次 onTouch() ;
因为 onTouch() 返回了false,所以事件无被消费,会继续往下传递,即调用 View.onTouchEvent() ;
调用 View.onTouchEvent() 时,对于抬起View事件,在调用 performClick() 时,因为设置了点击事件,所以会回调 onClick() 。
点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次 onTouch() ;
因为 onTouch() 返回true,所以事件被消费,不会继续往下传递, View.dispatchTouchEvent() 直接返回true;
所以最终不会调用 View.onTouchEvent() ,也不会调用 onClick() 。
事件分发机制流程总结
从上表可以看到 Activity和 View都是没有事件拦截的,这是因为:
Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。
View作为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。
安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。