Android View 事件分发机制,看这一篇就够了

news/2024/5/17 14:48:43/文章来源:https://blog.csdn.net/yujun2023/article/details/130652186

在 Android 开发当中,View 的事件分发机制是一块很重要的知识。不仅在开发当中经常需要用到,面试的时候也经常被问到。

如果你在面试的时候,能把这块讲清楚,对于校招生或者实习生来说,算是一块不错的加分项。对于工作几年的我们来说,这是必须掌握的,讲不明白,那你回去等通知吧,哈哈。

目录大概如下:

  1. View 事件分发机制简介
  2. View 常见滑动冲突解决
  3. View 双击,多击事件是怎么实现的
  4. 手势识别
  5. 小结

View 事件分发机制简介

View 触摸事件

对于屏幕的点击,滑动,抬起等一系的动作,其实都是由一个一个MotionEvent对象组成的。根据不同动作,主要有以下三种事件类型:

1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件 2.ACTION_MOVE:手指在屏幕上移动时候产生该事件 3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件 4.ACTION_CANCEL 当前 View 的手势被打断,后续不会再收到任何事件

从 ACTION_DOWN 开始到 ACTION_UP/ACTION_CANCEL 结束我们称为一个事件序列

正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在 MotionEvent 上来讲无外乎下面 3 种 case。

  1. 点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP
  2. 点击后再风骚的滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
  3. 某些情况下,我们可能会没有收到 ACTION_UP 事件,是收到 ACTION_CANCEL 事件。

ACTION_CANCEL 一般是指 ChildView 原先拥有事件处理权,后面由于某些原因,该处理权需要交回给上层去处理,ChildView便会收到 ACTION_CANCEL 事件。对于一些复位或者重置操作,我们应该在 ACTION_UP 和 ACTION_CANCEL 里面同时进行处理

代码逻辑上是:上层判断之前交给ChildView的事件处理权需要收回来了,便会做事件的拦截处理,拦截时给ChildView发一个ACTION_CANCEL事件

几个主要方法

我们知道,View 的事件分发机制主要涉及到以下几个方法

  • dispatchTouchEvent ,这个方法主要是用来分发事件的
  • onInterceptTouchEvent,这个方法主要是用来拦截事件的(需要注意的是 ViewGroup 才有这个方法,View 没有 onInterceptTouchEvent 这个方法)
  • onTouchEvent 这个方法主要是用来处理事件的
  • requestDisallowInterceptTouchEvent(true),这个方法能够影响父View是否拦截事件,true 表示父 View 不拦截事件,false 表示父 View 拦截事件

我们先来看一张图。

以下内容参考图解 Android 事件分发机制这一篇博客

  • 仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
  • 事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 进行分发
  • 箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。)
  • dispatchTouchEvent和 onTouchEvent的框里有个【true---->消费】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
  • 目前所有的图的事件是针对ACTION_DOWN的,对于ACTION_MOVE和ACTION_UP我们最后做分析。

当触摸事件发生时,首先 Activity 将 TouchEvent 传递给最顶层的 View,TouchEvent最先到达最顶层 view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法进行分发,

如果dispatchTouchEvent返回true 消费事件,事件终结。

如果dispatchTouchEvent返回 false ,则回传给父View的onTouchEvent事件处理;

如果dispatchTouchEvent返回super的话,默认会调用自己的onInterceptTouchEvent方法。

  • 默认的情况下onInterceptTouchEvent回调用super方法,super方法默认返回false,所以会交给子View的onDispatchTouchEvent方法处理
  • 如果 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,
  • 如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent 再来开始这个事件的分发。

关于更多详细分析,请查看原博客图解 Android 事件分发机制,真心推荐,写得很好。


View 滑动事件冲突

在开发当中,View 的滑动冲突时经常遇到的,比如 ViewPager 嵌套 ViewPager,ScrollView 嵌套 ViewPager。下面让我们一起来看看怎么解决。

常见的三种情况

第一种情况,滑动方向不同

第二种情况,滑动方向相同

第三种情况,上述两种情况的嵌套

解决思路

看了上面三种情况,我们知道他们的共同特点是父View 和子View都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件 同一时刻只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突。

那既然同一时刻只能由某一个 View 或者 ViewGroup 消费拦截,那我们就只需要 决定在某个时刻由这个 View 或者 ViewGroup 拦截事件,另外的 某个时刻由 另外一个 View 或者 ViewGroup 拦截事件,不就 OK了吗?

综上,正如 在 《Android开发艺术》 一书提出的,总共 有两种解决方案

以下解决思路来自于 《Android开发艺术》 书籍

下面的两种方法针对第一种情况(滑动方向不同),父View是上下滑动,子View是左右滑动的情况。

外部解决法

从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不要的时候返回false,为代码大概 如下

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {final float x = ev.getX();final float y = ev.getY();final int action = ev.getAction();switch (action) {case MotionEvent.ACTION_DOWN:mDownPosX = x;mDownPosY = y;break;case MotionEvent.ACTION_MOVE:final float deltaX = Math.abs(x - mDownPosX);final float deltaY = Math.abs(y - mDownPosY);// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截if (deltaX > deltaY) {return false;}}return super.onInterceptTouchEvent(ev);
}

内部解决法

从子View着手,父View先不要拦截任何事件,所有的事件传递给 子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View处理。

实现思路 如下,重写子 View的dispatchTouchEvent方法,在Action_down 动作中通过方法 requestDisallowInterceptTouchEvent(true) 先请求 父 View不要拦截事件,这样保证子 View 能够接受到 Action_move 事件,再在 Action_move 动作中根据自己的逻辑是否要拦截事件,不需要拦截事件的话再交给 父 View 处理。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {int x = (int) ev.getRawX();int y = (int) ev.getRawY();int dealtX = 0;int dealtY = 0;switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:dealtX = 0;dealtY = 0;// 保证子View能够接收到Action_move事件getParent().requestDisallowInterceptTouchEvent(true);break;case MotionEvent.ACTION_MOVE:dealtX += Math.abs(x - lastX);dealtY += Math.abs(y - lastY);Log.i(TAG, "dealtX:=" + dealtX);Log.i(TAG, "dealtY:=" + dealtY);// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截if (dealtX >= dealtY) {getParent().requestDisallowInterceptTouchEvent(true);} else {getParent().requestDisallowInterceptTouchEvent(false);}lastX = x;lastY = y;break;case MotionEvent.ACTION_CANCEL:break;case MotionEvent.ACTION_UP:break;}return super.dispatchTouchEvent(ev);
}

更多细节,可以查看我的这篇文章,里面有详细介绍哦 ViewPager,ScrollView 嵌套ViewPager滑动冲突解决


View 双击,多击事件是怎么实现的

实现之前,我们首先来阐述一下思路,怎样实现双击事件,正所谓,授人以鱼不如授人以渔。

单击:用户点击一次之后,一段时间之内不再点击

双击;用户点击一次之后,一段时间之内再次点击

实现思路

  1. 我们监听 onTouch 事件,在 ACTION_DOWN 的时候,点击次数 clickCount +1;
  2. 同时,在 ACTION_DOWN 的时候,延时一段时间,执行相应的 Runnable 任务,这里我们用 handler 的 postDelayed 实现
  3. 在延时任务执行的时候,我们根据点击的次数,进行单击或者多级的回调,最后,记得重置点击次数,以及移除延时任务
open class MyDoubleTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {private var clickCount = 0 //记录连续点击次数private val handler: Handler = Handler()interface MyClickCallBack {fun oneClick() //点击一次的回调fun doubleClick() //连续点击两次的回调}override fun onTouch(v: View, event: MotionEvent): Boolean {if (event.action == MotionEvent.ACTION_DOWN) {clickCount++handler.postDelayed({if (clickCount == 1) {myClickCallBack.oneClick()} else if (clickCount == 2) {myClickCallBack.doubleClick()}handler.removeCallbacksAndMessages(null)//清空handler延时,并防内存泄漏clickCount = 0 //计数清零}, timeout.toLong()) //延时timeout后执行run方法中的代码}return false //让点击事件继续传播,方便再给View添加其他事件监听}companion object {private const val TAG = "MyClickListener"private val timeout = ViewConfiguration.getDoubleTapTimeout() //双击间四百毫秒延时init {Log.i(TAG, "timeout is $timeout ")}}}

三击事件

三级事件呢,其实也很简单,我们直接判断在指定时间间隔内点击的次数即可

open class MyMultiTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {private var clickCount = 0 //记录连续点击次数private val handler: Handler = Handler()interface MyClickCallBack {fun oneClick() //点击一次的回调fun doubleClick() //连续点击两次的回调fun threeClick() // 连续点击三次的回调}override fun onTouch(v: View, event: MotionEvent): Boolean {if (event.action == MotionEvent.ACTION_DOWN) {clickCount++handler.postDelayed({if (clickCount == 1) {myClickCallBack.oneClick()} else if (clickCount == 2) {myClickCallBack.doubleClick()} else if (clickCount == 3) {myClickCallBack.threeClick()}handler.removeCallbacksAndMessages(null)//清空handler延时,并防内存泄漏clickCount = 0 //计数清零}, timeout.toLong()) //延时timeout后执行run方法中的代码}return false //让点击事件继续传播,方便再给View添加其他事件监听}companion object {private const val TAG = "MyClickListener"private val timeout = 600 //双击间四百毫秒延时init {Log.i(TAG, "timeout is $timeout ")}}
}

手势识别

在 Android 开发当中,几乎所有的事件都会与用户进行交互,而我们用得的最多的就是手势了。

我们知道当我们触摸屏幕的时候,会产生很多事件,比如 down,move,up, fling 事件等等。一些简单的处理,我们可以直接重写 View 的 onTouchEvent 方法,根据 View 的 MotionEvent 事件进行处理。

而 Google 为了方便开发者方便接入,提供了几个默认处理类,那就是 GestureDetector 和 ScaleGestureDetector。

GestureDetector这个类对外提供了两个接口和一个外部类 。 接口:OnGestureListener,OnDoubleTapListener

内部类:SimpleOnGestureListener,同时实现了 OnGestureListener,OnDoubleTapListener 接口,如果只想使用接口里面的某个方法,可以直接使用它,方便快捷。

讲解之前,我们向来看一下怎么使用

GestureDetector(Context context, GestureDetector.OnGestureListener listener)

GestureDetector 基本使用

第一步,初始化 GestureDetector 对象

 mDetector = GestureDetectorCompat(this, MyGestureListener())

可以看到有两个参数,第一个参数 context,第二个参数 OnGestureListener,我们可以直接实现 OnGestureListener 接口,也可以直接使用 GestureDetector.SimpleOnGestureListener

    private class MyGestureListener : GestureDetector.OnGestureListener {private val TAG = "GestureDemoActivity"override fun onShowPress(e: MotionEvent?) {Log.d(TAG, "onShowPress: e is $e")}override fun onSingleTapUp(e: MotionEvent?): Boolean {Log.d(TAG, "onSingleTapUp: e is $e")return false}override fun onDown(event: MotionEvent): Boolean {Log.d(TAG, "onDown: $event")return true}override fun onFling(event1: MotionEvent,event2: MotionEvent,velocityX: Float,velocityY: Float): Boolean {Log.d(TAG, "onFling: $event1 $event2")return false}override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {Log.d(TAG, "onScroll: distanceX is $distanceX,distanceY is $distanceY ")return false}override fun onLongPress(e: MotionEvent?) {Log.d(TAG, "onLongPress: e is $e")}}

第二步:设置双击监听


// 设置双击监听
mDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {override fun onDoubleTap(e: MotionEvent?): Boolean {Log.d(TAG, "onDoubleTap: e is e")return false}override fun onDoubleTapEvent(e: MotionEvent?): Boolean {Log.d(TAG, "onDoubleTapEvent: e is e")return false}override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {Log.d(TAG, "onSingleTapConfirmed: e is e")return false}})

最后,重写 Activity 或者 View 的 onTouchEvent ,将事件交给 mDetector 处理。

通常会有两种写法,第一种是如果手势处理器处理了,直接返回 true,进行消费。否则,进行默认处理

override fun onTouchEvent(event: MotionEvent): Boolean {return if (mDetector.onTouchEvent(event)) {true} else {super.onTouchEvent(event)}}

第二种写法是直接在 onTouchEvent 方法中,直接调用 mDetector.onTouchEvent(event) 方法

override fun onTouchEvent(event: MotionEvent): Boolean {mDetector.onTouchEvent(event)return super.onTouchEvent(event)}

第二种写法,一般不会影响当前 View 或者 Activity 事件的传递,在开发当中,有时候为了减少一些触摸事件的冲突,经常这样写。

ScaleGestureDetector 这里暂时不展开描述了了,写着写着,发现好多呀,一个周末就这样过去,贼快,觉得对你有帮助的,请来个三连,点赞,收藏,转发😆


小结

这篇文章,其实不难。主要是将 View 的事件分发机制,滑动冲突,以及开发当中经常用到的一些知识点,总结一下。

参考博客:

图解 Android 事件分发机制

ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

Demo 地址

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

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

相关文章

对STM32栈的理解Stack_Size EQU 0x00000400

对STM32栈的理解Stack_Size EQU 0x00000400 Stack_Size EQU 0x00000400表示什么意思可以通过查找flash内存的方式定位存储1.flash2.RAM内部 本人主要为个人参考网络及个人总结而来比较,如有雷同请告知,即刻删除 以下引用网上资料 理解堆和栈的区别 &…

PFCdocumentation_FISH Rules and Usage

目录 FISH Scripting FISH Rules and Usage Lines Data Types Reserved Names for Functions and Variables Scope of Variables Functions: Structure, Evaluation, and Calling Scheme Arithmetic: Expressions and Type Conversions Redefining FISH Functions Ex…

中断相关内容大全

中断基本概念:程序中断指计算机执行现行程序过程中,出现某种急需处理的异常情况或特殊请求,CPU暂时中止现行程序,而转去对这些异常情况或特殊请求进行处理,处理完毕后CPU又自动返回到现行程序的断点处,继续…

算法修炼之练气篇——练气十七层

博主:命运之光 专栏:算法修炼之练气篇 前言:每天练习五道题,炼气篇大概会练习200道题左右,题目有C语言网上的题,也有洛谷上面的题,题目简单适合新手入门。(代码都是命运之光自己写的…

【最新可用】chatGPT镜像网站国内使用,免费稳定!

新建了一个网站 https://ai.weoknow.com/ 每天给大家更新可用的国内可用chatGPT 2023.5.8新增一个 ChatGPT 国内免翻版 【网站名称】:Chat GPT Ai 【使用环境】:移动端/电脑网页端 ChatGPT是一款功能强大的免费在线聊天机器人,具有人工智能…

JavaScript通过js的方式来计算平行四边形的面积的代码

以下为通过js的方式来计算平行四边形的程序代码和运行截图 目录 前言 一、通过js的方式来计算平行四边形(html部分) 1.1 运行流程及思想 1.2 代码段 二、通过js的方式来计算平行四边形(js部分) 2.1 运行流程及思想 2.2 代码…

C#中如何使用ObjectPool来提高StringBuilder的性能

在C#中我们知道使用StringBuilder能提高大量字符串拼接的效率,其实StringBuilder的效率也可以提升,那就是使用ObjectPool。以下介绍怎么使用ObjectPool提高StringBuilder的性能。一、简介 C# ObjectPool类是一个内置的类库,用于实现对象…

AFG1062任意波形/函数发生器 产品资料

AFG1000 任意波形/函数发生器,提供 25MHz 或 60MHz 带宽,2 个输出通道,在整个带宽内 1mVpp 到 10Vpp 输出振幅,泰克 AFG1000 任意波形/函数发生器可以生成各种实验室测试所需波形。 *重要的是,它在泰克任意函数发生器系…

基于知识图谱的个性化学习资源推荐系统的设计与实现(论文+源码)_kaic

摘 要 最近几年来,伴随着教育信息化、个性化教育和K12之类的新观念提出,一如既往的教育方法向信息化智能化的转变,学生群体都对这种不受时间和地点约束的学习方式有浓厚的兴趣。而现在市面上存在的推荐系统给学生推荐资料时不符合学生个人对知识获取的…

小曾同学【五周年创作纪念日】——努力向前冲的菜鸟

😄作者简介: 小曾同学.com,一个致力于测试开发的博主⛽️, 如果文章知识点有错误的地方,还请大家指正,让我们一起学习,一起进步。😊 座右铭:不想当开发的测试,不是一个好…

自主可控不走捷径,中国长城做难且正确的事

2020-2022年是中国信创产业的重要推广期,在国家战略的支持下,自主可控领域诸多相关企业均获得绝佳发展良机。 但信创产业“完成替代”不是终点,“实现领先”方是目标。如今势已启、路尚远,前景广阔的市场并不意味着自主可控相关企…

Hadoop之block切片

切片是一个逻辑概念 在不改变现在数据存储的情况下,可以控制参与计算的节点数目 通过切片大小可以达到控制计算节点数量的目的 有多少个切片就会执行多少个Map任务 hdfs上数据存储的一个单元,同一个文件中块的大小都是相同的 因为数据存储到HDFS上不可变&#xff0…

Lucene(1):Lucene介绍

Lucene官网: http://lucene.apache.org/ 1 搜索技术理论基础 1.1 lucene优势 原来的方式实现搜索功能,我们的搜索流程如下图: 上图就是原始搜索引擎技术,如果用户比较少而且数据库的数据量比较小,那么这种方式实现搜…

2路 QSFP,40G 光纤的数据实时采集(5GByte/s 带宽)板卡设计原理图 -PCIE732

板卡概述 PCIE732 是一款基于 PCIE 总线架构的高性能数据传输卡,板卡具有 1 个 PCIex8 主机接口、2 个 QSFP40G 光纤接口,可以实现 2 路 QSFP 40G 光纤的数据实时采集、传输。板卡采用 Xilinx 的高性 能 Kintex UltraScale 系列 FPGA 作为实时处理器…

小程序开发中的插件、组件、控件到底有什么区别?

小程序插件代码由一些自定义组件和 JS 代码文件构成,插件开发者在发布插件时,这些代码被上传到后台保存起来。当小程序使用插件时,使用者需填写插件的 AppID 和版本号,就可从后台获取相应的插件代码。小程序代码编译时&#xff0c…

2023什么蓝牙耳机好?经销商盘点新手必入蓝牙耳机品牌

蓝牙耳机是除手机外我们使用频率最高的数码产品,我做蓝牙耳机经销商五年来,对各个品牌都有深入了解。近期看到很多新手们咨询什么蓝牙耳机好,我给大家盘点一下新手必看的五大蓝牙耳机品牌。 1.JEET Air 2蓝牙耳机 推荐理由:专为舒…

知识点回顾(一)

1.final,finally ,finalize final?修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final&…

外强中干——双向带头循环链表

前言:众所周知,链表有八种结构,由单向或双向,有头或无头,循环或不循环构成。在本篇,将介绍8种链表结构中最复杂的——双向带头循环链表。听着名字或许挺唬人的,但实际上双向带头循环链表实现起来…

H3C防火墙单机旁路部署(网关在防火墙)

防火墙旁路部署在核心交换机上,内网有三个网段vlan 10:172.16.10.1/24、vlan 20:172.16.20.1/24、vlan30:172.16.30.1。要求内网网关在防火墙设备上,由防火墙作为DHCP服务器给终端下发地址,同时由防火墙来控…

网站域名历史记录批量查询-老域名建站历史快照数据查询

域名建站历史查询软件 域名建站历史查询软件是一种用于查询一个域名被使用的网站的历史记录的工具。它可以提供许多有用的信息,包括该网站的创建和修改日期、使用的网站建设平台、使用的CMS系统、网站的历史页面内容和页面结构等。 域名建站历史查询软件的作用是帮…