Android事件分发从Viewgroup到View

                      Android事件分发从Viewgroup到View

1.      参考文档

https://blog.csdn.net/guolin_blog/article/details/9097463

https://www.jianshu.com/p/38015afcdb58

   https://www.jianshu.com/p/34cb396104a7(深度理解)

2.      认识

3. 思路分析 (细品红色字体)

当事物分发开始时,首先我们应该知道Activity的最顶层窗体是PhoneWindow,PhoneWindow的最顶层ViewDecorView。最先调用的就是DecorViewsuperDispatchTouchEvent()

 

在里面调用了父类FrameLayout的dispatchTouchEvent()方法,而FrameLayout中并没有dispatchTouchEvent()方法,所以我们直接看ViewGroup的dispatchTouchEvent()方法,我们知道ViewGroup继承View,其主要功能容纳组件的容器,以及控制组件的布局、属性等。

于是我们可以粗略的了解下ViewGroup的dispatchTouchEvent()方法

   我们可以看到一个其重要的方法,我把它叫做容器的事物拦截器onInterceptTouchEvent也就是注释中的 Check for interception。值得注意的是这里是两层判断,也就是有两个嵌套的if。在第一个if中,会确定触摸事件是否为down和mFirstTouchTarget是不是为空。其中mFirstTouchTarget表示的是事件是不是又子View消费了的,如果已经被消费,就不会为null。在第二个if中就会判断是否设置了FLAG_DISALLOW_INTERCEPT这个 标记符,这个FLAG_DISALLOW_INTERCEPT标记符的作用就是子View干涉父容器对事件的分发。如果子View设置了这个标记符,就不会调用onInterceptTouchEvent方法,从而intercepted为false。

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

 

这是viewGroup事件分发中重要的一个方法,if中有两个条件,如果DOWN事件最终不是由子View消耗掉的,那么显然mFirstTouchTarget将为null,所以也就不用判断了,直接把intercept设置为true,此后的事件都是由这个ViewGroup处理。

所以我们要在ViewGroup彻底拦截消息时应该在Down的条件下返回true

如果其返回true着不在为其容器里的组件分发事件,即自己消费。代码如下:

如果其返回false,且其可以点击。则进入这个if中会遍历容器组件。


但是我们要知道这个遍历的是ViewGroup中所有的View(包括了有些重叠View的),这个时候就有了点击事件的穿透问题了。也就是说,如果ViewGroup中子ViewA与子ViewB有一个重叠部分C,而我点击的部分就是C,那么谁响应?谁先响应?

我们看上图有一行 

final ArrayList<View> preorderedList =buildTouchDispatchChildList();

buildTouchDispatchChildList()获取一个视图组的先序列表,通过虚拟的Z轴来排序。里面的核心代码

 

如果你没有对子控件设置elevation或者translationZ, 那么就会返回空, 如果设置了的话那么返回一个根据Z轴大向后面排序的列表, 一般情况下都是没有设置的,如果你设置了Z轴的值, 那么在Z轴的值越大就越优先分发事件.

这里给一个小结:

你点击的区域有两个View, A和B, 它们大小相同, 位置重合

1.   如果你对A或B设置了elevation或者translationZ,那么会先分发给Z轴上值较大的View, 不设置的View默认是0, 此时index只能是xml上添加的顺序

2.   如果你没有设置Z轴的值, 设置了setChildrenDrawingOrderEnabled(true)和实现了父控件的getChildDrawingOrder()方法, 那么顺序就是由这个方法里的实现确定了, 例如在这个方法传入参数是0的时候返回的是A的index, 传入1的时候返回的是B的index, 即使实际上A的index比B大, 那么事件也会先传递给A

3.   如果你什么都没干, 就是正常使用, 那么分发顺序就是子控件在xml中的顺序的倒序, 就是后添加的先分发, 实际上如果两个控件重合了, 你看到的也是后添加的控件, 那么自然点击事件也是先分发给后添加的控件了(我认为是在解析xml后,将所有view对象按布局从上到下的顺序放入mChildren中,在事件分发时,反向遍历,就导致了上面的现象)如下图

回到前面穿透的问题

有上面小结3 我们就可以明白如果点击重叠部分,则正常情况下,是后添加的控件响应。好了,新的问题又来了,那我想要下面的控件响应怎么办??当然就是让遮挡的View不消费事件,接着遍历前面提到的mChildren。想明白了解决的办法,就要去源码看看如何让view不消费。

当ViewGroup把事件分发给了一个子view后,它会通过点击坐标确定是否点击了该view的消费事件范围

然后通过dispatchTransformedTouchEvent将事件分发给子View。

在上面的方法中我们需要注意两个地方:

1.取消操作

这块处理当我们ACTION_DOWN 后,不松手,move手指,当我们move超出了该view大小的范围时,执行取消操作。

2.      正常点击操作

我们可以看到这里调用了子View的dispatchTouchEvent终于把事件分发出去了。

--------------------------------------------------------------------------------------------------------------------------------

   欢迎进入view….

 首先我们先看view的dispatchTouchEvent里的重要代码

在第一个if中有四个要求,li这个是代理对象,运用了一种设计模式---hook(俗称钩子),用来代理监听事件对象,第二个是你设置点击控件setOnTouchListener,所以前面两个参数只要你设置了监听事件,不会为空,而第三个只要你没设置Android:enable=”false“ 那也为true,于是最后一个参数才是最重要的, 这是我们自己重写的方法。

是不是很熟悉,它返回的值取决于你自己是否执行后面if中的onTouchEvent()方法。。


现在补充下onTouch与onTouchEvent的区别:

  由于他们都将event做为传入参数,所以他们能做的事,基本都是一样的。所以主要还是用法的区别。我们可以理解为onTouch是留给我们对手势个性化处理的口子。而onTouchEvent是系统的手势处理方式。

所以当我们不要系统处理手势时,onTouch返回true,表示事件消费,反之亦然。

我接着看onTouchEvent看看系统如何处理手势的,

其核心代码

如果你的clickable 、longClickable、contextClickable都为false 时(忽略tooltip一般不会用到为false),则不会消费事件。

注意这点解决透传问题关键。如果clickable true ,我们可以知道会去判断点击的状态等等不细说,需要注意的是一但进入if语句就会返回true,表示事件被消费。。。

还有一点要提下:在进入系统处理手势的有一个方法performClick()

如果你注册了setOnClickListener,这会在这里被调用。。

好累。。。。。终于分析完了。。。接着我们就可以解决上面穿透问题了。。。

案例:

布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusableInTouchMode="true"
    android:orientation="vertical">
        <Button android:id="@+id/button1"
            android:background="#FF0000"
            android:layout_width="100dp"
            android:clickable="true"
            android:layout_height="100dp"
            android:text="按钮1" />
        <Button android:id="@+id/button2"
            android:background="#000000"
            android:layout_marginTop="30dp"
            android:enabled="true"
            android:clickable="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="按钮2" />
</RelativeLayout>

问题:如果点击黑色区域,如何让黑色不消费,红色消费????

 注:黑色区域(button2),红色区域(button1)

先回想下前面的知识,只要让view.dispatchTouchEvent返回false,则表示不消费事件。

具体有这么几种方法:

a.      注册button1的onTouch监听, 且button1的enable属性为true

1.不注册button2的onTouch监听,且设置button2.setClickable(false)或在xml的button2属性设置Android:clickable=”false“,Android:enable=”true”

2.  注册button2的onTouch监听,设置button2属性Android:enable=”false”,虽然简单,但是可能展示效果不是你想要的,如下图

a-2

b.      注册button1与button2的onTouch监听,且button1、button2的enable属性为true

1.      button2.onTouch返回false,button2属性设置Android:clickable=”false“,且不能给button2设置onClickListener监听。

2.      button2.onTouch返回false, button2设置onClickListener监听,且在监听后面加上button2.setClickable(false)

   

    注:在b的前提下,当我们设置了button2.setOnClickListener后,系统会自己再设置一下button2.setClickable(true)。如果我们不在其监听后面设置button2.setClickable(false),这会让我们进入view.onTouchEvent中,由于系统设置button2.setClickable(true),所以clickable=true ,前面讲过这会使view.onTouchEvent返回true,从而导致view.dispatchTouchEvent返回true,使得button2消费了事件。。。

总结:

1.      前面红色字体

2.      ViewGroup核心方法(按执行顺序展示):dispatchTouchEvent-àonInterceptTouchEventàonTouchàonTouchEvent

3.      View核心方法(按执行顺序展示):dispatchTouchEvent àonTouchàonTouchEvent

4.      view.dispatchTouchEvent返回false,则表示不消费事件。反之亦然。


希望这些对你们有帮助吧,如果有错请提醒我,共同进步,谢谢。。。



猜你喜欢

转载自blog.csdn.net/silently_frog/article/details/80689637
今日推荐