Android事件分发机制

1.触摸事件及MotionEvent

在学习事件的分发机制前,我们要先了解下什么是触摸事件。触摸事件就是捕获触摸屏幕后产生的事件。比如当点击一个button的时候,通常就会产生两个或者三个事件——按钮按下,这是事件一;如果不小心滑动一下,这是事件二;当手抬起,这是事件三。Android为触摸事件封装了一个类——MotionEvent。只要是重写触摸相关的方法,参数一般都含有MotionEvent,这在接下来实例演示的时候可以看到。

MotionEvent里面封装了一些常用的东西,比如触摸点的坐标,可以通过event.getX()和event.getRaw()方法取出坐标点,也可以通过不同的Action来获取点击事件的类型(如MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE、MotionEvent.ACTION_UP等),进而实现不同的逻辑。因此,触摸事件其实就是一个动作类型加一个坐标而已

2.事件分发机制概述

我们知道,Android的View结构是树形结构,View可以放在一个ViewGroup里面,ViewGroup又可以放在其他ViewGroup里面,甚至还可能继续嵌套,这样布局嵌套可能会复杂,而我们的触摸事件就只有一个,到底该分给谁呢,子View和父ViewGroup可能都有可能需要对触摸事件进行处理,这就需要用到事件传递、拦截、处理机制了。

3.事件从触摸到View树流程

Android的事件产生是从我们触摸屏幕开始,经过WindowManagerService到达应用程序。

对于应用层的主要事件流程,如下面的流程图所示:
在这里插入图片描述

上面DecorView是经过了两次,第一次是调用DecorView的dispatchTouchEvent,它的源码是:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Callback cb = getCallback();
        return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)
                : super.dispatchTouchEvent(ev);
    }

Callback就是Window.Callback,Activity实现了这个接口。

在Activity的attach函数中,会调用window的setCallback,将Activity设置给Window。所以这里getCallback返回的就是Activity,最终会调用Activity的dispatchTouchEvent。下面看一下Activity的dispatchTouchEvent函数:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

在ACTION_DOWN的时候会调用onUserInteraction方法,然后调用Window(实际上是PhoneWindow)的superDispatchTouchEvent。

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

而DecorView的superDispatchTouchEvent为:

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

最终还是调用DecorView的父类的dispatchTouchEvent,DecorView的父类是FrameLayout,它没实现该方法,最终会调用ViewGroup的dispatchTouchEvent方法。从这里开始就进入view树的事件派发流程了。

由以上的分析可以知道,当一个点击事件产生后,它的传递可以概括为如下顺序:Activity->Window->View树,即事件总是先传给Activity,Activity再传递给Window,Window再传递给顶级View,之后进入View的事件分发机制。

4.View的事件分发机制的实例演示

在了解了事件分发机制后,下面我们通过具体实例来直观感受下。

这里我自定义了一个View命名为MyView和一个ViewGroup命名为MyViewGroup,MyViewGroup包含了MyView。

代码非常简单,只是重写了事件拦截和处理的几个方法,为了方便看结果,加上一些log而已。

对于ViewGroup,重写了如下三个方法:

class MyViewGroup(context:Context,attrs:AttributeSet):RelativeLayout(context,attrs) {
    
    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            0-> Log.d("Tag","MyViewGroup dispatchTouchEvent down")
            1-> Log.d("Tag","MyViewGroup dispatchTouchEvent up")
            2-> Log.d("Tag","MyViewGroup dispatchTouchEvent move")
        }
        return super.dispatchTouchEvent(event)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            0-> Log.d("Tag","MyViewGroup onTouchEvent down")
            1-> Log.d("Tag","MyViewGroup onTouchEvent up")
            2->  Log.d("Tag","MyViewGroup onTouchEvent move")
        }
        return super.onTouchEvent(event)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when(ev?.action){
            0-> Log.d("Tag","MyViewGroup onInterceptTouchEvent down")
            1-> Log.d("Tag","MyViewGroup onInterceptTouchEvent up")
            2-> Log.d("Tag","MyViewGroup onInterceptTouchEvent move")
        
        }
        return super.onInterceptTouchEvent(ev)
    }
}

对于View,重写了如下两个方法,MyView代码如下。

class MyView(context: Context, attrs: AttributeSet) : AppCompatButton(context, attrs) {

    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            0->Log.d("Tag","MyView dispatchTouchEvent down")
            1->Log.d("Tag","MyView dispatchTouchEvent up")
            2->Log.d("Tag","MyView dispatchTouchEvent move")
        }
        return super.dispatchTouchEvent(event)
    }
    
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            0-> Log.d("Tag","MyView onTouchEvent down")
            1->Log.d("Tag","MyView onTouchEvent up")
            2->Log.d("Tag","MyView onTouchEvent move")
        }
        return super.onTouchEvent(event)
    }
}

可以发现,View比ViewGroup少了一个onInterceptTouchEvent()方法,这个方法从名字就可以看出来是事件拦截的核心方法。View是不包含onInterceptTouchEvent()这个方法的,这也很容易理解,毕竟View下不能像ViewGroup一样包含View了,也就没有拦截这一说法。这里补充一点,Activity也没有onInterceptTouchEvent()方法。

点击MyView,我们来看看打出的log,如下:

在这里插入图片描述
可以看见,事件的传递顺序是MyViewGroup->MyView。事件传递的时候,先执行dispatchTouchEvent()方法,再执行onInterceptTouchEvent()方法。而事件的处理顺序则相反,是MyView-MyViewGroup,事件处理执行的是onTouchEvent()方法。

一句话概括,传递是从上往下,处理是从下往上冒泡。下面谈谈事件传递和处理的返回值,很好理解。

事件传递的返回值:True,拦截,不继续;False:不拦截,继续流程。

事件处理的返回值:True,处理完了,不用给上级ViewGroup处理了;False:不处理,给上级ViewGroup处理。

初始情况下,默认返回值都是false。上面的演示默认返回false,所以流程完整走完了。

这里给出一个流程图,方便我们理解这一过程:

在这里插入图片描述

注意流程图里省略去了dispatchTouchEvent()方法,虽然dispatchTouchEvent()方法是事件分发的第一步,但一般情况下,我们不太会去改写这个方法,所以这里暂时忽略。后面第五部分会再次提及dispatchTouchEvent()方法。

接下里我们试着改写下代码,让事件拦截。我们把MyViewGroup中的onInterceptTouchEvent()返回值改为true。

 override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when(ev?.action){
            0-> Log.d("Tag","MyViewGroup onInterceptTouchEvent down")
            1-> Log.d("Tag","MyViewGroup onInterceptTouchEvent up")
            2-> Log.d("Tag","MyViewGroup onInterceptTouchEvent move")
        
        }
        //return super.onInterceptTouchEvent(ev)
     		return true
    }

同样点击MyView,看看打印出的log,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6ws2Zfa-1592483788418)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200615190348424.png)]

可以看到,结果和我们前面分析的一样,事件已经被外层MyViewGroup拦截了,MyView接收不到触摸事件。我们也可以看到,MyViewGroup只处理了down事件,move和up事件并没有处理。因为我们在MyViewGroup的onTouchEvent()返回默认的false,表示不处理此事件。那么这个事件就会往上级传递,这里MyViewGroup已经顶级View了,所以事件传递到了Activity去处理。

下面我们来看看事件的处理。我们再来改写一下代码,这次只修改MyView中的onTouchEvent()方法的返回值为true。

 override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            0-> Log.d("Tag","MyView onTouchEvent down")
            1->Log.d("Tag","MyView onTouchEvent up")
            2->Log.d("Tag","MyView onTouchEvent move")
        }
        //return super.onTouchEvent(event)
     		return true
    }

同样点击MyView,看看打印出的log,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aybGv4iZ-1592483788421)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200615190357198.png)]

可以看到,结果和我们前面分析的一样,事件已经被MyView处理了,不用传递给上级ViewGroup了,所以MyViewGroup的onTouchEvent()方法不会执行。由于事件被MyView处理了,所以MyView整个事件序列都会执行,所以可以看到down、move、up事件都有。这里可能大家会有个疑问,为什么MyViewGroup的dispatchTouchEvent()和onInterceptTouchEvent()也会执行完整时间序列呢?这里需要注意一点,应该把ViewGroup以及它所包含的子View都看作是这个ViewGroup的一部分,对于一个ViewGroup是否会处理一次事件,应该是包含了它的子View是否也处理

5.问题解答

(1)修改button的onTouchEvent返回值为false,事件是否会继续派发给button?

不会,button的onTouchEvent返回值为false的话,表示button不处理这个事件,那么button的dispatchTouchEvent()和onTouchEvent()只有down事件会相应,然后将此事件向上级View传递。

(2)派发DOWN事件给button时,onTouchEvent返回true。然后派发MOVE事件给button,onTouchEvent返回false。那么button可以接收到后续MOVE和UP事件吗?

可以,button的onTouchEvent返回true,表明button想处理此事件,那么整个事件序列都会派发给button,所以button可以接收到后续MOVE和UP事件,不关心move事件返回什么。

(3)修改button的dispatchTouchEvent返回值为false,事件是否会继续派发给button?

不会,这里需要注意一下dispatchTouchEvent()方法的返回值:

return true表示该View内部消化掉了所有事件
return false事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费(如果本层控件已经是Activity,那么事件将被系统消费或处理)
return super.dispatchTouchEvent(ev)事件将分发给本层的事件拦截onInterceptTouchEvent 方法进行处理

(4)修改button的dispatchTouchEvent返回值为true,onTouchEvent返回值为false,事件是否会继续派发给button?

会,可参考第四点,dispatchTouchEvent()方法return true的话,表示内部消化掉事件,所以事件会派发给button,程序运行结果如下,可以看到,事件序列都被处理了。

**[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PLraeMGo-1592483788427)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200615194556157.png)]**

(5)纵向的listview中包含一个横向的pageview,如何处理滑动冲突?

当上下滑时,我们需要让外部的listview拦截点击事件,当左右滑时,需要让内部pageview拦截点击事件。这里提供一种解决方法,我们可以通过水平方向和竖直方向的滑动距离差来判断,比如竖直方向滑动的距离差大就判定为竖直滑动,否则为水平滑动,进而执行相应的拦截策略。

6.总结

(1)如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发。

(2)可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法。

(3)一个点击事件产生后,它的传递过程如下:Activity->Window->View。顶级View接收到事件之后,就会按相应规则去分发事件。如果一个View的onTouchEvent方法返回false,那么将会交给父容器的onTouchEvent方法进行处理,逐级往上,如果所有的View都不处理该事件,则交由Activity的onTouchEvent进行处理。

(4)如果某一个View开始处理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),则同一事件序列比如接下来进行ACTION_MOVE,ACTION_UP,则不会再交给该View处理。

(5)ViewGroup默认不拦截任何事件,Android源码中Viewgroup的onInterceptTouchEvent方法默认返回false。

(6)诸如TextView、ImageView这些不作为容器的View,一旦接受到事件,就调用onTouchEvent方法,它们本身没有onInterceptTouchEvent方法。正常情况下,它们都会消耗事件(返回true),除非它们是不可点击的(clickable和longClickable都为false),那么就会交由父容器的onTouchEvent处理。

(7)点击事件分发过程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent–>OnClickListener的onClick方法。也就是说,我们平时调用的setOnClickListener,优先级是最低的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值