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

news2024/11/25 3:45:34

在 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.coloradmin.cn/o/519913.html

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

相关文章

三种方式在HTML使用阿里字体图标--iconfont阿里巴巴矢量图标库

好久没用到阿里巴巴的图标,突然要用到就发现不会用了,只会导出png格式的图标png了 目录 1、字体图标 方法一、本地使用通过类名使用阿里矢量图标 1、把图标添加入库 2、把图标添加到项目 3、下载字体图标 4、使用文件 5、在对应的HTML页面上引入…

瑞吉外卖项目笔记01——环境搭建、后台登录功能

1.1 数据库 数据库&#xff1a; 创建一个空白数据库reggie&#xff0c;然后导入执行SQL文件创建的表如下&#xff1a; 1.2 项目依赖 项目依赖&#xff1a; pom.xml文件内的依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"…

国外APP项目的上线流程

现在很多创业者希望自己的项目能走出国内&#xff0c;向全球各地发展&#xff0c;尤其对于一些通用APP来说&#xff0c;国外可以增加一个新的收入渠道。比如常见的出海APP有小型游戏、手机清理、杀毒软件等等&#xff0c;这些类型的APP在全球的使用基本都是一样的&#xff0c;因…

Lucene(1):Lucene介绍

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

Lucene(2):Lucene全文检索的流程

1 索引和搜索流程图 &#xff08;1&#xff09;绿色表示索引过程&#xff0c;对要搜索的原始内容进行索引构建一个索引库&#xff0c;索引过程包括&#xff1a; 确定原始内容即要搜索的内容 获得文档创建文档分析文档索引文档 &#xff08;2&#xff09;红色表示搜索过程&…

win10安装conda

conda是一个开源的软件包管理系统和环境管理系统&#xff0c;用于安装、管理和升级各种软件和应用程序。它可以在多个操作系统上运行&#xff0c;支持数百种语言和各种计算机程序。conda提供了安装软件包、创建和管理虚拟环境、配置依赖关系等功能&#xff0c;并且可以轻松地在…

Mac+Vscode+PyQt

纵览 1、需要安装Python&#xff0c;而我安装了Anaconda&#xff0c;并建立了一个虚拟环境env_qt2、在Vscode的终端中利用命令&#xff1a;3、在Vscode的扩展中安装「PYQT Integration」插件&#xff0c;4、可以在bin文件夹下创建一个无后缀的文件des&#xff0c;输入&#xff…

Hacking The Box----Awkward

信息收集 nmap扫描&#xff0c;发现22号端口和80号端口打开&#xff0c;80号端口上运行着http服务器。访问ip后URL变为hat-valley.htb 修改/etc/hosts文件&#xff0c;添加10.10.11.185 hat-valley.htb&#xff0c;然后就能正常访问网站。可以看到是一家卖帽子的公司的网站&a…

Windows如何安装使用curl命令

一、前言 个人主页: ζ小菜鸡大家好我是ζ小菜鸡&#xff0c;让我们一起学习Windows如何安装使用curl命令。如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连) 二、Curl是什么 curl命令网络应用curl命令是一个利用URL规则在命令行下工作的文件传输工具。 CURL支持的通信协议…

【从零开始学Skynet】高级篇(一):Protobuf数据传输

1、什么是Protobuf Protobuf是谷歌发布的一套协议格式&#xff0c;它规定了一系列的编码和解 码方法&#xff0c;比如对于数字&#xff0c;它要求根据数字的大小选择存储空间&#xff0c;小于等于15的数字只用1个字节来表示&#xff0c;大于15的数用2个字节表示&#xff0c;以此…

PBR核心理论与渲染原理

基于物理的渲染&#xff08;Physically Based Rendering&#xff0c;PBR&#xff09;是指使用基于物理原理和微平面理论建模的着色/光照模型&#xff0c;以及使用从现实中测量的表面参数来准确表示真实世界材质的渲染理念。 以下是对PBR基础理念的概括&#xff1a; 微平面理论…

Datax的使用说明及入门操作案例演示

1.DataX DataX 是阿里云 DataWorks数据集成 的开源版本&#xff0c;在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS, dat…

power shell 入门

常用命令 powershell版本 不区分大小写: $psversiontable $psv + tab 自动补齐 // ps version get-command 或者 gcm //获取所有命令 get-help 命令名 // 获取命令帮助信息 路径相关 get-location 或者 gl 或者 pwd // 获取当前路径 pwd: print work directory 的缩写.

Linux命令易混淆(看到新知识点就更新)

DNS相关 在Linux操作系统中&#xff0c;TCP/IP网络是通过若干个文本文件进行配置的。系统在启动时通过读取一组有关网络配置的文件和脚本参数内容&#xff0c;来实现网络接口的初始化和控制过程&#xff0c;这些文件和脚本大多数位于/etc目录下。 Linux下配置dns的三种方法 1…

深入了解梯度消失与梯度爆炸

本文探讨深度学习中经常会提到的概念–梯度消失与梯度爆炸。他们是影响模型收敛&#xff0c;学习好坏的一个重要因素&#xff0c;对此现象也提出了对应的解决方案。在此记录其概念&#xff0c;原因和相关的解决方案&#xff0c;仅供参考。 目录 概念原因解决方案1. 参数初始化2…

Vim学习笔记【Ch00,Ch01】

Vim学习笔记 GitHub的Learn-Vim仓库学习笔记Ch00 前言Ch01 Starting VimVim的官方链接Windows10下载和安装VimVim初级使用打开Vim退出Vim保存文件打开文件帮助文档helpargument参数打开多个窗口暂停 GitHub的Learn-Vim仓库学习笔记 仓库地址&#xff1a;https://github.com/ig…

Qt--数据库--增删改查操作

目录 1. Qt数据库简介 2. 连接与关闭 3. 建表 dialog.h dialog.cpp dialog.ui 4. 增删改 1.添加数据 dialog.h dialog.cpp 2.删除数据 dialog.h dialog.cpp 3.修改数据 dialog.h dialog.cpp 5. 查询 dialog.h dialog.cpp 判断数据是否存在 dialog.h dialog.cpp 1. Qt数据库简介…

如何在IVD行业运用IPD?

IVD&#xff08;体外诊断&#xff0c;In Vitro Diagnostic&#xff09;是指对人体样本&#xff08;血液、体液、组织&#xff09;进行定性或定量的检测&#xff0c;进而判断疾病或机体功能的诊断方法。IVD目前已经成为疾病预防、诊断治疗必不可少的医学手段&#xff0c;约80%左…

ChatGPT国内使用办法,无需魔法上网,免费使用ChatGPT,长期更新!!

新建了一个网站 每天给大家更新可用的免翻国内可用chatGPT https://ai.weoknow.com/ 2023.5.7新增一个 软件名称ChatGPT✦ ▌ 软件摘要 软件名称&#xff1a;ChatGPT 适用设备&#xff1a;浏览器 文件大小&#xff1a;0MB ▌ 软件简介 ChatGPT非常强大&#xff0c;但国内合…

12-CSS-语法和选择器

一、语法格式&#xff1a; 选择器指向需要设置样式的 HTML 元素。声明块包含一条或多条用分号分隔的声明。每条声明都包含一个 CSS 属性名称和一个值&#xff0c;以冒号分隔。多条 CSS 声明用分号分隔&#xff0c;声明块用花括号括起来。 二、选择器&#xff1a; CSS 选择器…