Android系统分析之带着问题看事件分发机制

Android 触摸事件分发机制?

Android系统分析之带着问题看事件分发机制

一 事件分发机制

1 什么是事件分发机制?

1.1 什么是事件?

答:当用户触摸屏幕时,每一次的点击,按下,移动,抬起等都是一个事件。事件类型包括:按下(ACTION_DOWN),移动(ACTION_MOVE),抬起(ACTION_UP),取消(ACTION_CANCEL)。

1.2 什么是事件分发机制呢?

答:因为 Android 每一个页面都是基于 Activity 进行实现的,一个 Activity 里面有若干个 ViewGroup 和 View 组成的;而事件分发机制就是某一事件从屏幕传递到各个具体的 View,由某个 View 来使用这一事件 (消费事件) 或者忽略这一事件(不消费事件),这整个过程

1.3 什么是事件分发的对象?

答:系统会把整个事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)封装为MotionEvent 对象,事件分发的过程就是MotionEvent 对象分发的过程

1.4 什么是事件序列?

答:从手指按下屏幕开始,到手指离开屏幕为止所产生的一系列事件。也就是说一个完整的事件序列是以 ACTION_DOWN 事件开始,ACTION_UP 事件结束,中间有无数的 ACTION_MOVE 事件(当然可以没有,那就时一个点击事件),如图:

在这里插入图片描述

2 事件分发机制的原因和场景?

2.1 为什么会有事件分发机制?

答:Android 的布局结构是树形结构,这就会导致一些 View 可能会重叠在一起,当我们手指点击区域在多个布局范围之内,此时会有多个布局可以响应点击事件,这时候该让哪个 view 来响应点击事件呢?这就是事件分发机制存在的意义。

2.2 事件分发运用在哪里?

答:(1)自定义 View;(2)解决项目中各种事件冲突问题:例如,ViewPager 与 TabLayout 打造头尾循环的 ViewPager;ListView 的 Item 点击事件为什么和 Button 会冲突呢?

3 事件最先是从哪里来的?

3.1 事件最先是分发给 Activity 的吗?

答:不是。当事件由硬件传递到 Framework 层时,最先是由 WMS 通过 ViewRootImpl 将事件分发给 ViewRootImpl 的属性 mView,也就是 DecorView当触摸事件到达 DecorView 后,先经过 Activity 分发后,又回到 DecorView 中,再一步步传递到内部的子 View 中的
-–> 详细分析请看我的语雀文章:Android Activity 显示原 (Window/DecorView/ViewRoot)?

在这里插入图片描述

每日一问 | 事件到底是先到 DecorView 还是先到 Window 的?

Input 系统—事件处理全过程

二 Activity 事件分发

1 Activity 事件分发的思想?

答:当事件开始触发时,会调用 Activity 的 dispatchTouchEvent() 方法,然后调用 Window 的 superDispatchTouchEvent() 方法,将触摸事件分发给继承自 FrameLayout 的实例 DecorView。

如果 ViewGroup 捕捉了该事件并返回 true,代表事件传递到了 ViewGroup 中;否则调用 Activity 本身的 onTouchEvent() 方法捕捉该事件。

1.1 画一个 Activity 事件分发的流程图?

在这里插入图片描述

1.2 通过源码分析下 Activity 事件分发的流程?

答:当事件开始触发时,会调用 dispatchTouchEvent() 方法,那看下对应的源码:
在这里插入图片描述

1.3 步骤 1 ACTION_DOWN 代表事件序列的开始

答:ACTION_DOWN 代表事件序列的开始,所以这里基本是 true。进入onUserInteraction() 这个方法,在源码中该方法为空方法。所以说当我们可以重写 onUserInteraction() 方法就可以达到监听整个事件序列的开始

public void onUserInteraction() {}
1.4 步骤 2 将触摸事件分发给 DecorView

答:调用了 Window 的 superDispatchTouchEvent() 方法,因为 Window 的唯一的实现类 PhoneWindow 类,它持有了一个继承自 FrameLayout 的实例 DecorView,实质调用了 DecorView 的 dispatchTouchEvent 方法,最后调用了 ViewGroup 的 dispatchTouchEvent 方法

如果 ViewGroup 捕捉了该事件并返回 true,代表事件传递到了 ViewGroup 中;否则调用 Activity 本身的 onTouchEvent() 方法捕捉该事件,并且直接返回 onTouchEvent 的结果。源码如下:

// Window类是抽象类,且PhoneWindow是Window类的唯一实现类
// superDispatchTouchEvent(ev)是抽象方法,返回的是一个Window对象
public abstract boolean superDispatchTouchEvent(MotionEvent event);
// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) { 	
	// PhoneWindow 将事件直接传递给了 DecorView
	return mDecor.superDispatchTouchEvent(event);
}
// DecorView.java Activity最顶层的View
public boolean superDispatchTouchEvent(MotionEvent event) {
	// super.dispatchTouchEvent(event)方法,其实就应该是 ViewGroup 的 dispatchTouchEvent()
	return super.dispatchTouchEvent(event);
}
1.5 步骤 3:如果 ViewGroup 没有捕捉事件,就调用 Activity 本身的 onTouchEvent() 方法捕捉该事件

答:如果 ViewGroup 没有捕捉事件,就调用 Activity 本身的 onTouchEvent() 方法捕捉该事件,并且直接返回 onTouchEvent 的结果。无论返回值是什么,事件都要结束。

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

如果 mWindow.shouldCloseOnTouch(this, event) 返回结果为true,就将该 Activity finish 掉,并且返回 true;否则为 false

三 ViewGroup 事件分发

1 ViewGroup 事件分发的思想?

答:ViewGroup 是一组 View 的组合,在其内部有可能包含多个子 View,当手指触摸屏幕上时,手指所在的区域既能在 ViewGroup 显示范围内,也可能在其内部 View 控件上。

因此它内部的事件分发的重心是:处理当前 ViewGroup 和子 View 之间的逻辑关系:

  1. 检查当前 ViewGroup 是否需要拦截触摸事件?
  2. 是否需要将触摸事件继续分发给子 View?
  3. 根据 mFirstTouchTarget,将触摸事件重新分发给子 View?
1.1 画一个 ViewGroup 事件分发的流程图?

在这里插入图片描述

1.2 事件分发核心 dispatchTouchEvent

**
整个 View体系 之间的事件分发,实质上 dispatchTouchEvent 方法 就是一个大的递归函数。在这个递归的过程中会适时调用 onInterceptTouchEvent 来拦截事件,或者调用 onTouchEvent 方法来处理事件。

先从宏观角度,纵览整个 dispatch 的源码如下:
在这里插入图片描述

如代码中的注释,dispatch 主要分为 3 大步骤:

  • 步骤 1:判断当前 ViewGroup 是否需要拦截此触摸事件,如果拦截则此触摸事件不再会传递给子 View或者以 CANCEL 的方式通知子 View)。
  • 步骤 2:如果没有拦截,则将事件分发给子 View 继续处理。如果子 View 捕获了此次事件,则将捕获触摸事件的子 View 赋值给 mFirstTouchTarget
  • 步骤 3:根据 mFirstTouchTarget,将触摸事件重新分发给子 View
1.3 步骤 1 判断当前 ViewGroup 是否需要拦截此触摸事件,如果拦截则此触摸事件不再会传递给子 View

在这里插入图片描述

图中红框标出了是否需要拦截的条件

  • 如果事件为 DOWN 事件,则调用 onInterceptTouchEvent 进行拦截判断
  • 或者 mFirstTouchTarget 不为 null(事件类型是:后续的 MOVE、UP 等事件),代表已经有子 View 捕获了这个事件,则调用 onInterceptTouchEvent 进行拦截判断。(这也是,子 View 对触摸事件进行了捕获,则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的子 View 进行处理的原因。)

注意:如果不进行拦截判断,直接就判断为需要拦截,intercepted = true。

如果当前 ViewGroup 并没有对事件进行拦截,则执行步骤 2;如果进行了拦截,进入步骤 3:child 为 null,最终会调用 super.dispatchTouchEvent 方法,实际上最终会调用自身的 onTouchEvent 方法,捕捉触摸事件。

1.4 步骤 2 如果没有拦截,则将事件分发给子 View 继续处理。如果子 View 捕获了此次事件,则将捕获触摸事件的子 View 赋值给 mFirstTouchTarget

在这里插入图片描述

仔细看上述的代码可以看出:

  • 图中 ① 处表明事件主动分发的前提是事件为 DOWN 事件
  • 图中 ② 处遍历所有子 View;
  • 图中 ③ 处判断事件坐标是否在子 View 坐标范围内,并且子 View 并没有处在动画状态;
  • 图中 ④ 处调用 dispatchTransformedTouchEvent 方法将事件分发给子 View,如果返回 true 就是代表子 View 捕获事件成功,则将捕获事件的子 View 赋值给 mFirstTouchTarget 保存,如下图。
    在这里插入图片描述
1.5 步骤 3 根据 mFirstTouchTarget,将触摸事件重新分发给子 View

在这里插入图片描述

步骤 3 有 2 个分支判断。

  • 分支 1:如果此时 mFirstTouchTarget 为 null,说明在上述的事件分发中并没有子 View 对事件进行了捕获操作

    • 这种情况下,直接调用 dispatchTransformedTouchEvent 方法分发转换后的触摸事件,并传入 child 为 null,最终会调用 super.dispatchTouchEvent 方法,实际上最终会调用自身的 onTouchEvent 方法,捕捉触摸事件。自身 由父类实现 的 dispatchTouchEvent 返回 true 就是代表捕获触摸事件。
    • 也就是说:如果没有子 View 捕获处理触摸事件,ViewGroup 会通过自身的 onTouchEvent 方法进行处理。
  • 分支 2:mFirstTouchTarget 不为 null,说明在上面步骤 2 中有子 View 对触摸事件进行了捕获,则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的子 View 进行处理

    • 这种情况下,直接调用 dispatchTransformedTouchEvent 方法分发转换后的触摸事件,并传入 child 为 target.child,最终会调用 child.dispatchTouchEvent 方法,实际上最终会调用子 View 的 onTouchEvent 方法,捕捉触摸事件。子 View 的 dispatchTouchEvent 返回 true 就是代表捕获触摸事件。
    • dispatchTransformedTouchEvent 方法中本步骤涉及到的部分,如下图:
      在这里插入图片描述

2 ViewGroup 的问题合集

2.1 mFirstTouchTarget 有什么作用

答:mFirstTouchTarget 的部分源码如下:

在这里插入图片描述

可以看出其实 mFirstTouchTarget 是一个 TouchTarget 类型的链表结构。而这个 TouchTarget 的作用就是用来保存捕获了 DOWN 事件的 View,具体保存在上图中的 child 变量。在 1.5 步骤 3 中判断如果 mFirstTouchTarget 不为 null,则再次将事件分发给相应的 TouchTarget

可是为什么是链表类型的结构呢?因为 Android 设备是支持多指操作的,每一个手指的 DOWN 事件都可以当做一个 TouchTarget 保存起来

2.2 为什么 DOWN 事件特殊?

答:所有 触摸 事件都是从 DOWN 事件开始的,这是 DOWN 事件比较特殊的原因之一。另一个原因是 DOWN 事件的处理结果会直接影响后续 MOVE、UP 事件的逻辑

在步骤 2 和 3 中,只有 DOWN 事件会传递给子 View 进行捕获判断,一旦子 View 捕获成功,后续的 MOVE 和 UP 事件是通过遍历 mFirstTouchTarget 链表,查找之前接受 ACTION_DOWN 的子 View,并将触摸事件分配给这些子 View。也就是说后续的 MOVE、UP 等事件的分发交给谁,取决于它们的起始事件 Down 是由谁捕获的。
**

2.4 如果要传递事件到子 View,为什么父容器不能拦截 ACTION_DOWN 事件,父容器必须返回 false?

答:父容器拦截了 ACTION_DOWN,那么后续的 MOVE UP 等事件均不会再调用 onInterceptTouchEvent() 方法,都会直接交给父容器处理,这时事件没法再传递给子 View 了

2.5 如果子 View 需要实现点击事件,为什么父容器不能拦截 ACTION_UP 事件,父容器必须返回 false?

答:如果拦截掉了 ACTION_UP,肯定会导致子 View 的点击事件无法被处理,因为一个点击事件从 ACTION_DOWN 开始,从 ACTION_UP 结束,二者缺一不可,最终在 ACTION_UP 事件中实现

2.6 如果遍历所有的子 View 后事件都没有被合适的处理,有哪两种情况?

答:1.是ViewGroup 没有子 View;2.是子 View 处理了点击事件,但是因为子 View 在 onTouchEvent 返回了 false,然后在 dispatchTouchEvent 中也返回 false。这两种情况下,ViewGroup 都会自己处理点击事件

四 View 事件分发

1 View 事件分发的思想?

答:View 是一个单纯的控件,不能再被细分,内部也并不会存在子 View,所以它的事件分发的重点在于:当前 View 如何去处理 触摸 事件,并根据相应的手势逻辑进行一些列的效果展示(比如滑动,放大,点击,长按等)

  1. 是否存在 TouchListener
  2. onTouchEvent 方法中判断:子 View 是否接收并处理触摸事件
1.1 画一个 View 事件分发的流程图?

在这里插入图片描述

1.2 事件分发核心 dispatchTouchEvent

整个 View 的事件分发,实质上 dispatchTouchEvent 方法 就是一个大的递归函数。在这个递归的过程中会适时调用 onTouchEvent 方法来处理事件。

先从宏观角度,纵览整个 dispatch 的源码如下:
在这里插入图片描述
如代码中的注释,dispatch 主要分为 3 大步骤:

  • 步骤 1:判断该 View 是否有相应焦点,有没被其他 View 遮挡,能否被触摸。
  • 步骤 2:是否存在 TouchListener.onTouch
  • 步骤 3:onTouchEvent 方法中判断:子 View 是否接收并处理触摸事件
1.3 步骤 1 判断该 View 是否有相应焦点,有没被其他 View 遮挡,能否被触摸

答:判断该 View 是否有相应焦点,有焦点才进行后面的事件分发

if (event.isTargetAccessibilityFocus()) {
    if (!isAccessibilityFocusedViewOrHost()) {
        return false;
    }
    event.setTargetAccessibilityFocus(false);
}

检查安全策略,判断当前 View 有没被其他 View 遮挡,能否被触摸。如果当前 View 没被其他 View 遮挡、能被触摸,才进行后面的事件分发。

if (onFilterTouchEventForSecurity(event)) {}
1.4 步骤 2 是否存在 TouchListener.onTouch -->请看 五-2-2.2

答:判断当前的操控方式是不是为鼠标操作,如果是鼠标操作,直接返回值 ture,表示了捕捉该事件。然后 –>请看 五-2-2.2
![image.png](https://img-blog.csdnimg.cn/img_convert/c08707682edb664fc9a57810107de2c4.png#align=left&display=inline&height=305&margin=[object Object]&name=image.png&originHeight=305&originWidth=504&size=18535&status=done&style=none&width=504)

1.5 步骤 3 onTouchEvent 方法中判断:子 View 是否接收并处理触摸事件?

答:

  • 结论1:可点击的禁用(DISABLED)视图仍会捕捉触摸事件,只是不响应事件
  • 结论2:可点击状态下,才会响应点击事件。在 MOVE 事件中执行 长按事件,在 UP 事件中执行 点击事件。因为 MOVE 事件先于 UP 事件,因此 长按事件 先于 点击事件 执行。而且只有不响应 长按事件,才会响应 点击事件
  • 结论3:点击事件执行时机,onTouch() --> onTouchEvent() --> onLongClick() --> onClick()。
// View.java
public boolean onTouchEvent(MotionEvent event) {
        // 核心1 --> 可点击 且 处于一个禁用状态
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 可点击的禁用(DISABLED)视图仍会捕捉触摸事件,只是不响应事件
            return clickable;
        }
        
        // 核心2 --> 可点击状态下,才会响应点击事件
		if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                   // 如果没响应长按事件,才会响应点击事件
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    	// performClick(); 
	                	li.mOnClickListener.onClick(View.this);   
                    }
                case MotionEvent.ACTION_DOWN/ACTION_CANCEL:
                    ...
                case MotionEvent.ACTION_MOVE:
                    // MOVE 事件先于 UP 事件,因此 长按事件 先于 点击事件 执行
                    // hasPendingLongPressCallback() --> performLongClick()
                    li.mOnLongClickListener.onLongClick(View.this)
                break;
            }
            // 捕获事件
            return true;
        }
        // 不捕获事件
        return false;
    }
}

五 验证事件分发机制

1 事件分发流程代码演示?

1.1 布局和自定义View

(1)定义如下布局文件:(设置 MyView 可点击)
在这里插入图片描述

(2)MyViewGroup 和 MyView 是两个自定义 View,以及 Actvitiy,它们的源码分别如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)页面效果如下:
在这里插入图片描述

1.2 子 View 的 onTouchEvent 返回 false,表示不对触摸事件进行捕获

(1)MyView 的 onTouchEvent 返回 false,表示 View 没捕获事件
在这里插入图片描述
(2)点击:按下->抬起 (递归思想)
在这里插入图片描述
(3)滑动:按下->滑动->抬起
在这里插入图片描述

(4)结论:上图中在 DOWN 事件中,MyViewGroup 的 onInterceptTouchEvent 被触发了;然后在子 View 的 dispatchTouchEvent 中返回 false,代表它没捕获这个 DOWN 事件,事件层层回传,最后在最顶层的 Activity 处理。这种情况下 子 View 没被添加到 父容器(MyViewGroup) 中的 mFirstTouchTarget 中,因此当前以及后续的 MOVE 和 UP 事件不再进行拦截判断,都会直接交给 Activity 进行处理。

1.3 子 View 的 onTouchEvent 返回 true,表示对触摸事件进行捕获

(1)MyView 的 onTouchEvent 返回 true,表示 View 捕获了事件
在这里插入图片描述

(2)滑动:按下->滑动->抬起
在这里插入图片描述

(3)结论:上图中在 DOWN 事件中,MyViewGroup 的 onInterceptTouchEvent 被触发了;然后在子 View 的 dispatchTouchEvent 中返回 true,代表它捕获了这个 DOWN 事件。这种情况下 **子 View 会被添加到 父容器(MyViewGroup) 中的 mFirstTouchTarget 中,因此当前以及后续的 MOVE 和 UP 事件都会经过 MyViewGroup 的 onInterceptTouchEvent 进行拦截判断,最后全都交给子 View(mFirstTouchTarget 指向的子 View) 进行处理。

1.4 总结下 onTouchEvent 返回值的作用:返回 false/true 对事件分发的影响?

答:onTouchEvent() 用于处理事件,返回值决定子 View 是否捕获了这个事件,而且决定了子 View 的 dispatchTouchEvent 的返回值。。。也就是说,子 View 在处理完触摸事件后,是否还允许触摸事件继续向上(父容器)传递。如果返回 true,则父容器不用操心,子 View 自己处理触摸事件;返回 false,则向上传递给父容器

2 基于 1 的代码演示,验证事件的其他情况?

2.1 onTouchEvent 可点击时(clickable 或 longClickable 其一为true)返回true,否则返回false

(1)定义如下布局文件:
在这里插入图片描述
**(2)MyView 继承 View 或 TextView,返回默认的 onTouchEvent **
继承 View 或 TextView:
在这里插入图片描述

结果:
在这里插入图片描述
结论: View 和 TextView 等的 clickable 默认为 false。而且 longClickable 默认为 false。

**(3)MyView 继承 View 或 TextView,同时设置 setOnClickListener ,返回默认的 onTouchEvent **
在这里插入图片描述
设置 onClickListener

在这里插入图片描述
在这里插入图片描述
setOnClickListener 源码
在这里插入图片描述

结论: View 和 TextView 设置 setOnClickListener 后把 clickable 修改为 true

**(4)MyView 继承 Button,返回默认的 onTouchEvent **
在这里插入图片描述
在这里插入图片描述
结论: Button 等的 clickable 默认为 true。而且 longClickable 默认为 false。

2.2 setOnTouchListener 的作用是什么?onTouch 返回 false/true 对执行 onTouchEvent 的影响?

(1)定义如下布局文件:
在这里插入图片描述
(2)onTouch 返回 false
在这里插入图片描述
结果:
在这里插入图片描述
结论:onTouch 方法返回 false,表示没捕捉了事件,继续传递事件,会调用 View 的 onTouchEvent 方法

(3)onTouch 返回 true

在这里插入图片描述
结果:
在这里插入图片描述
结论: onTouch 方法返回 true,表示捕捉了事件,不再继续传递事件,不会调用 View 的 onTouchEvent 方法

(4)原因:
如果 设置了 setOnTouchListener 且 onTouch 方法返回 true,则 result 为 true,那么 !result 值为 false,表示捕捉了事件,不会调用 View 的 onTouchEvent 方法;同理,没设置 setOnTouchListener 或者 onTouch 方法返回 false,…,会调用 View 的 onTouchEvent 方法。

在这里插入图片描述

3 容易被遗漏的 CANCEL 事件

3.1 原因分析

(1)在上面的步骤 3 中,继续向子 View 分发事件的代码中,有一段比较有趣的逻辑:

在这里插入图片描述

上图红框中表明已经有子 View 捕获了触摸事件,但是蓝色框中的 intercepted 变量又是 true。这种情况下,事件主导权会重新回到父视图 ViewGroup 中,并传递给子 View 的分发事件中传入一个 cancelChild 值是 true

(2)看一下 dispatchTransformedTouchEvent 方法的部分源码如下:

在这里插入图片描述
因为之前传入参数 cancel 为 true,并且 child 不为 null,最终这个事件会被包装为一个 ACTION_CANCEL 事件传给 child

3.2 什么情况下会触发这段逻辑呢?

在 DOWN 事件中,当父容器的 onInterceptTouchEvent 先返回 false,将事件分发给子 View,然后在子 View 的 dispatchTouchEvent 返回 true,表示子 View 捕获了事件。关键步骤就是在接下来的 MOVE 事件的过程中,父容器的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的触摸事件

3.3 实际上有个很经典的例子可以用来演示这种情况

当在 Scrollview 中添加自定义 View 时,ScrollView 默认在 DOWN 事件中并不会进行拦截,onInterceptTouchEvent 先返回 false,事件会被传递给 ScrollView 内的子 View。只有当手指进行滑动并到达一定的距离开始滚动之后,onInterceptTouchEvent 返回 true,并触发 ScrollView 的滚动效果。当 ScrollView 进行滚动的瞬间,内部的子 View 会接收到一个 CANCEL 事件,并丢失触摸焦点

(1)比如以下布局:
在这里插入图片描述

(2)CancelView 和 MyScrollView 各是一个自定义的 View,其源码如下:(CancelView 的 onTouchEvent 返回 true,表示它会将接收到的触摸事件进行捕获消费。)

在这里插入图片描述
在这里插入图片描述
(3)上述代码执行后,当手指点击屏幕时 DOWN 事件会被传递给 CancelView,手指滑动屏幕将 ScrollView 上下滚动,刚开始 MOVE 事件还是由 CancelView 来消费处理,但是当 ScrollView 开始滚动时,CaptureTouchView 会接收一个 CANCEL 事件,并不再接收后续的 触摸 事件。具体打印 log 如下:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(4)结论
因此,我们平时自定义 View 时,尤其是有可能被 ScrollView 或者 ViewPager 嵌套使用的控件,不要遗漏对 CANCEL 事件的处理,否则有可能引起 UI 显示异常。

4 如何用外部拦截法解决滑动冲突?

答:外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要这个事件就拦截给它处理,需要重写 onIterceptTouchEvent 方法,模板如下:

// 自定义ViewGroup.java,继承 ViewGroup
public boolean onInterceptTouchEvent(MotionEvent ev) {
	boolean intercepted = false;
    switch (ev.getAction()){
         // ACTION_DOWN事件,父容器必须返回false,即不拦截ACTION_DOWN事件
         case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
         // ACTION_MOVE事件,根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false
         case MotionEvent.ACTION_MOVE:
            if("父容器的点击事件"){
               intercepted = true;
            } else {
               intercepted = false;
            }
            break;
         // ACTION_UP事件,父容器必须要返回false
         case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
	return intercepted;
}

(1) 常见滑动冲突场景?
答:场景 1,**外部滑动方向与内部滑动方向不一致,比如 ViewPager 中包含 ListView。**场景 2,**外部滑动方向与内部滑动方向一致,比如 ScrollView 中包含 ListView。**场景 3,上面两种情况的嵌套

(2) 滑动冲突处理规则?
答:通过判断是水平滑动还是竖直滑动来判断到底应该谁来拦截事件,可以根据水平和竖直两个方向的距离差或速度差来做判断

(3) 验证 ViewGroup 事件拦截?
答:点击 InterceptView 上,事件没传递到 InterceptView,在 InterceptViewGroup 这一层被拦截,打印如下:

 // InterceptViewGroup这一层被拦截:点击了InterceptView上,事件没传递到InterceptView,打印如下
    InterceptActivity dispatchTouchEvent
    InterceptViewGroup dispatchTouchEvent
    InterceptViewGroup onInterceptTouchEvent                // 拦截
    InterceptViewGroup onTouchEvent MotionEvent.ACTION_DOWN
    InterceptActivity onTouchEvent
    InterceptActivity dispatchTouchEvent
    InterceptActivity onTouchEvent

// InterceptViewGroup这一层不拦截:点击了InterceptView上,事件传递到InterceptView,打印如下
    InterceptActivity dispatchTouchEvent
    InterceptViewGroup dispatchTouchEvent
    InterceptViewGroup onInterceptTouchEvent
    InterceptView dispatchTouchEvent
    InterceptView onTouchEvent
    InterceptViewGroup onTouchEvent MotionEvent.ACTION_DOWN
    InterceptActivity onTouchEvent
    InterceptActivity dispatchTouchEvent
    InterceptActivity onTouchEvent

5 如何用内部拦截法解决滑动冲突?

答:内部拦截法是指父容器不直接拦截任何事件,所有的事件都传递给子 View,由子 View 控制。**如果子 View 要捕捉此事件就直接捕捉掉,否则就交由父容器进行处理。这种方法和 Android 中的事件分发机制不一致,需要配合 getParent().requestDisallowInterceptTouchEvent 方法才能正常工作,**需要重写子 View 的 dispatchTouchEvent 方法,模板如下:

// 自定义View.java
public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);  // 不拦截
                break;
            case MotionEvent.ACTION_MOVE:
                if("父容器的点击事件"){
                    getParent().requestDisallowInterceptTouchEvent(false);  // 拦截
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(event);
    }
5.1 requestDisallowInterceptTouchEvent 方法干扰 ViewGroup 的事件分发的原理是什么?

答:因为在 ViewGroup 的 dispatchTouchEvent 方法中,根据 disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 判断当前事件是否允许被拦截。

**

而 requestDisallowInterceptTouchEvent 方法通过修改标志位 mGroupFlags,使 disallowIntercept 结果变化

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
#前言 之前笔者其实已经写过事件分发机制的文章:[快速理解android事件传递拦截机制概念](http://blog.csdn.net/double2hao/article/details/51541061) 但是,现在看来其实更像是一篇知识概括,多出可能未讲清楚,于是打算重写事件分发,用一篇文章大致讲清楚。 首先,形式上笔者最先思考的是使用源码,此者能从原理上讲解分发机制,比起侃侃而谈好得多。但是源码的复杂往往会让新手产生畏惧难以理解,于是笔者最终还是打算使用实例log来让读者理解android事件分发。 #重要函数 笔者此次主要提及最常用的几个函数: (其间区别看源码很容易理解,此处直接给上结果) **onClick():**这个函数是是View提供给我们的OnClickListener这个接口中的函数,在这里可以自定义对点击事件处理逻辑。会在onTouchEvent()中进行调用。 **onTouch():**这个函数是View提供给我们的OnTouchListener这个接口中的函数,在这里面可以自定义对触摸事件处理逻辑。 **onTouchEvent():**这个函数是view内部的触摸事件处理方式,其间包括获取焦点,调用onClick()等等。 **dispatchTouchEvent():**这个是View事件分发函数,在ViewGroup中进行重写。在View中其间会调用onTouchEvent(),在ViewGroup中其间会调用onInterceptTouchEvent()和onTouchEvent()。 **onInterceptTouchEvent():**这个函数是事件拦截函数,是ViewGroup才有的函数。
Android 事件分发机制是指在用户与Android设备进行交互时,Android系统如何接收并分发这些事件的过程。事件分发机制包括三个阶段:分发、拦截和处理。 1. 分发阶段:事件Android设备的底层硬件驱动程序开始,通过InputEvent分发View层。在View层中,事件分为两类:MotionEvent和KeyEvent。MotionEvent表示触摸事件,包括按下、移动、抬起等操作;KeyEvent表示按键事件,包括按下和抬起。 2. 拦截阶段:在事件分发View层后,会从最上层的View开始进行事件分发,直到有View事件进行拦截。如果有View事件进行了拦截,则事件不会继续向下分发,而是由该View进行处理View是否拦截事件的判断由onInterceptTouchEvent方法完成,如果该方法返回true则表示拦截事件。 3. 处理阶段:如果事件没有被拦截,则会被传递到最底层的View进行处理。在View中,事件处理由onTouchEvent方法完成。如果该方法返回true,则表示事件已经被处理,不再需要继续向下分发;如果返回false,则会继续向上分发直到有View事件进行拦截。 Android事件分发机制的流程如下: ![image.png](attachment:image.png) 需要注意的是,事件分发机制是一个逆向分发的过程,即从底层向上分发,而不是从顶层向下分发。这是因为底层的View需要先处理事件,如果底层的View不拦截事件事件才能向上分发

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值