目录

  • 引言
  • ACTION_MOVE与ACTION_UP的传递机制
    • mFirstTouchTarget作用
    • mFirstTouchTarget为什么是链表结构

引言

关于Android事件分发机制网上相关的文章很多,多数都是一些较为基础并且重复的内容。本系列将从源码带领大家探究一些事件分发机制的“细枝末节”。但是在此之前,还是简单重复一下基础内容。即事件分发的三个重要方法:
事件传递给当前view时,dispatchTouchEvent方法会被调用。在方法内部会判断是否拦截事件onInterceptTouchEvent及如何处理事件onTouchEvent
一个完整的事件序列以Down开始,中间经过一个或者多个Move,最后以Up结束。
用一张图来总结ViewGroup的Down事件传递机制:

True
True
Flase
True
False
False
True
False
dispatchTouchEvent
onInterceptTouchEvent
mOnTouchListener.onTouch
End
子view dispatchTouchEvent
End
onTouchEvent
End
上层view或者Activity处理

事件Down传递给当前ViewGroup时,首先回调用dispatchTouchEvent方法,该方法内部会通过onInterceptTouchEvent方法判断是否拦截该Down事件。

如果拦截,则会首先判断ViewGroup是否设置了mOnTouchListener并且onTouch方法是否返回true,如果满足,则该事件处理结束。如果不满足,则会交给ViewGroup的onTouchEvent来处理,如果onTouchEvent返回true,则该事件处理结束;如果返回false,表示当前ViewGroup无法处理该事件,那么该事件回传递给上层View或者Activity来处理。
如果不拦截,则事件会交给子View来处理,回调用子View的dispatchTouchEvent方法,在子View的dispatchTouchEvent方法内部,也会判断是否设置了mOnTouchListener并且onTouch方法是否返回true,如果满足,则该事件处理结束。如果不满足,则会交给View的onTouchEvent来处理,如果onTouchEvent返回true,则该事件处理结束;如果返回False,则会继续走到父View的mOnTouchListener.onTouch判断逻辑中。

以上说的是Down事件的传递机制,我们知道如果一个View处理的Down事件,那么Move和Up事件也会自动交给它处理,那么这一过程是如何实现的呢?

ACTION_MOVE与ACTION_UP的传递机制

mFirstTouchTarget作用

在ViewGroup源码中使用了一个全局变量mFirstTouchTarget来记录是否有View处理了Down事件。mFirstTouchTarget默认为null,如果发现了View可以处理,那么就会把mFirstTouchTarget的值设置为对应的View。那么随之而来的Down和Up都会交给该View处理。
下面通过源码来说明:

首先看一下mFirstTouchTarget的赋值:

/*** Adds a touch target for specified child to the beginning of the list.* Assumes the target child is not already present.*/private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);target.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;}

上面的这段代码可以看出这是一个单链表的插入操作,将mFirstTouchTarget插入到链表的队头并且返回。再来看下这个方法的调用:

    @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {// 代码省略,主要是判断当前事件是否被cancel和interceptedif (!canceled && !intercepted) {// 处理Down事件if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {final int actionIndex = ev.getActionIndex(); // always 0 for downfinal int childrenCount = mChildrenCount;if (newTouchTarget == null && childrenCount != 0) {final float x = ev.getX(actionIndex);final float y = ev.getY(actionIndex);// Find a child that can receive the event.// Scan children from front to back.final ArrayList<View> preorderedList = buildTouchDispatchChildList();final boolean customOrder = preorderedList == null&& isChildrenDrawingOrderEnabled();final View[] children = mChildren;// 遍历每一个子viewfor (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);//... 省略部分代码// 找到可以处理该事件的viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 省略部分代码newTouchTarget = addTouchTarget(child, idBitsToAssign);    // 重点在这里!!!!将可以处理该事件的view设置为mFirstTouchTargetbreak;}// The accessibility focus didn't handle the event, so clear// the flag and do a normal dispatch to all children.ev.setTargetAccessibilityFocus(false);}if (preorderedList != null) preorderedList.clear();}// 略部分代码}}}// 省略部分代码。。。return handled;}

通过上面的代码可以看到,如果当前是Down事件,而且没有被拦截或者取消的话,就会遍历这个ViewGroup的children,找到可以处理事件的view,并且添加到mFirstTouchTarget单链表中。也就是说,mFirstTouchTarget单链表中存储的view是可以处理该Down事件的子view。

那么当Move事件及Up事件来的时候,又是如何根据mFirstTouchTarget的值来进行分发的呢?
继续看这部分源码:

@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {// 省略部分代码boolean handled = false;if (onFilterTouchEventForSecurity(ev)) {final int action = ev.getAction();final int actionMasked = action & MotionEvent.ACTION_MASK;// 省略部分代码// Check for interception.final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {// 重点1   mFirstTouchTarget != null时 会走这个iffinal 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;}// If intercepted, start normal event dispatch. Also if there is already// a view that is handling the gesture, do normal event dispatch.if (intercepted || mFirstTouchTarget != null) {ev.setTargetAccessibilityFocus(false);}// Check for cancelation.final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;// Update list of touch targets for pointer down, if needed.final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;TouchTarget newTouchTarget = null;boolean alreadyDispatchedToNewTouchTarget = false;// Dispatch to touch targets.if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {// 重点2 mFirstTouchTarget != null会走到这里// Dispatch to touch targets, excluding the new touch target if we already// dispatched to it.  Cancel touch targets if necessary.TouchTarget predecessor = null;TouchTarget target = mFirstTouchTarget;while (target != null) {final TouchTarget next = target.next;if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {handled = true;} else {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;}}}return handled;}

上面的代码部分一共标注了两处重点。在重点一的地方,我们看到ViewGroup仍然可能会通过onInterceptTouchEvent方法对事件进行拦截。假设ViewGroup没有进行拦截。那么在重点二的地方,就会遍历mFirstTouchTarget链表中的节点,并且将事件分发给对应的view,但是注意的是此时分发的不一定的Move或者Up事件,有可能是Cancel事件,详细可以查看Android事件分发之ACTION_CANCEL机制及作用。

mFirstTouchTarget为什么是链表结构

在上一小节可以看到,mFirstTouchTarget指向了可以处理事件的子view,但是直观上来说能够处理的子view只有一个,为什么会是一个链式结构呢?我们再通过源码看下mFirstTouchTarget的插入时机。

 if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// 。。。newTouchTarget = addTouchTarget(child, idBitsToAssign);}

我们可以看到,在三种情况下,可能会走到addTouchTarget方法中。分别看下这三种情况:

1.actionMasked == MotionEvent.ACTION_DOWN,这个就是我们所说的Down事件。
2.actionMasked == MotionEvent.ACTION_HOVER_MOVE,这个是用于监听鼠标移动的事件。暂时忽略。
3.split && actionMasked == MotionEvent.ACTION_POINTER_DOWN,重点来看下这个。MotionEvent.ACTION_POINTER_DOWN出现在多指触控时。第一根按下的手指触发ACTION_DOWN事件,之后按下的手指触发ACTION_POINTER_DOWN事件。
所以当有多指进行触控的时候,addTouchTarget方法可能会被调用多次,mFirstTouchTarget以链式结构存储对应的view。
下面我们用代码来验证一下我们的结论:


import android.content.Context
import android.support.constraint.ConstraintLayout
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import java.lang.reflect.Fieldclass CustomViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {// Log.d("TAG", "${ev?.action}:CustomViewGroup onInterceptTouchEvent")return false}override fun onTouchEvent(event: MotionEvent?): Boolean {// Log.d("TAG", "${event?.action}:CustomViewGroup onTouchEvent")return true}override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {super.dispatchTouchEvent(ev)ev?.let {if (it.action == MotionEvent.ACTION_DOWN) {val count = getField(this, this.javaClass, "mFirstTouchTarget")Log.d("TAG", "MotionEvent.ACTION_DOWN-->mFirstTouchTarget count:$count")}if ((it.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {val count = getField(this, this.javaClass, "mFirstTouchTarget")Log.d("TAG", "MotionEvent.ACTION_POINTER_DOWN-->mFirstTouchTarget count:$count")}}return true}}fun getField(obj: Any,clazz: Class<*>, fieldName: String
): Int? {if (clazz.superclass == null) {return null}var field: Field? = nulltry {field = clazz.getDeclaredField(fieldName)} catch (e: NoSuchFieldException) {return getField(obj, clazz.superclass, fieldName)}var nodeCount = 0val declaredClasses = clazz.declaredClassesdeclaredClasses.iterator().forEach {if (it.simpleName == "TouchTarget") {field.isAccessible = truevar mTouchTarget = field.get(obj)var next = it.getDeclaredField("next")while (mTouchTarget != null) {nodeCount++mTouchTarget = next.get(mTouchTarget)}}}return nodeCount
}

上面是一段kotlin代码。实现了一个自定义ViewGroup,继承于ConstraintLayout,在覆盖的dispatchTouchEvent方法中,判断当前的事件是MotionEvent.ACTION_DOWN或者MotionEvent.ACTION_POINTER_DOWN时,通过反射拿到ViewGroup中"mFirstTouchTarget"属性对应的链表中节点的数量。

我们看下布局文件:

<?xml version="1.0" encoding="utf-8"?>
<com.lee.myapplication.CustomViewGroupxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/parent"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:layout_width="160dp"android:layout_height="126dp"android:text="button1"android:gravity="center"android:textColor="#FFF"android:background="#8cFF0000"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"android:id="@+id/button1"android:layout_marginBottom="8dp" app:layout_constraintBottom_toTopOf="@+id/button2"/><Buttonandroid:layout_width="159dp"android:layout_height="142dp"android:text="button2"android:gravity="center"android:textColor="#FFF"android:background="#8c00FF00"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"android:id="@+id/button2"/><Buttonandroid:layout_width="159dp"android:layout_height="142dp"android:text="button3"android:gravity="center"android:textColor="#FFF"android:background="#8c0000FF"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"android:id="@+id/button3"android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/button2"/></com.lee.myapplication.CustomViewGroup>

其实就是我们自定义的ViewGroup中有三个Button,效果图如下:


假设我们现在点击button1,查看Log:

04-23 13:51:30.017 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN–>mFirstTouchTarget count:1

假设我们先点击了button1,然后手指不松开,又点击了button2:

04-23 13:52:55.599 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN–>mFirstTouchTarget count:1
04-23 13:52:56.217 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_POINTER_DOWN–>mFirstTouchTarget count:2

假设我们先点击了button1,手指不松开又点击了button2,手指不松开最后点击了button3:

04-23 13:53:56.316 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN–>mFirstTouchTarget count:1
04-23 13:53:56.763 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_POINTER_DOWN–>mFirstTouchTarget count:2
04-23 13:53:57.292 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_POINTER_DOWN–>mFirstTouchTarget count:3

通过上面的结论,验证了"mFirstTouchTarget"的链式结构。

Android事件分发之ACTION_MOVE与ACTION_UP的传递机制相关推荐

  1. Android事件分发之ACTION_CANCEL机制及作用

    目录 ACTION_CANCEL产生场景 ACTION_CANCEL作用 FLAG_DISALLOW_INTERCEPT的作用 如果要查看ACTION_MOVE与ACTION_UP的事件传递机制,查看 ...

  2. Android事件分发之ViewGroup篇 -- ViewGroup的dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent之间关系

    Android事件分发之ViewGroup篇(FatherViewGroup) – ViewGroup的dispatchTouchEvent.onTouchEvent.onInterceptTouch ...

  3. Android 事件分发面试题2

    问:LinearLayout 里面嵌套Button,LinearLayout 设置了onClick 事件,但是Button 没有设置点击事件,当我点击Button 的时候,LinearLayout 的 ...

  4. 图解Android事件传递之ViewGroup篇

    本篇文章主要讲述ViewGroup中关于触摸事件传递的相关逻辑.主要梳理一下dispatchTouchEvent函数. 一些知识点 FLAG_DISALLOW_INTERCEPT,可以使用reques ...

  5. Android onTouch事件传递机制

    Android onTouch事件介绍: Android的触摸事件:onClick, onScroll, onFling等等,都是由许多个Touch组成的.其中Touch的第一个状态肯定是ACTION ...

  6. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 七 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  7. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 六 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  8. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 五 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

  9. 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )

    Android 事件分发 系列文章目录 [Android 事件分发]事件分发源码分析 ( 驱动层通过中断传递事件 | WindowManagerService 向 View 层传递事件 ) [Andr ...

最新文章

  1. 2021-2027年中国中高端女装行业市场分析预测及投资方向研究报告
  2. 2019年,自动化机器学习AutoML技术还火吗? | BDTC 2019
  3. php 编译安装降解,对php编译安装的修正
  4. PHP遇到json解决的两个办法,转为数组,直接取值
  5. Oracle 中文分词CHINESE_VGRAM_LEXER与CHINESE_LEXER比较
  6. Android——Fragment介绍
  7. css实现3D立方体旋转特效
  8. 从底层看云:云计算准备好了么?
  9. 算法-递归求1-n的阶乘
  10. MyBatis-3.4.2-源码分析12:XML解析之mapperElement(root.evalNode(mappers))
  11. SPSS操作(五):主成分分析
  12. 计算机左侧没有桌面菜单栏,我的电脑左侧工具栏忽然不见了
  13. 专访架构师周爱民:谈企业软件架构设计
  14. 文件后缀bat是什么?(批处理文件)
  15. ES6, ES7, ES8, ES9 以及 ES10 新特征
  16. 舌苔发白是什么原因造成的?
  17. 解决:the account is locked 被锁
  18. 【Java】一次简单实验经历——社交网络图的简化实现
  19. 博通收购 VMware 计划受阻,英国监管机构介入
  20. 一般的Java项目需要JVM调优吗?

热门文章

  1. dp(八)买卖股票的最好时机 (一,二、三)
  2. 大专计算机专业可以在职研究生吗,专科可以报考西安邮电大学计算机在职研究生吗?...
  3. good things
  4. Bigger is Better
  5. crm客户管理系统培养客户忠诚度
  6. Qt 集成miniblink浏览器库之1编译使用
  7. 基于Miniblink的WebBrowser控件CXMBWebCtrl及简单的浏览器Demo
  8. miniblink 浏览器 源码阅读 下载
  9. 云虚拟主机FTP连接不上的解决办法
  10. DVWA上XSS(DOM)(基于 DOM 的跨站脚本)全难度