【Android】View的事件分发机制

news2024/11/27 2:28:37

文章目录

  • 分发顺序
    • 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博客

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java项目之微服务在线教育系统设计与实现(springcloud)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的闲一品交易平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 微服务在线教育系统设计与…

ChatGPT:真如吹的那般神乎其神吗?

ChatGPT的确是个神奇的东西。短短600多天&#xff0c;就已成全球访问量最大的网站之一。 ChatGPT已经出现在与这些大佬顶级大佬Google、Youtube、X.com、Baidu、Yahoo、amazon、Tiktok一起。 当然ChatGPT很优秀&#xff0c;这没有疑问&#xff0c;主要问题还是对度的把握上。…

【深度学习】实验 — 动手实现 GPT【二】:注意力机制、注意力掩码、多头注意力机制

【深度学习】实验 — 动手实现 GPT【二】&#xff1a;注意力机制、多头注意力机制 注意力机制简单示例&#xff1a;单个元素的情况简单示例&#xff1a;计算所有输入词元的注意力权重推广到所有输入序列词元&#xff1a; 注意力掩码代码实现多头注意力测试 注意力机制 简单示例…

简单的kafkaredis学习之kafka

简单的kafka&redis学习整理之kafka 1. kafka 1.1 什么是消息队列 在学习Kafka之前我们先来看一下什么是消息队列&#xff0c;消息队列(Message Queue)&#xff1a;可以简称为MQ 例如&#xff1a;Java中的Queue队列&#xff0c;也可以认为是一个消息队列 消息队列&#x…

基于人工智能的搜索和推荐系统

互联网上的搜索历史分析和用户活动是个性化推荐的基础&#xff0c;这些推荐已成为电子商务行业和在线业务的强大营销工具。随着人工智能的使用&#xff0c;在线搜索也在改进&#xff0c;因为它会根据用户的视觉偏好提出建议&#xff0c;而不是根据每个客户的需求和偏好量身定制…

ssm042在线云音乐系统的设计与实现+jsp(论文+源码)_kaic

摘 要 随着移动互联网时代的发展&#xff0c;网络的使用越来越普及&#xff0c;用户在获取和存储信息方面也会有激动人心的时刻。音乐也将慢慢融入人们的生活中。影响和改变我们的生活。随着当今各种流行音乐的流行&#xff0c;人们在日常生活中经常会用到的就是在线云音乐系统…

TVS 静电管 选型

参数选型举例: 静电管选型举例: 针对信号引脚一般只需ESD防护,关注其在IEC 61000−4−2波形下的测试结果:最大耐压值、钳位电压等,注意此时钳位电压的限值就不是Absolute maximum ratings值了,原因有2 1、Absolute maximum ratings值是指持续加压会损坏芯片 2、如果关…

监控调度台在交通运输行业的优势?

在当今快速发展的交通运输行业中&#xff0c;高效、安全的管理成为确保运营顺畅和乘客满意的关键。监控调度台作为这一领域的核心设备&#xff0c;正发挥着越来越重要的作用。它集成了视频监控、数据分析、实时通讯等多种功能&#xff0c;为交通运输行业带来了诸多优势。下面我…

华为ENSP--ISIS路由协议

项目背景 为了确保资源共享、办公自动化和节省人力成本&#xff0c;公司E申请两条专线将深圳总部和广州、北京两家分公司网络连接起来。公司原来运行OSFP路由协议&#xff0c;现打算迁移到IS-IS路由协议&#xff0c;张同学正在该公司实习&#xff0c;为了提高实际工作的准确性和…

设计模式07-结构型模式2(装饰模式/外观模式/代理模式/Java)

4.4 装饰模式 4.4.1 装饰模式的定义 1.动机&#xff1a;在不改变一个对象本身功能的基础上给对象增加额外的新行为 2.定义&#xff1a;动态地给一个对象增加一些额外的职责&#xff0c;就增加对象功能来说&#xff0c;装饰模式比生成子类实现更为灵活 4.4.2 装饰模式的结构…

Spring @RequestMapping 注解

文章目录 Spring RequestMapping 注解一、引言二、RequestMapping注解基础1、基本用法2、处理多个URI 三、高级用法1、处理HTTP方法2、参数和消息头处理 四、总结 Spring RequestMapping 注解 一、引言 在Spring框架中&#xff0c;RequestMapping 注解是构建Web应用程序时不可…

【Linux】IPC 进程间通信(一):管道(匿名管道命名管道)

✨ 无人扶我青云志&#xff0c;我自踏雪至山巅 &#x1f30f; &#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;Linux—登神长阶 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#…

单片机串口接收状态机STM32

单片机串口接收状态机stm32 前言 项目的芯片stm32转国产&#xff0c;国产芯片的串口DMA接收功能测试不通过&#xff0c;所以要由原本很容易配置的串口空闲中断触发DMA接收数据的方式转为串口逐字节接收的状态机接收数据 两种方式各有优劣&#xff0c;不过我的芯片已经主频跑…

信息学科平台系统开发:基于Spring Boot的最佳实践

3系统分析 3.1可行性分析 通过对本基于保密信息学科平台系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本基于保密信息学科平台系统采用Spring Boot框架&a…

探索 ONLYOFFICE 8.2 版本:更高效、更安全的云端办公新体验

引言 在当今这个快节奏的时代&#xff0c;信息技术的发展已经深刻改变了我们的工作方式。从传统的纸质文件到电子文档&#xff0c;再到如今的云端协作&#xff0c;每一步技术进步都代表着效率的飞跃。尤其在后疫情时代&#xff0c;远程办公成为常态&#xff0c;如何保持团队之间…

51c自动驾驶~合集4

我自己的原文哦~ https://blog.51cto.com/whaosoft/12413878 #MCTrack 迈驰&旷视最新MCTrack&#xff1a;KITTI/nuScenes/Waymo三榜单SOTA paper&#xff1a;MCTrack: A Unified 3D Multi-Object Tracking Framework for Autonomous Driving code&#xff1a;https://gi…

STM32HAL-最简单的长、短、多击按键框架(多按键)

概述 本文章使用最简单的写法实现长、短、多击按键框架,非常适合移植各类型单片机,特别是资源少的芯片上。接下来将在stm32单片机上实现,只需占用1个定时器作为时钟扫描按键即可。 一、开发环境 1、硬件平台 STM32F401CEU6 内部Flash : 512Kbytes,SARM …

【论文精读】LPT: Long-tailed prompt tuning for image classification

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;论文精读_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 摘要 2. …

队列的模拟实现

概念&#xff1a; 队列 &#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出 FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为 队尾&#xff08; Tail/Rear &#xff09; 出队列&a…

Centos安装配置Jenkins

下载安装 注意&#xff1a;推荐的LTS版本对部分插件不适配&#xff0c;直接用最新的版本&#xff0c;jenkins还需要用到git和maven&#xff0c;服务器上已经安装&#xff0c;可查看参考文档[1]、[2]&#xff0c;本次不再演示 访问开始使用 Jenkins 下载jenkins 上传至服务器…