通过问题来学习一个东西是很好的方法。学习Android中View的事件体系,我也通过给自己提问题,在解决问题的同时也就知道了其中原理。

首先来几个问题起步:

  • 什么是事件?什么是事件分发机制?

在我们通过屏幕与手机交互的时候,每一次点击、长按、移动等都是一个个事件。按照面向对象的思想,这些一个个事件都被封装成了MotionEvent。

分发机制就是某一个事件从屏幕传递给app视图中的各个View,然后由其中的某个View来使用这一事件或者忽略这一事件,这整个过程的控制就是分发机制了。

要注意的是,事件分发机制中,事件是按一个事件序列的形式分发给View的。这一序列由 ACTION_DOWN 开始,经过一系列 ACTION_MOVE 等事件,最后以 ACTION_UP 事件结束。这一个序列中的所有事件,要么被忽略,要么就只能有一个事件能使用。要是同一个序列,比如从按下到移动这一系列的动作,不同的View都能接受的话,那整个界面就会非常混乱,而且逻辑很复杂。

接下来我提出这三个问题:

某一个事件从屏幕一直传递到View上这一过程的大致流程是怎样的?前面说了事件分发的其实是事件序列。那么同一个序列里那么多事件,是怎样的机制只交给一个View的?我们平时在应用开发时,在外部给View设置的的OnClick OnLongClick 的监听,是在哪里被View处理的?

问题一:事件传递的流程是怎样的?

Android中的View是树状结构,如下图所示:

每一个Activity内部都包含一个Window用来管理要显示的视图。而Window是一个抽象类,其具体实现是 PhoneWindow类。DecovrView作为PhoneWindow的一个内部类,实际管理着具体视图的显示。他是FrameLayout的子类,盛放着我们的标题栏和根视图。我们自己写的一些列View和ViewGroup都是由他来管理的。因此事件分发的时候,顶层的这些“大View”们实际上是不会对事件有任何操作的,他们只是把事件不断的向下递交,直到我们可以使用这些事件。

所以,事件自顶向下的传递过程应该是这样的:

Activity(不处理)-> 根View -> 一层一层ViewGroup(如果有的话) -> 子View

如果传递到最后我们的子View们没有处理这一事件怎么办呢?这时候就会原路返回,最终传递给Activity。只有当Activity也没有处理这一事件时,这一事件才会被丢弃。

Activity(不处理则丢弃) <- 根View <- 一层一层ViewGroup(如果有的话) <- 子View

具体在传递事件的时候,是由以下三个方法来控制的:

  • dispatchTouchEvent : 分发事件onInterceptTouchEvent : 拦截事件onTouchEvent : 消费事件

这三个方法有一个共同点,就是他们具体是否执行了自己的功能(分发、拦截、消费)完全由自己的返回值来确定,返回true就表示自己完成了自己的功能(分发、拦截、消费)。不同之处除了功能外,还有使用的场景。dispatchTouchEvent()和onTouchEvent()这两个方法,无论是Activity ViewGroup 还是View,都会被用到。而onInterceptTouchEvent()方法因为只是为了拦截事件,那么Activity和View一个在最顶层,一个在最底层,也就没必要使用了。因此在View 和 Activity中是没有onInterceptTouchEvent()方法的。

我这里自定义几个ViewGroup和View,分别重写他们的这些方法,在重写的时候打上log。在不添加任何监听(即没有View消费事件)的条件下看一下运行结果:

点击外部ViewGroup:

点击子View:

可以看到,事件分发首先由ViewGroup的dispatchTouchEvent()方法开始,先调用自己的onInterceptTouchEvent()方法判断是否拦截,返回false表示自己没有拦截,那么接下来直接把事件传给子View。子View调用自己的dispatchTouchEvent()方法进行分发,因为View没有onInterceptTouchEvent()方法,所以不存在拦截操作,因此直接将事件交给自己的onTouchEvent()方法消费。因为我的子View没有使用这个事件,因此onTouchEvent()方法直接返回了false表示自己没有消费,那么这个事件此时就算是传到底了。因为自己没有消费,因此自己就没有分发出去,那么子View的dispatchTouchEvent()方法返回false,把这个事件交还给上一层的ViewGroup。ViewGroup发现这个事件没有子View消费,那么就自己动手吧!将事件传给自己的onTouchEvent()方法消费。可是ViewGroup也没有消费,那么onTouchEvent()方法只能是再返回false了。同理,ViewGroup自己没有消费事件,因此他的dispatchTouchEvent()方法也返回了false。这段文字说得可能有点乱,那么就贴一张图来演示一下:(图中红色箭头表示事件自顶向下分发的过程,黄色则表示自底向上返回的过程)

接下来,我在子View上添加OnClick监听,再看一下点击子View时的运行结果:

乍一看,呀,怎么重复打印了两遍log?其实并不是哪里写错了。前面我说了,事件分发分发的是一个事件序列,我添加了点击事件,那么我就要消费点击事件。而点击事件其实是要分成两个事件的,即ACTION_DOWN + ACTION_UP ,只有这样才算是一次点击事件。因此打印了“两遍”log其实是先打印了ACTION_DOWN的分发流程,再打印了一遍ACTION_UP的分发流程,因此会看到最后一行打印了click事件。即,click事件是在ACTION_UP事件发生后才发生的。

然后看看各个方法的返回值。果然由于我的子View明确表示要消费这个事件序列,因此从ACTION_DOWN开始的所有事件就都交给他消费了。所以子View的onTouchEvent的返回值为true,表示自己需要消费这个事件,然后他的dispatchTouchEvent也返回了true,表示这一事件被自己分发了。既然自己的子View消费了事件,ViewGroup就认为这一事件是被自己分发了,因此他的dispatchTouchEvent也就返回了true。还是来一张图更清楚一点:最后,我在上一步的基础上,给ViewGroup的onInterceptTouchEvent()方法返回值强行改为true,表示事件传到这一层的时候就被拦截了,看一下log:

果然,虽然我要在子View消费事件,但是事件在传到子View之前就被ViewGroup拦截了,那么事件就只会由ViewGroup来消费了,所以ViewGroup就把事件传给了自己的onTouchEvent()来消费。再来一张图:

综上,事件分发的大致流程就是这样。

问题二:如何保证统一序列的事件都交给一个View来处理

先上结论:在传递过程中,只要有一个View主动去消费了第一个事件(ACTION_DOWN),那么ViewGroup会将这个View保存起来,之后同一事件序列的其他事件都直接交给这个View来处理。具体怎么操作,需要看一下源码:

//这是ViewGroup dispatchTouchEvent()的源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//省略前面一部分无关代码
//handled是返回的结果,表示是否被分发,默认当然是
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 判断一下是不是ACTION_DOWN,如果是的话,代表一个新的事件序列来临了
if (actionMasked == MotionEvent.ACTION_DOWN) {
//要注意一下这两个方法,在这里会做一下相当于是“清零”的操作
//在这里包含了诸如mFirstTouchTarget=null这样的初始化操作
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// intercepted是用来记录是否被拦截的结果
final boolean intercepted;
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 {
// 没有mFirstTouchTarget,同时事件为非ACTION_DOWN,那么就算要在这里拦截了
intercepted = true;
}
//忽略部分拦截相关的代码
//这两个对象记一下,后面会碰到
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 这里就开始对事件类型区分了,如果是ACTION_DOWN,那么就算是一个新的事件序列开始
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 准备一下,接下来开始遍历自己的子View们
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 获取到点击的坐标,用来从子View中筛选出点击到的VIEW
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 按从后向前的顺序开始遍历子View们
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);
// 其实筛选只是将不合适的View们过滤掉
//一个一个continue就表示在发现View不合适的时候直接进入下一次循环
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//终于找到了合适的子View,注意这里将子View封装为一个target
//要是返回的结果不为空就跳出循环
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;
}
//就算返回结果为空也没关系,在这里继续递归的调用子View的dispatchTransformedTouchEvent()
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 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;
}
}
if (preorderedList != null) preorderedList.clear();
}
//没有找到要接受事件的View
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//接下来就是对于非ACTION_DOWN事件的分发了,这里有两种情况
if (mFirstTouchTarget == null) {
// 1.压根就没有找到要接受事件的view,或者被拦截了,调用了自身的dispatchTransformedTouchEvent()且穿了一个null的View进去,这样有什么用呢?需要后面分析dispatchTransformedTouchEvent()
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//2.有View接受ACTION_DOWN事件,那么这个View也将接受其余的事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//alreadyDispatchedToNewTouchTarget这个变量在前面View接受ACTION_DOWN事件时设为了true
//同时这个mFirstTouchTarget也就是那个View封装好的target
//那么这个返回值handled就为true
handled = true;
} else {
//对于非ACTION_DOWN事件,依然是递归调用dispatchTransformedTouchEvent
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// 处理ACTION_UP和ACTION_CANCEL
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

接下来看看dispatchTransformedTouchEvent()的源码:

//前面在分析dispatchTouchEvent()的时候发现有多处调用了这个dispatchTransformedTouchEvent(),而且有的地方传来的第三个参数是null
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//处理ACTION_CANCEL
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;
}
//忽略部分代码……
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//如果传来的参数child为空时,调用自身dispatchTouchEvent()
handled = super.dispatchTouchEvent(event);
} else {
//不为空,那么就调用他的dispatchTouchEvent()
handled = child.dispatchTouchEvent(event);
}
return handled;
}
} else {
//...
}
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}

上面是对dispatchTouchEvent()和dispatchTransformedTouchEvent()的分析,看起来有点乱,这里梳理一下:

  • 首先明确一点,事件分发是从ViewGroup的dispatchTouchEvent()开始的ViewGroup在遇到一个新的事件序列,即事件ACTION_DOWN时,开始遍历自己的所有子View,找到需要接收到事件的View无论是否找到,都会调用dispatchTransformedTouchEvent()方法,区别在于如果找到了,那么在这个方法中传入的是那个View,否则就是nulldispatchTransformedTouchEvent()方法中第三个参数child为空时,会调用父类的dispatchTouchEvent()方法,否则会调用那个child的dispatchTouchEvent()方法。总而言之,都会去调用View类的dispatchTouchEvent()方法。dispatchTransformedTouchEvent()方法是进行具体的事件分发,除了OnClick()等事件外,onTouchEvent()方法就是在这里调用的只要找到了要接受事件的View,就会将他封装为一个target,保存起来,后续的其他事件都由他来接受

问题三:OnClick OnLongClick等对外的监听是在哪里处理的?

首先想一想一个很简单的逻辑,OnClick事件是先ACTION_DOWN之后再ACTION_UP,所以必定要在onTouchEvent()处理。同理,OnLongClick是在保持ACTION_DOWN一段时间后发生,因此也要在onTouchEvent()中处理。看看源码,发现果然是在这里:

//以下源码均为忽略了不想关部分,只保留了重点
public boolean onTouchEvent(MotionEvent event) {
//...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 处理click
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// a short period in case this is a scroll.
if (isInScrollingContainer) {
//...
} else {
// 处理longclick
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
//...
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
//...
break;
}
return true;
}
return false;
}

根据前面的分析,在View的dispatchTouchEvent()方法中,会对

public boolean dispatchTouchEvent(MotionEvent event) {
//...
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
//...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//只要获取到的ListenerInfo不为空,就说明我们设置了监听,那么就会认为我们想让这个View处理所有事件
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//所以会在这里执行onTouch()
result = true;
}
//而如果没有处理,那么再调用onTouchEvent(),直到onTouchEvent()也返回false才会认为该View不消费事件
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}

可以看到,在View的dispatchTouchEvent()方法中,会通过查看是否由设置监听器等方法来判断是否要消费事件。onTouchEvent()方法永远会调用,click和longclick都在这里面。而无论内部如何处理,只要返回了true,就会认为消费了这一事件。

分析就到这了,作为一个小菜鸡,分析过程难免有些错误和疏漏,欢迎在评论区讨论~

Android程序员事件分发机制学习笔记的更多相关文章

  1. Android中的事件分发机制

    Android中的事件分发机制 作者:丁明祥 邮箱:2780087178@qq.com 这篇文章这周之内尽量写完 参考资料: Android事件分发机制完全解析,带你从源码的角度彻底理解(上) And ...

  2. Android Touch事件分发机制学习

    Android  事件分发机制 ViewGroup dispatchTouchEvent 返回true dispatchTouchEvent: Activity ACTION_DOWN Myrelat ...

  3. Android view 的事件分发机制

    1 事件的传递顺序是 Activity -> Window -> 顶层View touch 事件产生后,最先由 activity 的 dispatchTouchEvent 处理 /** * ...

  4. Android中的事件分发机制总结

    Android 的事件分发机制 一.View的事件分发总结: View的onTouchEvent和OnTouch区别  还是以自定义的TestButton为例. 我们可以通过重写onTouchEven ...

  5. Android的Touch事件分发机制简单探析

    前言 Android中关于触摸事件的分发传递是一个很值得研究的东西.曾不见你引入了一个ListView的滑动功能,ListView就不听你手指的指唤来滚动了:也不知道为啥Button设置了onClic ...

  6. Android View的事件分发机制

    准备了一阵子,一直想写一篇事件分发的文章总结一下.这个知识点实在是太重要了. 一个应用的布局是丰富的,有TextView,ImageView,Button等.这些子View的外层还有ViewGroup ...

  7. Android View的事件分发机制和滑动冲突解决方案

    这篇文章会先讲Android中View的事件分发机制,然后再介绍Android滑动冲突的形成原因并给出解决方案.因水平有限,讲的不会太过深入,只希望各位看了之后对事件分发机制的流程有个大概的概念,并且 ...

  8. Android View的事件分发机制探索

    概述 Android事件传递机制也是Android系统中比较重要的一块,事件类型有很多种,这里主要讨论TouchEvent的事件在framework层的传递处理机制.因为对于App开发人员来说,理解f ...

  9. 浅谈Android中的事件分发机制

    View事件分发机制的本质就是就是MotionEvent事件的分发过程,即MotionEvent产生后是怎样在View之间传递及处理的. 首先介绍一下什么是MotionEvent.所谓MotionEv ...

随机推荐

  1. 2016年蓝桥别A组模拟训练

    1. 网友年龄 某君新认识一网友. 当问及年龄时,他的网友说: “我的年龄是个2位数,我比儿子大27岁, 如果把我的年龄的两位数字交换位置,刚好就是我儿子的年龄” 请你计算:网友的年龄一共有多少种可能 ...

  2. 动手学深度学习5-softmax回归

    softmax回归 softmax 回归模型 单样本分类的矢量计算表达式 小批量样本分类的矢量计算表达式 交叉熵损失函数 模型预测以及评价 小结 softmax回归 前几节介绍的是线性回归模型适用于输 ...

  3. LeetCode 485:连续最大1的个数 Max Consecutive Ones(python java)

    公众号:爱写bug 给定一个二进制数组, 计算其中最大连续1的个数. Given a binary array, find the maximum number of consecutive 1s i ...

  4. day11——函数名的使用、f格式化、迭代器、递归

    day11 函数名的第一类对象及使用 1.可以当作值被赋值给变量 def func(): print(1) print(func) a = func a() 2.当作元素存放在容器中 def func ...

  5. RDD的转换操作,分三种:单value,双value交互,(k,v)对

    import org.apache.spark.rdd.RDDimport org.apache.spark.{Partitioner, SparkConf, SparkContext} object ...

  6. K8s 学习者绝对不能错过的最全知识图谱(内含 58个知识点链接)

    作者 | 平名 阿里服务端开发技术专家 导读:Kubernetes 作为云原生时代的“操作系统”,熟悉和使用它是每名用户的必备技能.本篇文章概述了容器服务 Kubernetes 的知识图谱,部分内容参 ...

  7. Debian kvm网络配置

    安装brctl apt-get install bridge-utils 设置网桥 可编辑 /etc/network/interface 文件.不过,我建议在 /etc/network/interfa ...

  8. MySQL如何定位慢sql

    MySQL如何定位慢sql MySQL"慢SQL"定位 数据库调优我个人觉得必须要明白两件事 1.定位问题(你得知道问题出在哪里,要不然从哪里调优呢) 2.解决问题(这个没有基本的 ...

  9. 让你的 vs code 跑在云上,用手机浏览器就能写代码

    让你的vs code 跑在云服务器上 在B站上看到一个视频Run VS Code in the browser with massive computing resources(教你如何配置一个云ID ...

  10. 构建简单Windows Service示例

    示例源码:WindowsServiceSample ServiceHelper源码:ServiceHelper 1. 创建Windows Service项目,如图: 2. 配置服务参数 3. 安装,启 ...