Android 事件分发:为什么有时候会出现事件冲突?事件的顺序是如何的?出现事件冲突如何解决呢?比如为什么左右可以滑动,而上下却不行?

目录:


一、为什么要学习事件呢?

1.在开发复杂的应用时,经常需要处理复杂的用户交互逻辑。学习事件分发机制可以帮助你更好地控制事件的传递和处理流程,从而解决一些复杂的交互问题,如滑动冲突、点击穿透等。

2.面试需要:事件冲突的原因是什么?在开发过程中,可能会遇到一些与事件处理相关的问题,如事件没有被正确传递、事件被错误地拦截等。了解事件分发机制可以帮助你更快地定位问题所在,并找到解决方案。


二、事件是什么?事件是如何触发的?

事件(Event)是用户与应用程序界面(UI)交互时发生的动作或情况的抽象表示。这些事件可以是由用户通过触摸屏幕、按键、旋转设备等方式触发的,也可以是由系统产生的,比如系统状态变化(如屏幕旋转、电量变化)等。

当事件发生时,Android系统会将该事件发送给注册了该事件监听器的组件。然后,事件监听器中的相应方法会被调用,执行开发者定义的事件处理逻辑。


三、事件冲突是什么:举例说明,比如ViewPage和Recyclerview一起使用,会出现左右不能滑动或上下不能滑动的情况

遇到的问题

2.1 ViewPager的代码

kt 复制代码
class MyViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return false
    }
}

2.2 RecyclerView的代码

kt 复制代码
class MyRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) {

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {

        return super.dispatchTouchEvent(ev)
    }
}

出现了上下可以滑动,但是左右不可以。到底是什么原因导致的呢?

首先我们先了解一下事件的流程是怎么样的。


四、事件的大致流程

事件产生的原因,如下图:1个事件的发生,通过触摸手机屏幕------>导电------>传感器------>Linux------>Activity里面,再传到ViewGroup,最终再传到 View。

那么Activity里面,我们需要关注dispaatchToucheEvent方法。这里需要注意,ViewGrouper的dispaatchToucheEvent和View的dispaatchToucheEvent是做着不同的事情。后面我们会重点讲解

Android事件的处理流程是一个从硬件到软件、从底层到高层的复杂过程,涉及到底层硬件、Linux内核、Android框架以及最终的应用层。

这里我们只需要关注ViewGroup的dispaatchToucheEvent,以及View的dispaatchToucheEvent。也是下面我们需要重点讲解的


五、事件分发和事件处理是什么意思?

5.1 事件分发

事件分发,指在安卓系统中,如何处理和传递用户交互事件的一套机制,当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。这也是事件分发存在的目的,控件太多,他不知道具体是哪一个,所以他需要通过分发的方式,递归去找到这个事件的处理者

5.2 事件处理

事件处理是指当事件被传递到相应的组件后,该组件执行具体的操作来响应事件的过程。如处理触摸事件、点击事件等。

下面我们一起看看源码,了解一下他是如何进行事件分发的,如何事件处理的,从中找出如何解决事件冲突的方案。


六、事件处理源码分析

我们先看看事件处理的源码,从比较简单的先开始。事件分发留到后面。

比如我们现在给一个控件设置了点击事件和触摸事件。如下:

kt 复制代码
  btnEvent.setOnClickListener {
            Log.d(TAG, "onClick: ")
        }
  btnEvent.setOnTouchListener { view, event ->
      Log.d(TAG, "OnTouch: "+event.action)

      false
  }
  1. 那么你觉得哪个会先执行,事件的优先级是如何的,以及你可以如何去拦截事件的处理?日志如下:

可以看到onTouch方法的优先度会高,为什么呢?我们看看源码:

在dispatchTouchEvent方法中,如果控件注册了onTouchListener,则会先执行onTouch回调方法。

kt 复制代码
btnEvent.setOnClickListener {
            Log.d(TAG, "onClick: ")
        }
  btnEvent.setOnTouchListener { view, event ->
      Log.d(TAG, "OnTouch: "+event.action)

      true
  }

如果onTouch方法返回true,表示该事件已被消费,不会继续传递;如果返回false,则事件会继续传递给onTouchEvent方法。

onTouchEvent方法会处理包括ACTION_DOWN、ACTION_MOVE、ACTION_UP等在内的事件。当事件为ACTION_UP时,如果控件是可点击的(如按钮),则会调用performClick方法,进而触发onClick方法。

2.该事件已被消费,不会继续传递

  1. 我们看看继续传递的情况,看看onClick在哪里

因为点击事件是松开后触发的,所以我们要找到松开的地方。




好的,事件处理的源码我们就看到这里,我们看看事件分发。


七、事件分发源码分析:我们会将源码划分为三块来讲

我们上面的例子,ViewPager+Recyclerview,Viewpager就是容器,而Recyclerview就是子view,下面我们就代入进去分析代码。

7.1 第一块源码:是否拦截子view,也就是,是否将事件分发给子view来出来,还是说自己处理


举例:如果我们的ViewPager的onInterceptTouchEvent方法返回了true,那么就拦截了子view,返回false,就不拦截子view,让他分发给子view处理事件。


7.2 第二块源码:不拦截子view,那么就开始遍历子view,看谁是否需要进行处理事件

如下是第二块代码,主要进行子view的事件分发,如果intercepted为false,那么就进来了。如果为true,那么第二块代码相当于什么都不执行,直接到第三块代码。


7.3 第三块源码:如果拦截了子view,那么就循环自己(ViewPager)是否需要处理事件,如果没有拦截,那么这里会看哪个子view需要进行事件的处理

7.4 具体案例

案例一:假如ViewPager里面直接返回true:RecyclerView的上下滑动事件,就不会得到执行。这是为什么?

kt 复制代码
class MyViewPager(context: Context) : ViewPager(context) {
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
     
        return true
    }
}

其实我们看第一块源码我们就已经知道了答案,如果onInterceptTouchEvent返回true,就会进行拦截,那么就不会进行子View的事件分发。[效果如下gif]

案例二:为什么Viewpager返回false,RecyclerView不用设置,左右不能滑动了,但是上下能滑动,为什么呢?[效果如下gif]

首先,我们看看源码,第一块肯定是通过了,我们看第二块。首先要拿到所有的子view,我们这里可能只有一个,但也有可能写多个,所以我们要拿到所有的子view,通过buildTouchDispatchChildList方法。
拿到所有的子view以后,我们要进行倒序处理

为什么呢?我们知道,我们写三个控件,他们是层层叠加的,比如Viewpager+RecyclerView,那么Viewpager是在最底部,RecyclerView则是在上面,Viewpager就是0,RecyclerView就是1,所以我们要倒序过来,处理最上层view,看他是否需要处理事件,如果不处理,才到Viewpager。

如下代码,是判断是否点到了view的位置。不满足,肯定是不需要执行事件咯。所以我们设置了点击事件,但是没点击到,肯定就不会执行。
如果ViewPager和RecyclerView都返回false,为什么viewpager的可以得到事件处理呢?因为他最终都会进行处理。一旦子view处理了事件,就不会在给父容器处理事件了。因为事件,始终只有一个人处理。

所以,如果ViewPager返回了false,而RecyclerView返回true,那么RecyclerView就能上下滑动,而ViewPager就不行了。

7.5 Down事件

Down事件其实就是我们上面说的这些代码。前面我们已经知道事件冲突的原因,因为事件,始终只有一个人处理。那么我们如何解决他呢,就需要我们了解move事件分发流程。

7.6 Move事件

我们先看看结论,看看是如何解决的。

kt 复制代码
class MyViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        if (ev?.action == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(ev)
            return false
        }
        return true
    }
}
kt 复制代码
class MyRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs) {
    var mLastX:Float = 0f
    var mLastY:Float = 0f

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        var x = ev?.getX()
        var y = ev?.getY()

        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                requestDisallowInterceptTouchEvent(true)
            }

            MotionEvent.ACTION_MOVE -> {
                val deltaX = (x!! - mLastX).toInt()
                val deltaY = (y!! - mLastY).toInt()

                if (Math.abs(deltaX)>Math.abs(deltaY)){
                    requestDisallowInterceptTouchEvent(false)

                }
            }

            MotionEvent.ACTION_UP -> {}
            else -> {}
        }
        mLastX = x!!;
        mLastY = y!!;
        return super.dispatchTouchEvent(ev)
    }
}

首先,我们先了解一下,起始事件,是MotionEvent.ACTION_DOWN,也就是按下事件,大家都同意吧,那么这个时候,我们不要拦截子View,所以我们就返回了false.

kt 复制代码
if (ev?.action == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(ev)
            return false
        }

当滑动的事件的时候,那么可能是子View要处理,也可能是父View要处理,所以子View里面是这样写的

kt 复制代码
 when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                requestDisallowInterceptTouchEvent(true)
            }

            MotionEvent.ACTION_MOVE -> {
                val deltaX = (x!! - mLastX).toInt()
                val deltaY = (y!! - mLastY).toInt()

                if (Math.abs(deltaX)>Math.abs(deltaY)){
                    requestDisallowInterceptTouchEvent(false)

                }
            }

            MotionEvent.ACTION_UP -> {}
            else -> {}
        }

requestDisallowInterceptTouchEvent(true)究竟是什么?如果子视图调用了这个方法,并且返回了 true,那么父视图将不会调用其 onInterceptTouchEvent(MotionEvent ev) 方法来尝试拦截这个触摸事件,也就是说子类一定会得到处理。除非后续的事件(如 ACTION_MOVE 或 ACTION_UP)改变了这个决策。所以我们在子View里面进行了处理,如果是左右方向,那么就可以让父容器去拦截,如果是上下方向,就不要让父容器拦截,让子view去进行处理。

为什么可以这样呢,因为Move事件不是触发一次,而是多次。

我们进入requestDisallowInterceptTouchEvent方法里面去看看。

这里设置了条件以后,我们可以到第一块代码这看看,因为第一块代码是决定是否分发事件给子View。

强行使其给子View进行事件的分发。所以我们需要requestDisallowInterceptTouchEvent分发来进行处理。

好了,那么事件的介绍就分享到这里了~

八、学习总结

事件分发的源码第一次看比较复杂,需要反复观看,才能理解,并且结合控件案例去测试,效果会更好。

相关推荐
深圳智物通讯1 小时前
安卓开发板_联发科MTK开发板使用ADB开发
android·adb·安卓开发板
居安思危_Ho1 小时前
【Android笔记】Android Studio打包 提示Invalid keystore format
android·笔记·gradle·android studio·android 打包
lareina_yy3 小时前
Android Studio新建工程(Java语言环境)
android·java·android studio
薛文旺4 小时前
Android MediaProjection录屏权限处理
android
jim_dayday_up4 小时前
android BLE 蓝牙的连接(二)
android
Clank的游戏栈4 小时前
Unity3D Android多渠道极速打包方案详解
android
向晚流年4 小时前
Android Graphics 显示系统 - 图层的生命周期 Layer Lifecycle
android
程序喵D6 小时前
MapBox Android版开发 5 示例清单
android·mapbox
偶是老李头6 小时前
Android - NDK:在Jni中打印Log信息
android·jni·android ndk log·jni log
LittleLoveBoy6 小时前
Android前台服务如何在后台启动activity?
android