Android中的事件分发浅析

2024-09-03 04:08
文章标签 android 事件 浅析 分发

本文主要是介绍Android中的事件分发浅析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

说在前面

Android在启动一个activity时,会实例化一个PhoneWindow,而PhoneWindow对象持有一个Decorview的引用,DecorView中有个id为content的ViewGrou,而我们平常在xml写的布局,就是加载在这个ViewGroup中的,如下图所示(图片引用自网络,侵删)。

 事件分发

一般开发过程中,我们需要处理的事件,也就是我们的手指在ContentView上的触摸事件,在我们眼中,我们用手指触摸、点击一个个按钮、图片等,而无论是按钮、图片 等等,都无非是以下两种的分支:

1、View

拥有dispatchTouchEvent、onTouchEvent两个方法,分管事件分发、事件消费

2、ViewGroup

拥有dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三个方法,分管事件分发、事件拦截、事件消费

预先准备

我们先准备一个View以及两个ViewGroup,他们分别是ChildView(黑色)、MotherLayout(红色)、FatherLayout(蓝色),Child盖在Mother上,Mother盖在Father上,所以Father是三者的最高层级如下图所示: 

 我们分别重写三个类的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent方法并打上log,具体源码为:

FatherLayout.kt

class FatherLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs){private val TAG="FatherLayout"companion object{fun getActionName(action:Int?):String{return when(action){0->{"按下"}1->{"抬起"}2->{"滑动"}else->{"其他"}}}}override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了父亲的dispatch方法,事件为:${getActionName(ev?.action)}")return super.dispatchTouchEvent(ev)}override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了父亲的onIntercept方法,事件为:${getActionName(ev?.action)}")return super.onInterceptTouchEvent(ev)}override fun onTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了父亲的onTouchEvent方法,事件为:${getActionName(ev?.action)}")return super.onTouchEvent(ev)}
}

MotherLayout.kt 

class MotherLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs){private val TAG="MotherLayout"override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了母亲的dispatch方法,事件为:${FatherLayout.getActionName(ev?.action)}")return super.dispatchTouchEvent(ev)}override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了母亲的onIntercept方法,事件为:${FatherLayout.getActionName(ev?.action)}")return super.onInterceptTouchEvent(ev)}override fun onTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了母亲的onTouchEvent方法,事件为:${FatherLayout.getActionName(ev?.action)}")return super.onTouchEvent(ev)}}

ChildView.kt

class ChildView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {private val TAG = "ChildView"override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了孩子的dispatch方法,事件为:${FatherLayout.getActionName(ev?.action)}")return super.dispatchTouchEvent(ev)}override fun onTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了孩子的onTouchEvent方法,事件为:${FatherLayout.getActionName(ev?.action)}")return super.onTouchEvent(ev)}
}

xml布局

<?xml version="1.0" encoding="utf-8"?>
<com.wp.facetest.eventHandle.FatherLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="450dp"android:layout_gravity="center"android:layout_height="450dp"android:background="#3a7dff"><com.wp.facetest.eventHandle.MotherLayoutandroid:layout_width="300dp"android:layout_gravity="center"android:layout_height="300dp"android:background="#ff3a7d"><com.wp.facetest.eventHandle.ChildViewandroid:layout_width="150dp"android:layout_gravity="center"android:layout_height="150dp"android:background="#333" /></com.wp.facetest.eventHandle.MotherLayout>
</com.wp.facetest.eventHandle.FatherLayout>

开始测试

我们先后单击Father、Mother、Child,得出如下日志及流程图:

PS:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent方法默认返回false

单击Father

单击Mother

单击Child

 以上可以看出,默认情况下,点击一个布局后,onDown(按下)事件首先会传导到最上层的父布局(FatherLayout)的dispatchTouchEvent方法,紧接着onInterceptTouchEvent、onTouchEvent,然后传到次一层的ViewGroup或View(MotherLayout或ChildView),重复以上传导,如果当前布局无子布局,则会在当前布局的onTouchEvent调用后,依次向上调用对应层次父布局的onTouchEvent,然后本次点击事件结束。

正常来说,一次单击动作,是包含Down和Up两个事件的,那么为什么以上只发现了Down事件呢?

原因是从父到子,无一个敢于站出来承担这次的Down事件,那么上级领导自然没必要再将Up事件下放下来,只能自己默默把Down和Up事件一起消化掉~

那么,我们让父、母、子勇敢一点,会发生什么呢?

针对dispatchTouchEvent

1、将FatherLayout的dispatchTouchEvent删除super.dispatchTouchEvent(ev),并且分别返回true和false,随后点击FatherLayout、MotherLayout、ChildView任何一个,代码、日志分别如下:

返回true

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了父亲的dispatch方法,事件为:${getActionName(ev?.action)}")return true}

返回false

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了父亲的dispatch方法,事件为:${getActionName(ev?.action)}")return false}

当dispatchTouchEvent删除super.dispatchTouchEvent(ev)时,无论返回值为true还是false,事件都不会往下传递,也不会传递到本身的onInterceptTouchEvent和onTouchEvent,只不过,如果返回值为true,则可以接收到所有的事件,如果返回false,只能收到第一个事件。

2、将FatherLayout的dispatchTouchEvent保留super.dispatchTouchEvent(ev),并且返回true(无需试验false,前面默认的就是false),随后点击ChildView,代码、日志分别如下:

 override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG,"调用了父亲的dispatch方法,事件为:${getActionName(ev?.action)}")super.dispatchTouchEvent(ev)return true}

可以看出,第一次的Down事件,在传递到MotherLayout、ChildView后,发现没有人处理,所以和我们上面发的流程图一样,通过onTouchEvent自下而上传回,而随后的Up事件也不会继续往下传递,只到FatherLayout这里截止。

接下来,我们在MotherLayout中也针对dispatchTouchEvent做类似处理:


3、将FatherLayout和MotherLayout的dispatchTouchEvent都保留super.dispatchTouchEvent(ev),并且都返回true,随后点击ChildView,日志如下:

我们发现Down事件从ChildView往上传递,到了MotherLayout这里截止了,而Up事件也只传到了MotherLayout这里,没有继续往下传递

4、将FatherLayout和MotherLayout的dispatchTouchEvent都保留super.dispatchTouchEvent(ev),并且FatherLayout返回true、MotherLayout返回false,随后点击ChildView,日志如下:

我们发现Down事件从ChildView往上传递,还是到了MotherLayout这里截止了,但是后续的Up事件却不见了,原因是FatherLayout的dispatchTouchEvent返回了false,FatherLayout的上级领导看FatherLayout不作为,肯定不会再下放Up事件了!

所以,我们对dispatchTouchEvent得出结论:

1、如果删除super.dispatchTouchEvent(ev),则无论返回true还是false都无意义,因为这个时候只有dispatchTouchEvent可以收到事件,而onInterceptTouchEvent和onTouchEvent都收不到事件,所以我们平时处理事件分发不建议这样写。

2、保留super.dispatchTouchEvent(ev),返回 true,如果下级布局的dispatchTouchEvent都没有返回true,则此布局为第一责任人,下层的Down事件传到此后不会再往上传递,而后续的Up事件也只会从上传到此,不会往下传递。

3、保留super.dispatchTouchEvent(ev),返回 false,那么这就又回到无人敢承担责任这一说了,第一次的Down事件会传递到最下层(ChildView),随后再层层往上传递,然后第二次的Up事件则不会再传下来。

4、如果一个布局想要处理事件,首先自己的dispatchTouchEvent要返回true,其次他的所有父布局的dispatchTouchEvent也要返回true,正所谓,你想干成事,首先要争取到领导的支持,你的上级领导是个不作为的人,你做起事来,肯定是难度很大的。

针对onInterceptTouchEvent

咱们先看一下ViewGroup里onInterceptTouchEvent的源码:

 public boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.isFromSource(InputDevice.SOURCE_MOUSE)&& ev.getAction() == MotionEvent.ACTION_DOWN&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)&& isOnScrollbarThumb(ev.getX(), ev.getY())) {return true;}return false;}

根据一些逻辑,判断该返回true还是false,那这一次我们不管super.onInterceptTouchEvent,自己定规则!考虑到

1、将FatherLayout的onInterceptTouchEvent返回true,随后点击ChildView,日志如下:

我们发现事件被FatherLayout拦截了,没有往下传递。

2、将FatherLayout的onInterceptTouchEvent返回false、MotherLayout的onInterceptTouchEvent返回true,随后点击ChildView,日志如下

 我们发现事件被MotherLayout拦截了,但是我们发现,Down事件还是会往上传递!

所以,我们对onInterceptTouchEvent得出结论:

1、onInterceptTouchEvent返回true,事件被拦截,不会往下传递,但是还是会往上传递,且后续的Up事件不会传递下来,简单点说:接了活,但是没有能力干,也不分给下属,最后任务被上级领导收回了。

针对onTouchEvent

观察View的onTouchEvent源码得知,此方法处理一些点击事件等,所以我们不调用super.onTouchEvent,简单粗暴的返回true和false。

1、将FatherLayout的onTouchEvent返回true,随后点击ChildView,代码、日志如下:

override fun onTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG, "调用了父亲的onTouchEvent方法,事件为:${getActionName(ev?.action)}")return true}

我们看出,Down事件看不出什么,但是Up事件到FatherView的onTouchEvent后,没有继续传递

2、将FatherLayout的onTouchEvent返回false,将MotherLayout的onTouchEvent返回false,随后点击ChildView,代码、日志如下:

  override fun onTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG, "调用了母亲的onTouchEvent方法,事件为:${FatherLayout.getActionName(ev?.action)}")return true}

 我们发现,由于ChildView没有处理Down事件,然后传给了MotherLayout,但是母亲处理了Down,所以没有继续向上传递,而Up事件,也是在母亲的onTouchEvent这里截止了。

感觉onTouchEvent返回true和dispatchTouchEvent返回true的效果很类似啊,那是因为在View的dispatchTouchEvent中,有一行代码如下:

if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}if (!result && onTouchEvent(event)) {result = true;}}

result在最后会根据onTouchEvent的返回值来定,所以,如果dispatchTouchEvent 返回super.dispatchTouchEvent,且onTouchEvent返回true,那么dispatchTouchEvent像相当于返回了true,所以现象会和上面一致。

那么,我们将dispatchTouchEvent返回false,且onTouchEvent返回true,看一下效果:

3、将MotherLayout的dispatchTouchEvent返回false,onTouchEvent返回true,然后点击ChildView,代码和日志如下:

 override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG, "调用了母亲的dispatch方法,事件为:${FatherLayout.getActionName(ev?.action)}")super.dispatchTouchEvent(ev)return false}override fun onTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG, "调用了母亲的onTouchEvent方法,事件为:${FatherLayout.getActionName(ev?.action)}")return true}

可以得出:只要dispatchTouchEvent返回false,onTouchEvent返回true也是没有用的。你没有接这个任务,但是背地里偷偷去处理这个任务,最终,上级领导也是不买账的,因为你不是责任人!

接下来,整理最终结论:

1、dispatchTouchEvent管调度,这个不返回true,其他都免谈。

2、onInterceptTouchEvent管拦截,返回true就代表下层布局没资格处理任何事件。

3、onTouchEvent管处理,真正处理事件的地方。

本博讲述的,也只是三个方法比较浅显的理解,真正深入的理解,还是要进入系统类的源码中寻找~

这篇关于Android中的事件分发浅析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1131981

相关文章

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

禁止平板,iPad长按弹出默认菜单事件

通过监控按下抬起时间差来禁止弹出事件,把以下代码写在要禁止的页面的页面加载事件里面即可     var date;document.addEventListener('touchstart', event => {date = new Date().getTime();});document.addEventListener('touchend', event => {if (new

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Android 10.0 mtk平板camera2横屏预览旋转90度横屏拍照图片旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,拍照保存的图片 依然是竖屏的,所以说同样需要将图片也保存为横屏图标了,所以就需要看下mtk的camera2的相关横屏保存图片功能, 如何实现实现横屏保存图片功能 如图所示: 2.mtk

android应用中res目录说明

Android应用的res目录是一个特殊的项目,该项目里存放了Android应用所用的全部资源,包括图片、字符串、颜色、尺寸、样式等,类似于web开发中的public目录,js、css、image、style。。。。 Android按照约定,将不同的资源放在不同的文件夹中,这样可以方便的让AAPT(即Android Asset Packaging Tool , 在SDK的build-tools目

Android fill_parent、match_parent、wrap_content三者的作用及区别

这三个属性都是用来适应视图的水平或者垂直大小,以视图的内容或尺寸为基础的布局,比精确的指定视图的范围更加方便。 1、fill_parent 设置一个视图的布局为fill_parent将强制性的使视图扩展至它父元素的大小 2、match_parent 和fill_parent一样,从字面上的意思match_parent更贴切一些,于是从2.2开始,两个属性都可以使用,但2.3版本以后的建议使

Android Environment 获取的路径问题

1. 以获取 /System 路径为例 /*** Return root of the "system" partition holding the core Android OS.* Always present and mounted read-only.*/public static @NonNull File getRootDirectory() {return DIR_ANDR