Android的事件分发——附带学习过程与感想

2,225 阅读15分钟

Android的事件分发

ps:本博客是笔者自己根据官方开发文档一步一步自己学习记录下来的过程,可能会有点错误,不过我感觉大部分应该是正确的,中途思维有点跳跃,最后因为实践的时候遇到一点小问题,导致我当时都在怀疑自己,所以又多看了几遍文档的函数介绍。

事件分发

说到事件分发机制我第一反应会想到Dom的事件流,早期的IE和网景的事件流是相反的。(IE的事件流是我们常常听说的事件冒泡,从下至上;网景的事件流则相反,为事件捕获)

在现在浏览器都使用的Dom2事件监听方法中,实现了事件的捕获与事件冒泡,但只能二选一,默认为事件冒泡,所以我们经常听到的说法是事件冒泡。

再来说Android,在Android中我们称这为事件的分发机制,他的分发机制与dom2很相似,不过是它两个事件都启用,就是一个U型的分发机制。

查资料的过程

我上开发文档中去搜索了一下“dispatchTouchEvent”,发现“ViewGroup、Activity、Dialog、View” 中有着一模一样的一个方法。

https://developer.android.com/s/results?q=dispatchTouchEvent

ViewGroup的点击事件

先不管上面的那几个,我们查看一下第四个官方文档, “Manage touch events in a ViewGroup | Android Developers” 看标题他正好是我们想要去了解的。

每当在ViewGroup的表面上检测到触摸事件时(包括其子级的表面),都会调用 onInterceptTouchEvent() 方法。如果 onInterceptTouchEvent() 返回true,则将MotionEvent拦截,这意味着它不会传递给子级,而是传递给父级的onTouchEvent()方法。

该onInterceptTouchEvent()方法使父母有机会在其孩子之前看到任何触摸事件。如果true从返回onInterceptTouchEvent(),则以前处理触摸事件的子视图将收到ACTION_CANCEL,并且从该点开始的事件将发送到父级的onTouchEvent()方法进行常规处理。onInterceptTouchEvent()当false事件沿视图层次结构行进到通常的目标时,它们也可以返回并监视事件,事件将由其自己处理 onTouchEvent()。

Activity、ViewGroup、View 的关系

才接触事件分发(原来接触过一次是在ViewPage制作轮播图的时候,现在可以使用ViewPage2了),看完第一段真是一头雾水,自己或子集的onInterceptTouchEvent()方法?父级的onTouchEvent()方法?这一看就知道ViewGroup是一个中间商或零售商的角色啊,这需要我们了解一下Activity、ViewGroup、View 的关系了,正好是上面四大天团的三个。(Dialog我们就不管了)

Android视图间的关系

其实很多知识点是相通的,一步一步来,就像建造者模式一样,日积月累,最后就会做出一份完美的蛋炒饭。上面这个关系图呢是我在了解Activity的绘制流程的时候,根据当时整理的流程图画出来的。

扩展1

Android的绘制流程

有兴趣的话可以去了解一下Activity的启动流程。扯远了,后面我要是联想到其他地方去大家就将就复习或者预习一些知识点,再回到我们的事件分发,从图3中我们可以看到,一个用户交互的页面的最上层的是Activity,我们的四大组件之一,我们在AndroidManifest配置中注册Activity的时候可以加一个theme属性(android:theme=“@android:style/Theme.Dialog”)将一个Activity设置为窗口模式,所以我们就先不管上面那个Dialog了,先把它当作一个Activity吧。(弹出Dialog就把它当成一个新的Activity,后面那个可见Activity处于onPause状态,是不能进行交互的)

Android的分发机制

有同学可能会问什么是Android的事件分发机制? 我理解的事件分发机制就类似于在学校的项目组中,老师有了一个新需求。然后处理情况会有几种。(我先罗列出我们等等会用到的几种情况) 老师会将这个需求告诉前端组组长与后台组组长,组长根据这个需求去做安排。

  1. 如果组长感觉这个需求有点难,可能下面的人做不好,就自己接下了这个活,做好后组长直接就给老师反馈了,下面的人啥都不知道也就过去了;
  2. 如果这个需求组长感觉到下面的同学可以做,就指派给下面的一个同学,这个同学处理好了,给组长反馈,然后组长就给老师反馈做完了;
  3. ...................

分发机制的个人理解

前提:几个视图嵌套在一起
问题
这个操作事件(点击、移动)被处理了(谁处理的?) ——> View(View是怎么知道这个事件的?) ——> ViewGroup告诉他的(ViewGroup是怎么知道这个事件的?) ——> Activity告诉他的(Activity怎么知道的?) ——> 用户点击了屏幕 (倒回去看,这不就正是一个用户点击到系统处理的流程吗?)

这个事件处理完了(View:我要告诉谁?) ——> ViewGroup(收到)——> Activity(收到)——> 用户(哇,这个交互好棒)

结果:我们将上面的合在一起不就是一个完整的事件处理吗?他的传递过程就是事件的分发机制。

图示:Activity <==> ViewGroup <==> View

ps:我又想了一下最先知道用户点击事件的为什么不是那个View,为什么不是View收到事件处理完一层一层的返回上来,拦截是上层的对下层已经处理过的事件不进行上报。(这样就和Dom事件流的冒泡事件一样了) 我有个大胆的理解是,你在网页中,你使用的是鼠标,鼠标是在屏幕里面的,就可以把它想成一个最底层的组件,只是z-index最高就好;而在手机中,点击的一般是我们手指,你只能点到屏幕,他最开始收到信号的只能是屏幕,然后只能根据图3往下找,所以事件传递的顺序应该就是上面那个图示一样。

分发机制的三大方法

android的事件分发最重要的三个方法是“dispatchTouchEvent()”、“onInterceptTouchEvent()”、“onTouchEvent()”,分发、拦截,处理与我们上面🌰中的人物操作简直一模一样。

说了那么多,都是自己根据大脑思维的惯性去猜想的,还需要实际去验证,我们撸段Demo实践一下。

角色
Activity(老师)
ViewGroup(组长)
View(组员)

动作: 我们去Activity、ViewGroup、View中查看一下这三个方法的介绍及用法。

  • 发现这三个方法不是每个组件都具有的,Activity与View中无onInterceptTouchEvent()方法,确实在现实生活中“程序员”(View)他没有下面的人手去分配了,任务到他这就结束了,不管能否做出来。那有人会问老师呢?这个看了下面的分发(dispatchTouchEvent)介绍就知道了。

分发(dispatchTouchEvent)

  • 适用群体:Activity、ViewGroup、View (三个都具有这个方法)
  • 虽然都有该方法,但Activity中对该方法的介绍就与另外两个不一样,不愧是四大组件之一。
  • 如果消耗了此事件,返回true。
  • 如果事件是由视图处理的,返回true,否则返回false。
  • Activity:被调用来处理触摸屏事件。您可以重写此设置,以在所有触摸屏事件被发送到窗口之前拦截它们。对于应该正常处理的触摸屏事件,请确保调用此实现。soga,他是最初的源泉,就像“老师一样”,他收到一份用户的需求的时候,会先自行判断是否需要去实现这个需求,不需要去实现他就不用分配任务下去,分配任务这就阻塞了,这也就解释了为什么Activity中没有onInterceptTouchEvent()方法了,可以理解为他与dispatchTouchEvent()方法合并了。
  • ViewGroup、View:将触摸屏运动事件向下传递到目标视图,如果是目标视图,则传递到该视图。

拦截(onInterceptTouchEvent)

  • 这已经基本说了,View没有权限拦截,Activity没有必要拦截,我们就看他在ViewGroup中如何表现的就好了
  • 返回true可以从子元素中窃取动作事件,并通过onTouchEvent()将它们分配给这个ViewGroup。当前目标将接收一个ACTION_CANCEL事件,这里不再传递任何消息。
  • 哇!一开始我以为就他一个人有这个方法,简单了解下就好,没想到他介绍那么长,看完后感觉是个重点,事件的分发我们大概了解其流程,但那只是基础的流程,一些特殊的情况是拦截机制引起的,搞懂他应该就能清楚各种情况的事件处理流程了。(其实和我们生活中的事件处理流程十分相似的,主要是生活中的处理方式比较多,不同的公司,不同的人的思维不同,这才有了规章制度,也可以叫协议,统一下思维,就不会那么乱了)
  • 实现此方法来拦截所有触摸屏动作事件。这允许您在事件发送给孩子时查看它们,并在任何时候获得当前动作的所有权。
  • 使用这个函数需要注意,因为它与View.onTouchEvent(MotionEvent)的交互相当复杂,使用它需要以正确的方式实现这个方法和这个方法。事件将按以下顺序接收:
    • 您将在这里接收down事件。
    • down事件将由这个ViewGroup的一个子View组处理,或者交给您自己的onTouchEvent()方法处理;这意味着您应该实现onTouchEvent()来返回true,这样您将继续看到手势的其余部分(而不是寻找父视图来处理它)。另外,通过从onTouchEvent()返回true,您将不会在onInterceptTouchEvent()中收到任何后续事件,并且所有的触摸处理都必须在onTouchEvent()中正常发生。
    • 只要从这个函数返回false,接下来的每个事件(直到并包括最终的up)都将首先在这里传递,然后传递到目标的onTouchEvent()。
    • 如果您从这里返回true,您将不会接收到任何以下事件:目标视图将接收到相同的事件,但动作为MotionEvent.ACTION_CANCEL,并且所有进一步的事件将被传递到您的onTouchEvent()方法,并且不再出现在这里。
    • ps:MotionEvent.ACTION_CANCEL代表着事件已经终止,你将不会再收到这个事件的任何消息,可以看成一个上升事件。
  • 官网的介绍就先到这里,等等实操一下,验证一下然后理清一下这几个函数的调用,因为我们刚刚只是理清楚了android的事件分发的流程,但还有后续的操作,就像老师给了需求,后面需求的变更是需要怎么处理,是老师再给组长说呢?还是直接与接手这个需求的程序猿进行沟通?我们也可以先猜测一下,我猜的是直接找程序猿,因为我说了这来源于我们现实生活,在我们项目组里,是把bug、需求放在禅道里,老师看到任务指派的会直接去找他,一句话经过多个人的描述会变成另外的一句话,这样直接又快捷,我认为这种点击事件的处理需要的是快捷,要不带给用户的体验是糟糕的,这样这个系统应该是会被市场淘汰的。(下面看完onTouchEvent的介绍我们就来实践验证下)

处理事件(onTouchEvent)

  • 查看了官方文档后,又发现了那个问题,Activity他的方法介绍就是与其他妖艳的*货不一样,谁叫他是老大呢!
  • 果然,看简介就知道它是老大了,地位比别人高,同样的工作做的比别人少。可能少而精吧!
  • Activity:当触摸屏事件未被其下的任何视图处理时调用。这对于处理发生在窗口边界之外的触摸事件是最有用的,因为在窗口边界之外没有视图可以接收触摸事件。
  • ViewGroup、View:实现此方法来处理触摸屏运动事件。如果此方法用于检测单击操作,则建议通过实现和调用performClick()来执行操作。这将确保一致的系统行为。后面包括的拿三项有点没看懂,没关系,他说了是建议,大概就是哪些东西不建议在这个方法里面写,第三条勉强理解下,就是在处理Android无障碍服务中的按钮,正常情况下只要是View是clicked的,就可以使用AccessibilityNodeInfo.ACTION_CLICK;第二条可能是监听电话的时候,就是你打电话的时候吧;第一条可能是你调节音量的时候。应该不用管这些,你就记得这个方法用来处理触摸屏运动事件(MotionEvent.ACTION_MOVE )就好。

方法总结

到这我们就了解了这三个方法的用途,以及一些注意事项,和我最初的想法有些不同(一开始我以为,dispatchTouchEvent先监听然后如果不是自己处理就分发,是的话就到onInterceptTouchEvent中去,onInterceptTouchEvent拦截就表示自己接受了,拦截后子控件就收不到消息了,就到onTouchEvent中开始工作了,onTouchEvent处理事件都在这,不管是接到任务,还是做的过程,甚至做完,返回true就代表做完了)。现在改变下思路,人最可怕的就是知错不改。

我重新整理了下对这三个方法的认识:

  • dispatchTouchEvent()方法:如果事件是被该视图处理的就返回true,否则返回false。
  • onInterceptTouchEvent()方法:用来拦截事件的,如果你不拦截(返回false)的话,以后要更改的需求就直接跳到你这,这在多层嵌套的里面就省掉了上面好几层。如果拦截(返回true)以后的操作就直接跳到onTouchEvent中去处理,因为你接下了开始干活了,改需求直接去找那个在办公区工作中的你,你已经不再拦截的会议室了。
  • onTouchEvent()方法:处理后续的操作,最常见的就是Move,直到UP结束的时候。返回false就证明你没做好,可能虽然你接了任务,但你做不出来,会一层一层返回直到有一个返回ture的,然后后续的操作就会直接到那个返回true的onTouchEvent中执行,如果下面的一直没有返回true的就会执行Activity中的onTouchEvent()。

代码

代码:
Activity(MainActivity.kt)

class MainActivity : AppCompatActivity() {

    val ROLE_TAG:String = "老师"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"有个新需求,分配新需求")
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_UP -> Log.i(ROLE_TAG,"我靠!这个需求这么南吗?一个人都做不了?")
        }
        return super.onTouchEvent(event)
    }

}


ViewGroup(GroupLeader.kt)

class GroupLeader : LinearLayout {

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet) : super(context,attrs)
    constructor(context: Context?, attrs: AttributeSet, defStyleAttr: Int):super(context, attrs, defStyleAttr)

    val ROLE_TAG:String = "组长"

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"清楚需求了")
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_MOVE -> Log.i(ROLE_TAG,"正在努力工作")
            MotionEvent.ACTION_UP -> Log.i(ROLE_TAG,"做好了")
        }
        return true
    }
}


View(Programmer.kt)

class Programmer : View {

    val ROLE_TAG:String = "程序员"

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet) : super(context,attrs)
    constructor(context: Context?, attrs: AttributeSet, defStyleAttr: Int):super(context, attrs, defStyleAttr)

    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"知道要求了,我马上做")
        }
        return super.dispatchTouchEvent(event)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_MOVE -> Log.i(ROLE_TAG,"正在努力搬砖。。。")
            MotionEvent.ACTION_UP -> Log.i(ROLE_TAG,"做好了,累死了")
        }
        return true
    }
}


结果:

实验结果

遇到的问题

这是最经典最基础的一个流程了吧,一开始还好好的在意料之中,但有个东西让我吃了好大的苦头,卡在这半天了。就是在重写这三个方法的时候他们的默认返回值都是super.dispatchTouchEvent(ev),在啥都不怎么修改的时候,最原始的代码是没有问题的。(当改为true或false都不行,在有种情况中,我打印了一下super的返回值,发现是true,但结果却出乎我的意料,我只是先调用了super并记录下了它返回的值,在用这个值作为当前函数的返回值,得到的事件结果却是不一样的)

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        var mbool:Boolean = super.dispatchTouchEvent(ev)
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"有个新需求,分配新需求" + mbool)
        }
        return mbool
    }

变为了组长未卜先知了,这让我有点纳闷了,然后我直接将函数返回值改为了true和false,不再调用super,两个的返回值都一样,但事件被消费了,没有往下传递了,只有使用super的时候进行了向下的传递。(这个问题目前那么多时间去了解,我就先将super当成一个返回值的状态吧)

分发机制流程图

其他流程是我一个一个更改每个函数的返回值为true或false或默认的super试出来的。画了一个事件分发与拦截的流程图。

Android事件分发