[TOC]
# 基础知识
## Activity、PhoneWindow、DecorView
![](https://img.kancloud.cn/27/2a/272a20ae7493e435306de73c220ff3c6_281x418.png)
## 事件MotionEvent
MotionEvent负责处理报告所有类型的设备输入事件,如手指触摸、鼠标、触控笔、轨迹球等。在实际开发中我们主要关注手指触摸事件即可。
事件|说明
---|---
ACTION_DOWN|手指初次接触到屏幕时触发
ACTION_MOVE|手指在屏幕上滑动时触发,可多次触发
ACTION_UP|手指离开屏幕时触发
ACTION_CANCEL|事件被上层拦截时触发
ACTION_OUTSIDE|手指不在控件区域时触发
## Input系统
本段出自[http://gityuan.com/2015/09/19/android-touch/](http://gityuan.com/2015/09/19/android-touch/)
当手指触摸到屏幕时,屏幕硬件一行行不断地扫描每个像素点,获取到触摸事件后,从底层产生中断上报。再通过native层调用Java层InputEventReceiver中的dispatchInputEvent方法。经过层层调用,交由Activity的dispatchTouchEvent方法来处理。
![](https://img.kancloud.cn/89/44/89445a5d29b5bc8313ee33c566a145ba_529x749.png)
# Activity 的事件分发
## DecorView的分发
首先从上图中DecorView的dispatchTouchEvent看起:
```java
// DecorView.java
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
```
Activity在创建Window对象时,会为Window对象设置回调接口,也就是Activity自己,这样Window在收到外界的状态改变时,就会回调Activity的相应方法。
上面代码中cb指Window.Callback,Activity实现了此接口,因此接下来调用Activity的dispatchTouchEvent:
```java
// Activty.java
public boolean dispatchTouchEvent(MotionEvent ev) {
// 捕获用户正在和设备交互,为了更方便的管理通知栏的通知,暂不重要
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 事件分发给 Window
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 当 PhoneWindow 里的所有View都不消费事件时(多用于处理落在Window范围外部的触摸事件)
return onTouchEvent(ev);
}
```
主要的代码在Window的事件分发,一起来看看:
## PhoneWindow 的分发
先来看看 PhoneWindow 的分发:
```java
// PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
```
可以看到,直接调用自身 DecorView 的 superDispatchTouchEvent 方法:
```java
// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
```
而 DecorView 又直接调用了父类 ViewGroup 的 dispatchTouchEvent 方法。DecorView是一个FrameLayout,触摸事件在这一步传递给了根 ViewGroup。
## PhoneWindow不处理时,Activity 的处理
当 PhoneWindow 不消费事件,也就是当前 Activity 的所有 View 都不消费事件时,事件会回传给 Activity,调用 onTouchEvent 方法:
```java
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
```
mWindow 为 PhoneWindow 对象,在 PhoneWindow 代码中未找到 shouldCloseOnTouch 方法,来看看其父类 Window 中的该方法:
```java
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
```
当设置了 mCloseOnTouchOutside 为 true,并且为 MotionEvent.ACTION_DOWN 事件,手指在 Activity 界面以外,且当前 Activity 包含子 View 时,会返回 true,当前 Activity 会被 finish。
下面来仔细看看 ViewGroup 的事件分发。
# ViewGroup 的事件分发
## 概述
**分发事件**
方法:dispatchTouchEvent()
结果:返回值为true代表事件被当前View消费了,为false代表没被消费
1、由于Android视图结构为树状结构,所以触摸事件都是从rootView开始依次向下进行传递,称之为事件分发。
2、在没有拦截的情况下,事件最终将会分发给手指触摸区域的子 View,子 View 重叠时分发给最上层的子 View
**拦截事件**
方法:onInterceptTouchEvent()
1、事件在分发的过程中,可以被中途某个ViewGroup进行拦截,停止向下进行分发传递。
2、ViewGroup拦截后,直接调用其onTouchEvent方法看是否需要消费使用。
![](https://img.kancloud.cn/1c/5f/1c5fda29d08c3337720fd95824fd94a9_387x454.png)
**使用事件**
方法:onTouchEvent(MotionEvent event)
定义:拿到MotionEvent实例,可以获取到事件内容并使用
结果:onTouchEvent返回值为true时代表消费了事件,上层View不会再接收事件回传;返回值为false时代表没消费事件,可能只是使用了事件的内容,但没消费,事件会继续进行回传。
事件被消费,就意味着事件信息传递终止
1、如果事件没有被拦截,会按视图层次结构依次往下进行传递,直到传递到最底层的View,也就是最终命中的View。
2、命中的View拿到事件后,会决定是否消费,如果消费了就没上层View什么事了。
3、如果命中的View没有消费事件,事件会回传,依次调用上层View的onTouchEvent方法,看上层View是否需要使用事件做些什么。
![](https://img.kancloud.cn/06/28/0628cdfce78768557dd3af59e55bc594_394x580.png)
## ViewGroup 的事件分发机制伪代码:
这一段伪代码来源于 GcsSloop 大神的[文章](http://www.gcssloop.com/customview/dispatch-touchevent-source),很有嚼劲。理解了这段代码基本对于 Android 事件分发机制就有了宏观上的把握了。配合吴小龙同学的这篇 [Android 事件传递机制分析](http://wuxiaolong.me/2015/12/19/MotionEvent/)服用效果更佳!
```java
// 返回值代表是否将事件消费掉
public boolean dispatchTouchEvent(MotionEvent event) {
// 默认状态为未消费过
boolean result = false;
// 如果没有拦截(ViewGroup一般不会进行拦截,可点击的ViewGroup除外,下文源码部分有分析)
if (!onInterceptTouchEvent(event)) {
// 则交给childView
result = child.dispatchTouchEvent(event);
}
// 如果事件没有被childView消费
if (!result) {
// 则调用自身 onTouchEvent()
result = onTouchEvent(event);
}
// 返回事件消费状态
return result;
}
```
## ViewGroup 的事件分发机制源码
```java
public boolean dispatchTouchEvent(MotionEvent ev) {
//...
final boolean intercepted;
//这里首先对是否拦截此事件做了很多判断
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//...
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//...
// 如果不拦截才会往下走进行分发
if (!canceled && !intercepted) {
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
// 查找可以处理本次事件的childView
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//...
// 将事件交给childView
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//...
}
}
if (mFirstTouchTarget == null) {
// 此处会调用super.dispatchTouchEvent()
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
//...
}
return handled;
}
```
其中交给childView处理的dispatchTransformedTouchEvent方法源码如下:
```java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
//...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
//调用childView的dispatchTouchEvent方法
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
```
最后来看看刚刚 onInterceptTouchEvent 方法的默认实现:
```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;
}
```
可以看到,会先检查事件来源。当为鼠标事件,且当前为 ACTION_DOWN,且鼠标左键按下时,默认返回 true;大部分情况即非鼠标事件时,onInterceptTouchEvent 都是返回 false。
# View 的事件分发
在事件向下分发的过程中,如果中间没有ViewGroup拦截事件,事件会一直传递到最底层的子View。调用View的dispatchTouchEvent方法。
## 分发顺序
View 可以注册很多事件监听器,如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法。这些方法由 dispatchTouchEvent() 方法进行分发。
事件的调度顺序为:onTouchListener > onTouchEvent > onLongClickListener > onClickListener
## View 的事件分发机制伪代码
```java
// 返回值代表是否将事件消费掉
public boolean dispatchTouchEvent(MotionEvent event) {
if(mOnTouchListener.onTouch(this, event)) {
return true;
} else if (onTouchEvent(event)) {
return true;
}
return false;
}
```
onClickListener 和 onLongClickListener 在 onTouchEvent 中调用。
## View 的事件分发机制源码:
```java
public boolean dispatchTouchEvent(MotionEvent event) {
//...
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
// 回调mOnTouchListener接口
if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 调用onTouchEvent方法(在里面会回调onLongClickListener、onClickListener)
if (!result && onTouchEvent(event)) {
result = true;
}
}
//...
return result;
}
public boolean onTouchEvent(MotionEvent event) {
// 判断View是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
//...
// 执行单击事件
performClick();
break;
case MotionEvent.ACTION_DOWN:
//...
// 检测长按,并执行长按事件
checkForLongClick(0, x, y);
break;
}
// 只要是可点击的,就会返回true,也就是当前View就会消费事件
return true;
}
return false;
}
```
## 点击事件
View的onClick事件需要同时接收到ACTION_DOWN和ACTION_UP才能触发,如果这两个事件被分发给了不同的View,点击事件就不会被触发。
View只有消费了ACTION_DOWN事件,才能接收到后续的事件,并且后续事件传递来时,不会再传递给其他View,除非在途中被其他View拦截。如果后续事件确实被拦截,当前View会收到一个ACTION_CANCEL的事件,表示事件结束,不会有后续事件。
一次触摸流程中产生的事件应被同一 View 消费,全部接收或者全部拒绝。
## 注意要点
* 只要给 View 注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的。
* 可点击的 View 就会消费事件,不可点击的 View 不会消费事件
* ViewGroup 和 ChildView 同时注册了点击事件监听器时,事件优先给 ChildView 消费
* 同一次点击事件只能被一个 View 消费,防止事件混乱
# 总结
对于ViewGroup来说:
* 用户触摸事件从rootView依次向下进行传递。在没有拦截的情况下,事件会依次传递到最底部子View
* 事件分发过程中,可被中途某个ViewGroup进行拦截,停止向下进行分发传递。ViewGroup拦截后直接调用其onTouchEvent看是否需要消费使用,如不消费事件往上进行回传
* 如果事件没被拦截顺利传递给最底层的View,命中的View会决定是否消费,消费了就没上层View的事情了
* 子View如果不消费事件,事件会进行回传,每个层级的ViewGroup都可以拿到事件并决定是否消费
* 如果事件一直没有被消费,最后会回传给 Activity,如果 Activity 也不需要就被抛弃
对于子View来说:
* 只要注册了onClickListener、onLongClickListener、onContextClickListener或设置了clickable="true",就代表时可点击的
* 可点击的View就会消费事件,不可点击的View不会消费事件
* 当ChildView重叠时,一般会分配给显示在最上面的ChildView
# 补充
关于 ViewGroup 对于触摸事件的分发流程图,可参考下面这几幅图,[来源](https://blog.csdn.net/xyz_lmn/article/details/12517911)
1、都不拦截且都不消费的情况
![](https://img.kancloud.cn/a2/5c/a25cc0c7a0f9b0ab399d0aa2bc5af348_571x672.png)
2、View 消费了事件,不再进行回传
![](https://img.kancloud.cn/e0/06/e00648dce27442722a03c1414ee50771_685x562.png)
3、View 消费了 ACTION_DOWN
![](https://img.kancloud.cn/7c/8b/7c8bfac03d3ff75081ba338c65fc0e87_732x693.png)
View 消费了 ACTION_DOWN,但后续的 ACTION_MOVE 和 ACTION_UP 被上层拦截了,会给它个ACTION_CANCEL
![](https://img.kancloud.cn/08/34/083446b32dfa4eef6f8c5b303e578275_715x628.png)
4、上层一开始就拦截了事件,此时子 View 对于事件无感
![](https://img.kancloud.cn/d1/ff/d1ff3576cc05d6c3a32cbe079b36c3ac_619x628.png)
# 参考文档:
[安卓自定义View进阶-事件分发机制详解](http://www.gcssloop.com/customview/dispatch-touchevent-source)
- 导读
- Java知识
- Java基本程序设计结构
- 【基础知识】Java基础
- 【源码分析】Okio
- 【源码分析】深入理解i++和++i
- 【专题分析】JVM与GC
- 【面试清单】Java基本程序设计结构
- 对象与类
- 【基础知识】对象与类
- 【专题分析】Java类加载过程
- 【面试清单】对象与类
- 泛型
- 【基础知识】泛型
- 【面试清单】泛型
- 集合
- 【基础知识】集合
- 【源码分析】SparseArray
- 【面试清单】集合
- 多线程
- 【基础知识】多线程
- 【源码分析】ThreadPoolExecutor源码分析
- 【专题分析】volatile关键字
- 【面试清单】多线程
- Java新特性
- 【专题分析】Lambda表达式
- 【专题分析】注解
- 【面试清单】Java新特性
- Effective Java笔记
- Android知识
- Activity
- 【基础知识】Activity
- 【专题分析】运行时权限
- 【专题分析】使用Intent打开三方应用
- 【源码分析】Activity的工作过程
- 【面试清单】Activity
- 架构组件
- 【专题分析】MVC、MVP与MVVM
- 【专题分析】数据绑定
- 【面试清单】架构组件
- 界面
- 【专题分析】自定义View
- 【专题分析】ImageView的ScaleType属性
- 【专题分析】ConstraintLayout 使用
- 【专题分析】搞懂点九图
- 【专题分析】Adapter
- 【源码分析】LayoutInflater
- 【源码分析】ViewStub
- 【源码分析】View三大流程
- 【源码分析】触摸事件分发机制
- 【源码分析】按键事件分发机制
- 【源码分析】Android窗口机制
- 【面试清单】界面
- 动画和过渡
- 【基础知识】动画和过渡
- 【面试清单】动画和过渡
- 图片和图形
- 【专题分析】图片加载
- 【面试清单】图片和图形
- 后台任务
- 应用数据和文件
- 基于网络的内容
- 多线程与多进程
- 【基础知识】多线程与多进程
- 【源码分析】Handler
- 【源码分析】AsyncTask
- 【专题分析】Service
- 【源码分析】Parcelable
- 【专题分析】Binder
- 【源码分析】Messenger
- 【面试清单】多线程与多进程
- 应用优化
- 【专题分析】布局优化
- 【专题分析】绘制优化
- 【专题分析】内存优化
- 【专题分析】启动优化
- 【专题分析】电池优化
- 【专题分析】包大小优化
- 【面试清单】应用优化
- Android新特性
- 【专题分析】状态栏、ActionBar和导航栏
- 【专题分析】应用图标、通知栏适配
- 【专题分析】Android新版本重要变更
- 【专题分析】唯一标识符的最佳做法
- 开源库源码分析
- 【源码分析】BaseRecyclerViewAdapterHelper
- 【源码分析】ButterKnife
- 【源码分析】Dagger2
- 【源码分析】EventBus3(一)
- 【源码分析】EventBus3(二)
- 【源码分析】Glide
- 【源码分析】OkHttp
- 【源码分析】Retrofit
- 其他知识
- Flutter
- 原生开发与跨平台开发
- 整体归纳
- 状态及状态管理
- 零碎知识点
- 添加Flutter到现有应用
- Git知识
- Git命令
- .gitignore文件
- 设计模式
- 创建型模式
- 结构型模式
- 行为型模式
- RxJava
- 基础
- Linux知识
- 环境变量
- Linux命令
- ADB命令
- 算法
- 常见数据结构及实现
- 数组
- 排序算法
- 链表
- 二叉树
- 栈和队列
- 算法时间复杂度
- 常见算法思想
- 其他技术
- 正则表达式
- 编码格式
- HTTP与HTTPS
- 【面试清单】其他知识
- 开发归纳
- Android零碎问题
- 其他零碎问题
- 开发思路