【Android】View的事件分发机制

文章目录

  • 分发顺序
    • Activity
    • ViewGroup
    • View
  • 协作方法
  • 整体流程
    • 注意
  • Activity事件分发
  • ViewGroup事件分发
  • View点击事件
  • 总结

分发顺序

Activity->ViewGroup->View

Activity

  • 分发事件Activity 通过 dispatchTouchEvent 方法分发事件,首先尝试将事件传递给 Window 及其根视图(DecorView)。
  • 消费事件:如果根视图及其子视图没有消费事件,Activity 会调用自己的 onTouchEvent 方法处理事件。因此,Activity 具备兜底消费事件的能力。

ViewGroup

  • 分发事件ViewGroup 通过 dispatchTouchEvent 方法将事件分发给子视图(或进一步嵌套的子视图)。
  • 拦截事件ViewGroup 拥有 onInterceptTouchEvent 方法,可以决定是否拦截事件。拦截事件后,事件不会再传递给子视图,而是直接由 ViewGroup 自己处理。
  • 消费事件:当 ViewGroup 需要处理自己捕获的事件时,最终会调用其 onTouchEvent 方法来消费事件。因此,ViewGroup 可以在适当情况下选择消费事件。

View

  • 消费事件View 只能消费事件,而没有分发和拦截事件的能力。当 dispatchTouchEvent 将事件传递给 View 时,View 只能选择在 onTouchEvent 中处理和消费该事件,或者将事件交回父视图。
  • Activity:负责整体的事件分发和兜底消费。
  • ViewGroup:负责在视图层级中分发事件,具备拦截和消费事件的灵活性。
  • View:仅具备事件消费能力。

协作方法

  1. 分发事件 (dispatchTouchEvent)

    • dispatchTouchEvent(MotionEvent event) 方法是事件分发的入口。
    • 每当事件产生时(如点击、滑动),系统会将该事件封装成一个 MotionEvent 对象,并通过 dispatchTouchEvent 方法传递给根视图(通常是 Activity 中的 DecorView)。
    • dispatchTouchEvent 中,事件会根据层级逐层传递给子视图,直到找到可以处理事件的视图为止。
    • dispatchTouchEvent 返回 true,则事件处理停止;若返回 false,则事件会继续传递。
  2. 拦截事件 (onInterceptTouchEvent)

    • onInterceptTouchEvent(MotionEvent event) 主要用于 ViewGroup 及其子类。
    • ViewGroup 可以选择是否拦截事件并防止它传递给子视图。
    • 如果 onInterceptTouchEvent 返回 true,则事件会直接交由 ViewGroup 自己处理,子视图将无法接收到事件;如果返回 false,则事件会继续传递到子视图。
    • 常见场景是滑动容器(如 ScrollView)在检测到用户滑动手势时,会选择拦截触摸事件,使得事件不再传递给子视图。
  3. 处理事件 (onTouchEvent)

    • onTouchEvent(MotionEvent event) 方法是实际处理事件的地方。
    • 视图可以在该方法中根据 MotionEvent 的类型(如 ACTION_DOWNACTION_MOVEACTION_UP 等)进行具体的操作(如点击处理、滑动等)。
    • 如果 onTouchEvent 返回 true,表示视图已处理该事件;如果返回 false,则事件会继续向上层视图传递,直到被某个视图消费或到达根视图为止。

整体流程

  1. 事件从 ActivitydispatchTouchEvent 开始。
  2. 事件传递给根视图(通常是一个 ViewGroup),然后通过 dispatchTouchEvent 传递到子视图。
  3. ViewGroup 中调用 onInterceptTouchEvent 判断是否拦截事件。
  4. 如果 onInterceptTouchEvent 返回 false,则事件继续向下传递;否则由 ViewGrouponTouchEvent 处理。
  5. 最终,事件在目标视图的 onTouchEvent 中被消费。

注意

  • 事件的消费:当某个视图返回 true,表示事件被消费,后续事件(如 ACTION_MOVEACTION_UP)会继续传递给该视图。
  • 父视图与子视图的冲突:父视图可以通过拦截事件来管理事件的流向,避免子视图误处理事件。
  • requestDisallowInterceptTouchEvent(boolean disallowIntercept):子视图可以请求父视图不要拦截事件,适用于处理特殊的事件需求(如嵌套滑动)。

Activity事件分发

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
  1. getWindow().superDispatchTouchEvent(ev)
if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
}
Activity#dispatchTouchEvent() ->
PhoneWindow#superDispatchTouchEvent() ->
DecorView#superDispatchTouchEvent() ->
ViewGroup#dispatchTouchEvent()
  • getWindow()返回当前ActivityWindow对象(Window对象的唯一实现类是PhoneWindow类),调用其superDispatchTouchEvent方法来进一步分发事件
  • DecorView#superDispatchTouchEvent() 方法内部会将事件传递给根视图(一般是 DecorView),并由该视图将事件沿视图层次分发下去,此方法调用父类ViewGroup#dispatchTouchEvent()

ViewGroupdispatchTouchEvent

  • ViewGroup.dispatchTouchEvent 返回 true 表示事件已经在 ViewGroup 或其子视图中被消费,不再向上传递。
  • ViewGroup.dispatchTouchEvent 返回 false 表示事件未被处理,最终会由 Activity 兜底处理。
  1. onTouchEvent(ev)
return onTouchEvent(ev);
  • 如果 superDispatchTouchEvent(ev) 返回 false,即所有的视图和组件都未处理该事件,dispatchTouchEvent 会将事件传递给 Activity 自身的 onTouchEvent 方法。
  • onTouchEventActivity 处理事件的最后一步,通常用于处理默认的触摸行为。

ViewGroup事件分发

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {   

    // 如果是 ACTION_DOWN 或者存在 mFirstTouchTarget(表示当前视图或子视图已经接收了一个触摸事件),则可以继续检查是否拦截。
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        // 如果拦截事件(disallowIntercept 为 false),调用onInterceptTouchEvent,该事件交给viewGroup处理 
        // 不拦截事件则设置intercepted为false,后续继续向下传递给子视图
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action);
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }

    //...
}
  • ViewGroup.onInterceptTouchEvent()

    返回false:不拦截(默认)

    返回true:拦截,即事件停止往下传递(需手动复写onInterceptTouchEvent()其返回true)

// 从后往前遍历子视图
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);

    // 检查子视图是否可以接收指针事件以及触摸点是否在其边界内
    if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue; // 跳过不接收事件的子视图
    }

    // 获取当前子视图的触摸目标
    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // 子视图已经在处理触摸事件,更新指针ID位
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break; // 退出循环
    }

    // 将触摸事件分发给当前子视图
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // 更新最后的触摸状态
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = x; 
        mLastTouchDownY = y;
        newTouchTarget = addTouchTarget(child, idBitsToAssign); 
        alreadyDispatchedToNewTouchTarget = true; 
        break; // 退出循环
    }
}
  1. 遍历子视图

    • 首先,代码会遍历 ViewGroup 的所有子视图,从后向前(通常是为了优先处理最上层的视图),以确保能够找到可以接收触摸事件的视图。
  2. 判断是否能够接收点击事件

    • 判断子视图是否能够接收点击事件主要考虑两个条件:
      • 动画状态:如果子视图正在播放动画,它可能不希望接收触摸事件。在这种情况下,该视图将被跳过。
      • 触摸坐标:需要检查触摸事件的坐标是否落在子视图的区域内。通过计算触摸点与子视图边界的关系来判断。
  3. 传递触摸事件

    • 如果找到一个满足条件的子视图,该视图将接收触摸事件。此时,会调用 dispatchTransformedTouchEvent 方法,实际上是调用了子视图的 dispatchTouchEvent 方法,将触摸事件传递给它进行处理。

image-20241103204811747

如果子元素view返回了true,表示被子元素消耗了,那么此时就会跳出循环

img

View点击事件

View事件分发机制从dispatchTouchEvent()开始

public boolean dispatchTouchEvent(MotionEvent event) {  
    if ( (mViewFlags & ENABLED_MASK) == ENABLED && 
        mOnTouchListener != null &&  
        mOnTouchListener.onTouch(this, event)) {  
        return true;  
    } 
    return onTouchEvent(event);  
}
// 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
//   1. (mViewFlags & ENABLED_MASK) == ENABLED
//   2. mOnTouchListener != null
//   3. mOnTouchListener.onTouch(this, event)
// 下面对这3个条件逐个分析
/**
  * 条件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
    }  
});
/**
  * 分析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;  
}
/**
  * 分析2:performClick()
  */  
public boolean performClick() {  
    if (mOnClickListener != null) {
        // 只要通过setOnClickListener()为控件View注册1个点击事件
        // 那么就会给mOnClickListener变量赋值(即不为空)
        // 则会往下回调onClick() & performClick()返回true
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}

img

总结

img



感谢您的阅读
如有错误烦请指正


参考:

  1. Android 事件分发机制详解(上)
  2. Android 事件分发机制详解(下)_montouchlistener-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/907693.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

湘潭市学生公交卡线上申领流程及一寸照片自拍方法

在湘潭市&#xff0c;学生公交卡的线上申领流程已经非常便捷&#xff0c;同时&#xff0c;为了满足学生公交卡申领时所需的一寸照片要求&#xff0c;本文将详细介绍整个申领流程以及如何使用手机自拍并制作线上申领学生公交卡所需的一寸照片电子版。 一、湘潭市学生公交卡线上申…

翻译工具体验分享:deepl翻译等10款神器对比

作为一位在外贸行业摸爬滚打多年的客服&#xff0c;我深知在与国际客户沟通时&#xff0c;准确、高效的翻译工具是多么的重要。今天&#xff0c;我就来和大家分享一下我使用过的几款翻译工具&#xff0c;一共是十款&#xff0c;大家可以先看看。 一、福昕在线翻译 传送门&…

Linux入门(2)

林纳斯托瓦兹 Linux之父 1. echo echo是向指定文件打印内容 ehco要打印的内容&#xff0c;不加任何操作就默认打印到显示器文件上。 知识点 在Linux下&#xff0c;一切皆文件。 打印到显示器&#xff0c;显示器也是文件。 2.重定向 >重定向操作&#xff0c;>指向的…

六 在WEB中应用MyBatis(使用MVC架构模式)

六、在WEB中应用MyBatis&#xff08;使用MVC架构模式&#xff09; 实现功能&#xff1a; 银行账户转账 使用技术&#xff1a; HTML Servlet MyBatis WEB应用的名称&#xff1a; bank 6.1 需求描述 6.2 数据库表的设计和准备数据 6.3 实现步骤 第一步&#xff1a;环境…

React + Vite + TypeScript + React router项目搭建教程

一、创建项目 运行项目 二、目录结构 项目目录&#xff1a; ├─node_modules //第三方依赖 ├─public //静态资源&#xff08;不参与打包&#xff09; └─src├─assets //静态资源├─components //组件├─config //配置├─http //请求方法封装├─layout //页面…

Java-I/O框架09:InputStreamReader、OutputStreamWriter使用

视频链接&#xff1a;16.24 转换流的使用_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Tz4y1X7H7?spm_id_from333.788.videopod.episodes&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5&p24 1.InputStreamReader使用 package com.yundait.Demo05;import java…

一键AI换衣-可图AI试衣

我们的真的实现了穿衣自由了吗&#xff1f;上传一张人物图片和衣服的图片&#xff0c;就能实现一键换衣。 这就是可图AI试衣项目 魔塔地址&#xff1a;https://www.modelscope.cn/studio ... lors-Virtual-Try-On 参考&#xff1a; 一键AI换衣-可图AI试衣 https://www.jinsh…

Linux的IP网路命令: 用于显示和操作网络接口(网络设备)的命令ip link详解

目录 一、概述 二、用法 1、基本语法 2、常用选项 3、常用参数 4、获取帮助 三、示例 1. 显示所有网络接口的信息 &#xff08;1&#xff09;命令 &#xff08;2&#xff09;输出示例 &#xff08;3&#xff09;实际操作 2. 启动网络接口 3. 停止网络接口 4. 更改…

程序员记笔记有没有必要?如何高效记笔记?

本文转载自&#xff1a;https://fangcaicoding.cn/article/57 大家好&#xff01;我是方才&#xff0c;目前是8人后端研发团队的负责人&#xff0c;拥有6年后端经验&3年团队管理经验&#xff0c;截止目前面试过近200位候选人&#xff0c;主导过单表上10亿、累计上100亿数据…

lust变频器维修电梯变频器CDD34.014.W2.1LSPC1

LUST伺服在安装时须注意&#xff0c;不可有任何的铁屑、螺丝、导线等掉人驱动器内。在安装完成后应作基本的检测动作&#xff0c;如对地阻抗&#xff0c;和短路检测等。 所有的安装及使用事项需要符合安全规定&#xff0c;并且也需要符合当地的相关规定和灾害预防措施。DC BUS…

NFTScan Site:以蓝标认证与高级项目管理功能赋能 NFT 项目

自 NFTScan Site 上线以来&#xff0c;它迅速成为 NFT 市场中的一支重要力量&#xff0c;凭借对各类 NFT 集合、市场以及 NFTfi 项目的认证获得了广泛认可。这个平台帮助许多项目提升了曝光度和可见性&#xff0c;为它们在竞争激烈的 NFT 市场中创造了更大的成功机会。 在最新更…

深度学习在复杂系统中的应用

引言 复杂系统由多个相互作用的组成部分构成&#xff0c;这些部分之间的关系往往是非线性的&#xff0c;整体行为难以通过简单的线性组合来预测。这类系统广泛存在于生态学、气象学、经济学和社会科学等多个领域&#xff0c;具有动态演变、自组织、涌现现象以及多尺度与异质性…

Vue computed watch

computed watch watch current prev

批量提取当前文件夹内的文件名

在需要提取的文件夹内新建一个txt文件&#xff0c;输入&#xff1a; dir ./b>name.txt 然后将该txt文件的扩展名改为.bat 如图 双击即可提取当前文件夹文件名&#xff0c;并保存到name.txt内

Android OpenGL ES详解——模板Stencil

目录 一、概念 1、模板测试 2、模板缓冲 二、模板测试如何使用 1、开启和关闭模板测试 2、开启/禁止模板缓冲区写入 3、模板测试策略函数 4、更新模板缓冲 5、模板测试应用——物体轮廓 三、模板缓冲如何使用 1、创建模板缓冲 2、使用模板缓冲 3、模板缓冲应用——…

QML基础语法2

函数 函数格式&#xff1a; function关键字 函数名(参数名1:参数类型,参数名2:参数类型,...):返回值类型{} 其中&#xff1a; 函数名必须以小写字符开头&#xff0c;后面驼峰可以有多个参数或者没有参数参数类型可以不写返回值类型也可以不写 如何调用&#xff1a;通过id点…

Qt自定义控件:汽车速度表

1、功能 制作一个汽车速度表 2、实现 从外到内进行绘制&#xff0c;初始化画布&#xff0c;画渐变色外圈&#xff0c;画刻度&#xff0c;写刻度文字&#xff0c;画指针&#xff0c;画扇形&#xff0c;画内圈渐变色&#xff0c;画黑色内圈&#xff0c;写当前值 3、效果 4、源…

Rust 力扣 - 1461. 检查一个字符串是否包含所有长度为 K 的二进制子串

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 长度为k的二进制子串所有取值的集合为[0, sum(k)]&#xff0c;其中sum(k)为1 2 4 … 1 << (k - 1) 我们只需要创建一个长度为sum(k) 1的数组 f &#xff0c;其中下标为 i 的元素用来标记字符串中子串…

【2024年11月高质量国际学术会议推荐1】拓展学术视野,点亮学术之路!开启科研新征程!——数学|物理|电离|能源|遥感|交通各大领域...

【2024年11月高质量国际学术会议推荐1】拓展学术视野&#xff0c;点亮学术之路&#xff01;开启科研新征程&#xff01;——数学|物理|电离|能源|遥感|交通各大领域… 【2024年11月高质量国际学术会议推荐1】拓展学术视野&#xff0c;点亮学术之路&#xff01;开启科研新征程&…

沪深A股上市公司数据报告分析

数据分析报 目录 数据分析报告 1.引言 1.1 背景介绍 1.2 报告目的 1.3 报告范围 1.4 关键术语定义 2. 数据收集与预处理 2.1 数据来源概述 2.2 数据收集过程 2.3 数据预处理步骤 3. 数据可视化 3.1分析地区对公司数量的影响 3.2分析行业分类是否影响公…