tips:文章有点长,但是耐心看完一定有收获,部分内容为自己总结,部分内容 参考了其他人的博客,站在了巨人的肩膀上。图文丰富、详细解读。
一、基础概念
事件是什么
事件是指用户与应用程序交互时产生的动作或操作,例如点击屏幕、滑动手指、按下按键等。在安卓中,事件可以分为多种类型,包括触摸事件、按键事件、手势事件等。
四个基本动作:
MotionEvent.ACTION_DOWN
:表示用户按下触摸屏幕或点击某个 View 的动作。MotionEvent.ACTION_UP
:表示用户抬起触摸屏幕或取消点击某个 View 的动作。MotionEvent.ACTION_MOVE
:表示用户在触摸屏幕上滑动手指的动作。MotionEvent.ACTION_CANCEL
:表示事件被取消,通常由于某些非人为的原因,导致当前事件序列的中断。
事件通常是由这四个基本动作组合而成的,但并不限于这四个动作。事件的组成可以更为复杂,可以包含更多的动作和状态。这四个动作是触摸事件的基础,但触摸事件还可能包含其他动作,例如双击、长按、双指缩放等。
事件序列:从手指按下屏幕开始,到手指离开屏幕所产生的一系列事件。
事件分发机制是什么
安卓事件分发机制是指在安卓系统中,如何处理和传递用户交互事件的一套机制
本质:当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。
事件分发机制存在的意义
为什么会有事件分发机制?
简单讲:因为点击事件可能会发生冲突,所以用这个解决。
详细讲:在安卓的布局结构中,它是竖形结构,导致有些布局会重叠在一起,当我们用手指点击的地方在很多个布局范围之内时,应该让哪个View来响应呢?
这就是存在的意义:是为了确保用户在应用程序界面上的交互能够被正确捕获、传递和处理,从而实现用户与应用程序的有效交互。
事件在哪些对象之间进行传递?传递顺序?
Activity、ViewGroup、View。
事件传递的顺序:Activity
-> ViewGroup
-> View
Android
的UI
界面由Activity
、ViewGroup
、View
及其派生类组成
- Activity:Activity 是安卓应用程序中的一个重要组件,通常负责管理用户界面和处理用户交互事件。事件首先被传递给当前处于活动状态的 Activity。
- ViewGroup:ViewGroup 是一种特殊的 View,用于包含其他 View。例如,LinearLayout、RelativeLayout 等布局容器都是 ViewGroup 的子类。如果事件发生在某个 ViewGroup 内部,那么事件将首先被传递给该 ViewGroup。
- View:View 是安卓界面的基本构建块,是所有UI组件的基类。用于显示用户界面上的各种元素。当事件发生在某个 View 上时,事件将被传递给该 View。一般Button、TextView等控件都是继承父类View
事件分发过程由哪些方法协作完成?
答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()
方法 | 作用 | 调用时刻 |
---|---|---|
dispatchTouchEvent() | 这是 View 或 ViewGroup 中用于分发触摸事件的方法。 | 当一个触摸事件到达一个 View 或 ViewGroup 时,首先调用该方法。该方法会根据事件的类型和目标 View 的状态,决定是在当前 View 处理事件还是将事件传递给子 View。 |
onInterceptTouchEvent() | 这是 ViewGroup 中的一个方法,用于拦截触摸事件,阻止它们继续传递给子 View。 | 当一个 ViewGroup 的父 View 在 dispatchTouchEvent 方法中决定是否传递事件给它时,会先调用该方法。如果该方法返回 true,则表示拦截事件,停止传递给子 View;如果返回 false,则事件会继续传递给子 View。 |
onTouchEvent() | 这是 View 或 ViewGroup 中用于处理触摸事件的方法。 | 当一个触摸事件被分发给一个 View 或 ViewGroup 时,如果该 View 或 ViewGroup 没有拦截该事件,或者拦截后又决定放弃拦截,那么该方法会被调用。在该方法中,开发人员可以实现自定义的触摸事件处理逻辑。 |
二、事件分发机制流程概述
Android
事件分发流程 = Activity -> ViewGroup -> View
即:1个点击事件发生后,事件先传到Activity
、再传到ViewGroup
、最终再传到 View
即:要想充分理解Android分发机制,本质上是要理解:
Activity
对点击事件的分发机制ViewGroup
对点击事件的分发机制View
对点击事件的分发机制
下面,按顺序讲解:Activity
事件分发机制、ViewGroup
事件分发机制、View
事件分发机制
流程1:Activity的事件分发机制
Android事件分发机制首先会将点击事件传递到Activity中,具体是执行dispatchTouchEvent()进行事件分发。
源码总结
当一个点击事件发生时,从Activity
的事件分发开始(Activity.dispatchTouchEvent()
),流程总结如下:
三者介绍
- Activity:每个 Activity 都会包含一个或多个 Window,并通过 Window 来管理其显示内容。
- Window:它是抽象类,用于表示一个界面窗口,唯一的实现是PhoneWindow。每个 Activity 都包含一个 Window 对象,通过该 Window 对象来管理界面的布局、绘制和交互。
- DecorView:DecorView 是Activity的最顶层的View,它继承FrameLayout ,包含了整个界面的布局。
在事件分发源码中,这些类之间的关系体现在事件从顶层到底层的传递过程中:
- Activity:作为应用程序中的一个窗口管理器,Activity 负责创建和管理 Window,并在需要时处理事件分发。Activity 会将触摸事件传递给它的 Window 对象。
- Window:作为 Activity 的窗口,负责创建 DecorView 并管理用户界面的显示和交互。Window 会将触摸事件传递给它的 DecorView。
- DecorView:作为整个界面布局的根节点,负责管理并响应用户的触摸事件。
- DecorView 的父类是FrameLayout,FrameLayout没有这个方法的实现,所以再去找FrameLayout的父类ViewGroup
每个Activity 都持有一个window对象,而window是一个抽象类,它唯一的实现就是PhoneWindo,PhoneWindo又持有
核心方法总结
Activity.dispatchTouchEvent()
- 返回true:表示事件分发结束了,可能是下面两种之一
- 事件被下层的ViewGroup消费了
- 事件被Activity自己消费了
- 返回false:表示事件分没有结束,表示事件没有被消费
Activity.OnTouchEvent
- 返回true:表示事件分发结束了,表示Activity自己消费了这个事件
- 返回false:表示事件分发结束了,表示Activity没有消费了这个事件
onTouchEvent()
源码分析
事件分发是从dispatchTouchEvent 开始的,我们直接重写一下,去定位这个方法:super.dispatchTouchEvent(ev);
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//从这里开始分发
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
}
1.dispatchTouchEvent
点击后来到Activity类的源码,如下图所示
上图源码分析
如果用户点击了
那就执行 onUserInteraction(),onUserInteraction()是一个空方法,如果你有需要自己重写。
将事件交给Activity所持有的Window
如果返回true,Activity的dispatchTouchEvent 也就返回true了,表示整个事件传递就结束了,事件已经被消费了
如果返回false,那么就会调用Activity的onTouchEvent(ev)方法
2.getWindow().superDispatchTouchEvent(ev)
所以我们 定位到getWindow().superDispatchTouchEvent(ev) 方法中去看看
直接来到了 Window类的源码,如下图所示
那么这个抽象方法的实现 在哪里呢?
经过查询,我们知道它的实现是在PhoneWindow这个类中,我们搜索到这个类,去看源码,在这个类中找到superDispatchTouchEvent这个方法,如下图所示
而这个方法里面是调用了 mDecor 的superDispatchTouchEvent 方法,PhoneWindow 持有DecorView的实例。
再来看DecorView里面的superDispatchTouchEvent 方法,如下图所示
可以看到,DecorView它直接调用了它父类的dispatchTouchEvent方法,我们直接定位这个方法,发现直接来到了ViewGroup类,如下图所示
所以我们看懂了,如果 ViewGroup 的dispatchTouchEvent 返回true,那么一些列的 那些dispatchTouchEvent 都返回true,最终Activity的dispatchTouchEvent 就返回了true,事件就结束了。如果 ViewGroup 的dispatchTouchEvent 返回false,那么最终 Activity就会调用Activity的onTouchEvent(ev)方法。
为什么DecorView 会直接跳到ViewGroup?
因为DecorView 的父类是FrameLayout
FrameLayout中没有dispatchTouchEvent这个方法,它的父类是ViewGroup,ViewGroup中有这个方法
所以走到了这里
这里就完成了 从 Activity - 》 ViewGroup
3.onTouchEvent
我们返回到Activity的源码中,去定位一下onTouchEvent
上图源码分析
先进行了一个判断,如果`mWindow.shouldCloseOnTouch(this, event)` 返回true, `finish()`那么就会关闭当前的Activity,最终返回true。
否则,返回false
无论返回什么事件都会结束
那我们就定位一下mWindow.shouldCloseOnTouch(this, event)
,定位不过去的话,我们直接去Window类里面搜索这个方法,因为mWindow是Window的一个实例。
上图源码分析
先定义了常量 isOutside
1.当变量mCloseOnTouchOutside为true的时候,说明当前Activity是支持点击空白部分让Activity消失的。
2.当peekDecorView() 不为空的时候,表示当前Window是持有DecorView的
3.当isOutside 为true,说明当前的点击事件是在Window所持有的DecorView外面的
当1 2 3全部为true时,才返回ture,否则返回false
总结:该方法就是判断点击事件是不是在边界且能关闭Activity
- 如果返回true:表示事件分发结束,判断了点击事件在Window边界外,事件被Activity消费
- 如果返回false:表示事件分发结束,判断了点击事件在Window边界内,事件不作处理,不算被Activity消费
那么,ViewGroup
的dispatchTouchEvent()
什么时候返回true
/ false
?
请继续往下看ViewGroup事件的分发机制
流程2: ViewGroup的事件分发机制
从上面Activity的事件分发机制可知,在Activity.dispatchTouchEvent()实现了将事件从Activity->ViewGroup的传递,ViewGroup的事件分发机制从dispatchTouchEvent()开始。
源码总结
Android
事件分发传递到Acitivity后,总是先传递到ViewGroup
、再传递到View
。流程总结如下:(假设已经经过了Acitivity事件分发传递并传递到ViewGroup)
核心方法总结
ViewGroup.dispatchTouchEvent
- 返回true:表示事件分发结束了,可能是下面两种之一
- 事件被ViewGroup消费了
- 事件被View消费了
- 返回false:表示事件没有被消费,向上层传递给Activity
ViewGroup.OnTouchEvent
- 返回true:表示ViewGroup自己消费了这个事件
- 返回false:表示ViewGroup没有消费了这个事件
OnInterceptTouchEvent
上图中展示的很清楚,起到拦截的作用
源码分析
那我们就分析一下 ViewGroup 的 dispatchTouchEvent 做了什么
这个方法的源码很长,有两百行,可以自己去阅读一下。
这个方法主要做了三件事情:
- 去判断是否需要拦截事件
- 在当前ViewGroup中找到用户真正点击的View
- 分发事件到View上
/**
* 源码分析: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;
}
// 回到调用原处
实例分析
1. 布局说明
2. 测试代码
布局文件:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/my_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:focusableInTouchMode="true"
android:orientation="vertical">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮2" />
</LinearLayout>
核心代码:MainActivity.java
public class MainActivity extends AppCompatActivity {
Button button1,button2;
ViewGroup myLayout;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button1 = (Button)findViewById(R.id.button1);
button2 = (Button)findViewById(R.id.button2);
myLayout = (LinearLayout)findViewById(R.id.my_layout);
// 1.为ViewGroup布局设置监听事件
myLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "点击了ViewGroup");
}
});
// 2. 为按钮1设置监听事件
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "点击了button1");
}
});
// 3. 为按钮2设置监听事件
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "点击了button2");
}
});
}
}
3. 测试结果
// 点击按钮1,输出如下
点击了button1
// 点击按钮2,输出如下
点击了button2
// 点击空白处,输出如下
点击了ViewGroup
4. 结果分析
- 点击Button时,因为ViewGroup默认不拦截,所以事件会传递到子View Button,于是执行Button.onClick()。
此时ViewGroup. dispatchTouchEvent()会直接返回true,所以ViewGroup自身不会处理该事件,于是ViewGroupLayout的dispatchTouchEvent()不会执行,所以注册的onTouch()不会执行。即onTouchEvent() -> performClick() -> onClick()整个链路都不会执行,所以最后不会执行ViewGroup设置的onClick()里。
- 点击空白区域时,ViewGroup. dispatchTouchEvent()里遍历所有子View希望找到被点击子View时找不到,所以ViewGroup自身会处理该事件,于是执行onTouchEvent() -> performClick() -> onClick(),最终执行ViewGroupLayout的设置的onClick()
流程3:View的事件分发机制
从上面ViewGroup
事件分发机制知道,View
事件分发机制从dispatchTouchEvent()
开始
源码总结
这里需要特别注意的是,onTouch()
的执行 先于 onClick()
核心方法总结
主要包括:dispatchTouchEvent()、onTouchEvent()
dispatchTouchEvent()
返回true,表示事件分发结束了,事件被消费了
返回false,表示事件没有被消费,将事件传给上层
onTouchEvent()
若该控件可点击,就返回true,表明事件被当前这个View消费
若该控件不可点击,就返回false,表明事件没有被消费,将事件上传给上层ViewGroup的onTouchEvent处理
源码分析
dispatchTouchEvent()
展示核心逻辑:
上图分析:
- 判断触摸事件是否符合安全策略
- 判断当前的事件是否是通过鼠标进行滚动条的拖动,是的话就返回true,表示事件被消费了
- 判断是否注册了事件,也就是判断当前的View有没有注册TouchListener,如果注册了,那就不往下走了,说明事件已经被消费了,如果没注册,那就继续往下走,执行onTouchEvent()来消费这个事件。
-
说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true,因为result默认是false的;否则执行onTouchEvent()。 1. (mViewFlags & ENABLED_MASK) == ENABLED 2. mOnTouchListener != null 3. mOnTouchListener.onTouch(this, event)
-
具体分析可以看下面
-
- 经过1 2 后 result依然是false,并且 onTouchEvent() 返回true,那就表示事件被消费了。
- 事件也就是从这里进入到 onTouchEvent() 方法里面去了
三个条件的分析
/**
* 条件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
}
});
onTouchEvent()
这个方法代码量也很多,但是关于事件分发的逻辑不多
总结就是:
若该控件可点击,就返回true,表明事件被当前这个View消费
若该控件不可点击,就返回false,表明事件没有被消费,将事件上传给上层ViewGroup的onTouchEvent处理
/**
* 源码分析:View.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if ( (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener != null &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
/**
* 分析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;
}
实例分析
在本示例中,将分析两种情况:
- 注册Touch事件监听 且 在onTouch()返回false
- 注册Touch事件监听 且 在onTouch()返回true
分析1:注册Touch事件监听 且 在onTouch()返回false
代码示例
// 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("执行了onTouch(), 动作是:" + event.getAction());
return false;
}
});
// 2. 注册点击事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("执行了onClick()");
}
});
测试结果
执行了onTouch(), 动作是:0
执行了onTouch(), 动作是:1
执行了onClick()
测试结果说明
- 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次onTouch();
- 因为onTouch()返回了false,所以事件无被消费,会继续往下传递,即调用View.onTouchEvent();
- 调用View.onTouchEvent()时,对于抬起View事件,在调用performClick()时,因为设置了点击事件,所以会回调onClick()。
分析2:注册Touch事件监听 且 在onTouch()返回true
代码示例
// 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {
@Overridepublic boolean onTouch(View v, MotionEvent event) {
System.out.println("执行了onTouch(), 动作是:" + event.getAction());return true;}});// 2. 注册点击事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {
@Overridepublic void onClick(View v) {
System.out.println("执行了onClick()");}});
测试结果
执行了onTouch(), 动作是:0
执行了onTouch(), 动作是:1
测试结果说明
- 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次onTouch();
- 因为onTouch()返回了true,所以事件被消费,不会继续往下传递,View.dispatchTouchEvent()直接返回true;
- 所以最终不会调用View.onTouchEvent(),也不会调用onClick()。
至此,关于Android事件分发机制的内容已经讲解完毕,即Activity、ViewGroup、View的事件分发机制。
即:Activity
、ViewGroup
、View
的事件分发机制
三、常见事件分发场景
提示: 这里 可以搭配下面的 demo来看,方便理解
下面,我将通过实例说明常见的事件传递情况 & 流程
一般的事件传递场景有:
- 默认情况
- 处理事件
- 拦截
DOWN
事件 - 拦截后续事件(
MOVE
、UP
)
背景描述
讨论的布局如下:
情景:
- 用户先触摸到屏幕上
View C
上的某个点(图中黄区)
Action_DOWN
事件在此处产生
- 用户移动手指
- 最后离开屏幕
一般的事件传递情况
一般的事件传递场景有:
- 默认情况
- 处理事件
- 拦截
DOWN
事件 - 拦截后续事件(
MOVE
、UP
)
场景1:默认场景
- 即不对控件里的方法(
dispatchTouchEvent()
、onTouchEvent()
、onInterceptTouchEvent()
)进行重写 或 更改返回值 - 那么调用的是这3个方法的默认实现:调用下层的方法 & 逐层返回
- 事件传递情况:(呈
U
型)
- 从上往下调用dispatchTouchEvent()
Activity A ->> ViewGroup B ->> View C
- 从下往上调用onTouchEvent()
View C ->> ViewGroup B ->> Activity A
注:虽然ViewGroup B
的onInterceptTouchEvent
()对DOWN
事件返回了false
,但后续的事件(MOVE、UP)
依然会传递给它的onInterceptTouchEvent()
这一点与onTouchEvent()
的行为是不一样的:不再传递 & 接收该事件列的其他事件
同一个事件序列:后续事件
同一个事件序列,如果子View(ViewGroup)没有处理改事件(没有消费这个事件)
那么后续事件就不会再传递到子View中,直接调用Activity的onTouchEvent方法
场景2:处理事件
设View C
希望处理该点击事件,即:设置View C
为可点击的(Clickable)
或 复写其onTouchEvent()
返回true
最常见的:设置
Button
按钮来响应点击事件
事件传递情况:(如下图)
DOWN
事件被传递给C的onTouchEvent
方法,该方法返回true
,表示处理该事件- 因为
View C
正在处理该事件,那么DOWN
事件将不再往上传递给ViewGroup B 和Activity A
的onTouchEvent()
; - 该事件列的其他事件
(Move、Up)
也将传递给View C
的onTouchEvent()
会逐层往dispatchTouchEvent()
返回,最终事件分发结束
场景3:拦截DOWN事件
假设ViewGroup B
希望处理该点击事件,即ViewGroup B
复写了onInterceptTouchEvent()
返回true
、onTouchEvent()
返回true
事件传递情况:(如下图)
DOWN
事件被传递给ViewGroup B
的onInterceptTouchEvent()
,该方法返回true
,表示拦截该事件,即ViewGroup自己处理该事件(事件不再往下传递)- 调用自身的
onTouchEvent()
处理事件(DOWN
事件将不再往上传递给Activity A
的onTouchEvent()
) - 该事件列的其他事件
(Move、Up)
将直接传递给ViewGroup B
的onTouchEvent()
注:
- 该事件列的其他事件
(Move、Up)
将不会再传递给ViewGroup B
的onInterceptTouchEvent
();因:该方法一旦返回一次true
,就再也不会被调用 - 逐层往
dispatchTouchEvent()
返回,最终事件分发结束
场景4:拦截DOWN的后续事件
如果一个ViewGroup在DOWN事件时没有拦截(即返回false
),但在后续的MOVE事件中拦截了事件(返回true
),则会发生以下情况:
- DOWN事件被首个接收到它的View处理,通常是一个子View。这意味着DOWN事件将传递给该子View的
onTouchEvent()
方法进行处理。 - 当MOVE事件到达ViewGroup时,ViewGroup会拦截该事件,不再将其传递给子视图。这是因为在MOVE事件中,ViewGroup已经表明了它有意拦截并处理后续的事件。
- 一旦ViewGroup拦截了MOVE事件,它将负责处理后续的触摸事件,而不会再将其传递给内部的子视图。这意味着在MOVE事件之后,ViewGroup的
onTouchEvent()
方法将被调用来处理触摸事件。
这种行为通常被用来实现一些特定的交互效果,例如拖拽操作或滑动手势,其中在一开始由子视图处理DOWN事件,但是在后续的MOVE事件中被ViewGroup拦截以实现整体的滑动或拖动效果。
结论
- 若
ViewGroup
拦截了一个半路的事件(如MOVE
),该事件将会被系统变成一个CANCEL
事件 & 传递给之前处理该事件的子View
; - 该事件不会再传递给
ViewGroup
的onTouchEvent()
- 只有再到来的事件才会传递到
ViewGroup
的onTouchEvent()
场景描述 ViewGroup B
无拦截DOWN
事件(还是View C
来处理DOWN
事件),但它拦截了接下来的MOVE
事件
即
DOWN
事件传递到View C
的onTouchEvent()
,返回了true
实例讲解
- 在后续到来的MOVE事件,
ViewGroup B
的onInterceptTouchEvent()
返回true
拦截该MOVE
事件,但该事件并没有传递给ViewGroup B
;这个MOVE
事件将会被系统变成一个CANCEL
事件传递给View C
的onTouchEvent()
- 后续又来了一个
MOVE
事件,该MOVE
事件才会直接传递给ViewGroup B
的onTouchEvent()
后续事件将直接传递给ViewGroup B
的onTouchEvent()
处理,而不会再传递给ViewGroup B
的onInterceptTouchEvent()
,因该方法一旦返回一次true,就再也不会被调用了。
View C
再也不会收到该事件列产生的后续事件
至此,关于Android
常见的事件传递情况 & 流程已经讲解完毕。
为什么第一个move没有被拦截?
问题回顾
这个问题是关于【场景4:拦截DOWN的后续事件】
就是当我们ViewGroup的onInterceptTouchEvent中,选择了只拦截MOVE事件,我们手指点击View处,再平移一段距离,再抬起,会发生下面三点
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
super.onInterceptTouchEvent(ev);
// 在MOVE事件时拦截事件
if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
return true; // 拦截MOVE事件
}
return false; // 不拦截DOWN事件
}
- DOWN事件,没有被拦截,传到了MyView消费了
- 第一个MOVE事件,onInterceptTouchEvent 检测到了,但是没有拦截,还是传到了MyView
- 后续的MOVE事件,不再传到MyView,MyViewGroup直接消费了。
为什么第一个MOVE不拦截呢?
原因:
简单讲:如果DOWN事件传给了子View,但是父ViewGroup要拦截后续的MOVE事件,那么第一个MOVE事件变成了CANLE事件传给了子VIEW。
详细讲:
当 当前view消费了ACTION_DOWN之后,在后续的ACTION_MOVE过程中,父容器拦截了事件,这时候,就会先将当前这个ACTION_MOVE转变成ACTION_CANCEL事件,发送给之前消费ACTION_DOWN事件的View,同时会将mFirstTouchTarget置为null,这样的话,在下一个ACTION_MOVE到来时,就不会走拦截和分发流程,直接将ACTION_MOVE事件交给父容器处理。
源码分析
ViewGroup关于这部分的源码我分成三部分来展示
第一部分:
判断事件是否需要拦截,如果父容器拦截,那么intercepted = true
//第一部分,判断事件是否需要拦截,disallowIntercept的值可由子view调用requestDisallowInterceptTouchEvent
//来改变,可以看出子类调用requestDisallowInterceptTouchEvent(false)时,disallowIntercept置为false,
//允许父容器拦截,onInterceptTouchEvent返回true时,intercepted = true,
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
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;
}
第二部分
intercepted
为true时,调用了这个dispatchTransformedTouchEvent
这个方法,给它的参数cancel
传递了true
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
//intercepted为true时,cancelChild为true,dispatchTransformedTouchEvent调用时,传入true
if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
handled = true;
}
//消费ACTION_CANCEL之后,将mFirstTouchTarget 置空,因此在后面的MOVE事件来到时,不会走入第一部分的拦截判断
//会走入else分支,intercepted = true;将后续MOVE交给自己处理。
if (cancelChild) {
mFirstTouchTarget 置空
}
//cancel为true
第三部分
dispatchTransformedTouchEvent
这个方法做了什么?
主要是下面黄底的那两行:
- 如果cancle为ture,或者当前的事件是CANCLE事件
- 那么就把事件设置为了CANCLE事件
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// cancel为true时,event.setAction(MotionEvent.ACTION_CANCEL),将MOVE事件变成ACTION_CANCEL事件。
// child为消费ACTION_DOWN的view。child不为空,即消费ACTION_CANCEL。
// 需要注意的是,手指移动时ACTION_MOVE会多次触发,这里消费的是当前转换来的MOVE事件,
// 当下一个MOVE事件来的时候,mFirstTouchTarget已经为空了,所以会将后续的ACTION_MOVE事件交给父容器处理。
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);
}
//转换为CANCEL事件并消费后,重新设置回ACTION_MOVE类型。
event.setAction(oldAction);
return handled;
}
}
总结
所以,看了源码就解释了,为什么第一个MOVE会成“漏网之鱼”,那是因为,DOWN事件传下去了,但是MOVE被拦截了,那么第一个MOVE事件会被变成CANCLE事件,被消费,当下一个MOVE事件来的时候,mFirstTouchTarget已经为空了,所以会将后续的ACTION_MOVE事件交给父容器处理。
参考博客:Android事件分发机制
参考视频:彻底搞懂Android事件分发机制_哔哩哔哩_bilibili
四、Demo验证
验证1:默认场景
场景解释:默认场景,不做任何改动。
我们在MainActivity、MyView、MyViewGroup里面重写了 事件分发的主要的方法
代码
MainActivity
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG,"MainActivity: dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"MainActivity: onTouchEvent");
return super.onTouchEvent(event);
}
}
MyView
public class MyView extends View {
public static final String TAG = "MainActivity";
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"MyView: onTouchEvent");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG,"MyView: dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
}
MyViewGroup
为什么要继承FrameLayout,而不去继承ViewGroup? 因为默认情况下ViewGroup中没有子View的布局,为了避免重新去写布局,这就直接继承FrameLayout,因为FrameLayout继承了ViewGroup
public class MyViewGroup extends FrameLayout {
public static final String TAG = "MainActivity";
public MyViewGroup(@NonNull Context context) {
super(context);
}
public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG,"MyViewGroup: dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"MyViewGroup: onTouchEvent");
return super.onTouchEvent(event);
}
}
activity_main.xml
外层用了MyViewGroup ,内层用了MyView,简单设置了颜色,外层蓝色,内层红色
<?xml version="1.0" encoding="utf-8"?>
<com.example.eventdistributiondemo.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/design_default_color_primary_variant"
tools:context=".MainActivity"
>
<com.example.eventdistributiondemo.MyView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/design_default_color_error"
></com.example.eventdistributiondemo.MyView>
</com.example.eventdistributiondemo.MyViewGroup>
效果
模拟用户: 触摸View -> 抬起
输出:
可以看到,上面6行 是 Down事件,下面2行是Up事件
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MyViewGroup: onTouchEvent
MainActivity: onTouchEvent
MainActivity: dispatchTouchEvent
MainActivity: onTouchEvent
验证了:
- 呈 U 形 的传递
- 同一个序列的后续事件是不会往下传递的
验证2:处理事件
场景解释:让View去处理这个事件。
代码
只需要将上面【验证1】的代码的MyView 的 onTouchEvent 方法 返回true,表示MyView处理了这个事件
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"MyView: onTouchEvent");
// return super.onTouchEvent(event);
super.onTouchEvent(event);
return true;
}
效果
模拟用户: 触摸View -> 抬起
输出:
可以看到,DOWN 和 UP 事件都传到了MyView
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
验证3:拦截DOWN场景
场景解释:让ViewGroup拦截事件,并去处理这个事件。
只需要将上面【验证1】的代码的 MyViewGroup 改一下,让onInterceptTouchEvent()
返回true
、onTouchEvent()
返回true
,表示MyViewGroup 处理这个事件,不会再向下传递到MyView了。
代码
MyViewGroup 代码更改:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
// return super.onInterceptTouchEvent(event);
super.onInterceptTouchEvent(ev);
return true;//返回true,直接拦截
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"MyViewGroup: onTouchEvent");
// return super.onTouchEvent(event);
super.onTouchEvent(event);
return true;//返回true,表示事件有MyViewGroup来处理
}
效果
模拟用户: 触摸View -> 抬起
输出:
可以看到,确实是没有经过MyView
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyViewGroup: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onTouchEvent
验证4:拦截DOWN的后续事件
场景解释:让ViewGroup只拦截MOVE事件,其他事件不拦截,并且View和ViewGroup都能处理这个事件,就看会事件是怎么分发的了。
代码
在【验证1】的基础上改:
MyViewGroup
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG,"MyViewGroup: onInterceptTouchEvent");
super.onInterceptTouchEvent(ev);
// 在MOVE事件时拦截事件
if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
return true; // 拦截MOVE事件
}
return false; // 不拦截DOWN事件
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"MyViewGroup: onTouchEvent");
// return super.onTouchEvent(event);
super.onTouchEvent(event);
return true;//返回true,表示事件有MyViewGroup来处理
}
MyView
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"MyView: onTouchEvent");
// return super.onTouchEvent(event);
super.onTouchEvent(event);
return true;
}
效果
用户 DOWN -》MOVE-》MOVE-》MOVE … -》UP
输出
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onInterceptTouchEvent
MyView: dispatchTouchEvent
MyView: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onTouchEvent
MainActivity: dispatchTouchEvent
MyViewGroup: dispatchTouchEvent
MyViewGroup: onTouchEvent
.......
讲解:
1-5行:DOWN事件,没有被拦截,传到了MyView消费了
6-10行:第一个MOVE事件,onInterceptTouchEvent
检测到了,但是没有拦截,还是传到了MyView
11-13以及后续的:后续的MOVE事件,不再传到MyView,MyViewGroup直接消费了。
五、Down事件和其他事件的区别
上面的Demo的验证1里面,验证了一个结论:同一个序列的后续事件是不会往下传递的
也就是说Down事件传递了下去,最终结束,而同一个序列的其他事件是不会再全部走的,而是直接在对应位置结束了事件分发。
可以比喻成:Down事件是探路的先锋,后续事件根据Down事件的路程,直接走了捷径。
嵌套ViewGroup的问题
上面我们的例子中都是一个ViewGroup,下面我们多嵌套一个,最后的结论是一样的,只是三层的结构变成了四层,规律不变,可以根据下面的图,来分析。
情况1:各层的dispatchTouchEvent 返回true
首先要明确:dispatchTouchEvent 返回true,就是事件分发结束了
这种情况的总结:红色的down事件 和 蓝色的其他事件 走的路径相同
在ViewGroup1 的dispatchTouchEvent 方法返回true
这里多嵌套了一个ViewGroup
红色的箭头代表ACTION_DOWN 事件的流向 蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
在ViewGroup2 的dispatchTouchEvent 返回true
同理在View的dispatchTouchEvent 返回true
与上图效果类似,不做图了
情况2:各层的的onTouchEvent 返回true消费这次事件
首先要明确:onTouchEvent 返回true,就是事件分发结束了,且被当前的View\ViewGroup\Activity消费了
这种情况的总结:红色的down事件是走了全程,而蓝色的其他事件是走了“捷径”
View的的onTouchEvent 返回true消费这次事件
在ViewGroup 2 的onTouchEvent 返回true消费这次事件
从这里就能看出区别了,红色的down事件是走了全程,而蓝色的其他事件是走了捷径
在ViewGroup 1 的onTouchEvent 返回true消费这次事件
Activity 的onTouchEvent 返回true消费这次事件
情况3:各层的dispatchTouchEvent 返回false
首先要明确:
Activity:dispatchTouchEvent 返回false,事件分发结束,事件没有被消费
ViewGroup:dispatchTouchEvent 返回false,会将事件回传到上层Activity.OnTouchEvent处,表示事件没有被ViewGroup消费。
View:dispatchTouchEvent 返回false,会将事件回传到上层ViewGroup.OnTouchEvent处,表示事件没有被View消费。
这种情况的总结:红色的down事件是走了全程,而蓝色的其他事件是走了“捷径”
我们在View的dispatchTouchEvent 返回false并且Activity的onTouchEvent 返回true消费这次事件
在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事件
在View的dispatchTouchEvent 返回false并且在ViewGroup 2 的onTouchEvent 返回true消费这次事件
在ViewGroup2的dispatchTouchEvent 返回false并且在ViewGroup1 的onTouchEvent返回true消费这次事件
情况4:ViewGroup的onInterceptTouchEvent返回true
首先要明确:
onInterceptTouchEvent返回true,就是ViewGroup要拦截这个事件,事件不会再向下传递
这种情况的总结:红色的down事件是走了全程,而蓝色的其他事件是走了“捷径”
在ViewGroup2的onInterceptTouchEvent 返回true拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。
我在上面验证1里只展示了一种例子,如果想看更多的例子,可以看这个博客,结论是一样的
具体示例可以参考https://www.jianshu.com/p/e99b5e8bd67b
参考
总结部分:可以去看参考文章【1】里面的总结
参考文章:
【1】Carson带你学Android:手把手带你深入分析事件分发机制!
【2】 图解 Android 事件分发机制 - 简书 (jianshu.com)
【3】 Android事件分发机制–拿来吧你 - 简书 (jianshu.com)
参考视频: