Android触摸事件解析(自定义DrawerLayout)

基础知识概述:

首先来了解三个方法:

  • dispatchTouchEvent(MotionEvent ev) 功能:事件的分发
  • onInterceptTouchEvent(MotionEvent ev) 功能:事件的拦截
  • onTouchEvent(MotionEvent ev) 功能:事件的消费
    可以看到这三个方法里面都包含了同一个类:MotionEvent 类,这个类里面包含了很多的代表触摸动作的常量,比如:
    • MotionEvent.ACTION_DOWN: 代表手指按下的动作
    • MotionEvent.ACTION_MOVE: 代表手指滑动的动作
    • MotionEvent.ACTION_CANCEL: 代表手指动作取消的动作
    • MotionEvent.ACTION_UP: 代表手指动作抬起的动作
      这里我们也是围绕着这四个动作进行解析,因为:** 从手指触摸屏幕,到手指离开屏 幕,会经历一次 ACTION_DOWN、若干次ACTION_MOVE或ACTION_CANCEL、和一次ACTION_UP,这也被称为一个事件序列。**

之后我们在说一下能够触发这三个方法传递触摸事件的容器:

  • Activity 事件就是从它开始往下分发的,这个容器里包含了dispatchTouchEvent()和onTouchEvent(),也就是拥有分发事件和消费事件的能力。
  • ViewGroup 它接收Activity的事件分发,并且可以选择是否继续往下传递还是自己直接进行消费,所以它包含了上面三个方法,具有事件的分发、拦截、消费功能。
  • View 如果它的父容器不进行拦截的话,它接受父容器的事件分发,并且判断是否消费事件,并且要把结果再通过dispatchTouchEvent()方法返回给父容器。
    用表格的形式展现下容器和方法之间的关系:

事件相关方法 | 方法功能 | Activity | ViewGroup | View
---||||---|||---
public boolean dispatchTouchEvent | 事件分发 | Yes | Yes | Yes
public boolean onInterceptTouchEvent | 事件拦截 | No | Yes | No
public boolean onTouchEvent | 事件消费 | Yes | Yes | Yes

  • 分发 : dispatchTouchEvent如果返回true,则表示在当前View或者其子View(子子...View)中,找到了处理事件的View;反之,则表示没有寻找到。
  • 拦截: onInterceptTouchEvent如果返回true,则表示这个事件由当前View进行处理,不管处理结果如何,都不会再向子View传递这个事件;反之,则表示当前View不主动处理这个事件,除非他的子View返回的事件分发结果为false。
  • 消费: onTouchEvent如果返回true,则表示当前View消费了该事件;反之,则表示当前View没有消费该事件,返回到父控件处理(如果有父控件的话)

代码示例

需求:我想用DrawerLayou类来做滑动侧边看,但是有一个问题,就是当侧边栏打开的时候,主屏幕会变暗并且不能触发点击事件,我现在想通过自定义的形式解决这一问题,并且当侧边栏打开的时候点击主界面的时候侧边栏不会自定收起。

先看下效果图:


图片.png

再看下布局的资源文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:titleTextColor="@color/colorAccent" />
    
    <com.mllwf.slidesidebar.TEDrawerLayout
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.mllwf.slidesidebar.CustormFrameLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.mllwf.slidesidebar.CustormButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center"
                android:onClick="toCanSlieDrawer"
                android:padding="16dp"
                android:text="是否能滑动" />
        </com.mllwf.slidesidebar.CustormFrameLayout>

        <RelativeLayout
            android:id="@+id/nav_view"
            android:layout_width="150dp"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:background="@android:color/darker_gray">

            <Button
                android:id="@+id/btn_one"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/colorPrimary"
                android:text="这是顶部按钮"
                android:textColor="@color/colorAccent" />

            <Button
                android:id="@+id/btn_two"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:background="@color/colorPrimary"
                android:text="这是中间的按钮"
                android:textColor="@color/colorAccent" />

            <Button
                android:id="@+id/btn_three"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:background="@color/colorPrimary"
                android:text="这是底部按钮"
                android:textColor="@color/colorAccent" />
        </RelativeLayout>
    </com.mllwf.slidesidebar.TEDrawerLayout>
</LinearLayout>

简要说明(请结合效果图和布局文件):

  • 为了能让侧边栏在标题栏的下面,我在DrawerLayout布局放在了ToolBar布局的下面,而根布局我用的是线性布局。
  • 可以看的我自定义了DrawerLayout类,这样做为了重写上面三个触摸事件分发,并且方便打印Log日志
  • DrawerLayout容器里面的第一个布局代表的是主界面布局,这里我用的是自定义的FrameLayout这样是也是为了重写事件分发和打印日志(注意重写方法为了实现上面说的功能,和打印Log)
  • 在主界面布局里面我放了一个自定义的按钮这只是为了方便打印日志用。
  • DrawerLayout容器里面第二个布局代表的是侧边栏布局。

下面分别看下分别看些这几个自定义布局的相关代码:

  • MainActivity.class(Activity)


    Activity触摸事件方法.png
  • CustormFrameLayout(ViewGroup)


    ViewGroup触摸事件.png
  • CustormButton(View)


    View事件触摸方法.png

首先默认没有打开侧边栏,点击按钮查看日志信息:


图片.png

从上面的日志信息可以看到,是一层层的传递的,这里用一张图解释下上面的传递流程:


只考虑了Down和Up动作.png
只考虑了Down和Up动作.png

因为Up事件和Down事件是一样的过程,所以就不多说了。
从上面的Log和流程图我们可以得出几点结论:
  • ACTION_DOWN事件从Activity#dispatchTouchEvent方法开始,然后一层层往下传递的,并且 ACTION_DOWN事件是否被消费的结果最终也是有dispatchTouchEvent传回到Activity中。
  • ACTION_DOWN事件传递至ViewGroup#dispatchTouchEvent方法,ViewGroup#onInterceptTouchEvent返回false,表示不拦截ACTION_DOWN,如果返回的是true,表示拦截不会再传递给子View,这个时候就会调用ViewGroup#onTouchEvent方法,判断是否消费事件,并回传给Activity的#dispatchTouchEvent方法
  • ACTION_DOWN事件传递到View#dispatchTouchEvent方法,在View#onTouchEvent进行执行,返回true,表示事件已经被消费,然后被回传到View#dispatchTouchEvent,之后回传到ACTION_DOWN事件的起点Activity#dispatchTouchEvent方法。如果这时候返回的是false的话,ViewGroup就是调用自己的onTouchEvent方法进行处理,如果依然返回false的话,Activity就会调用自己的onTouchEvent方法处理(返回false的情况后面解释)。
  • 如果某个View消费了ACTION_DOWN事件,那么这个事件序列中的后续事件也将交由其进行处理(有一些特殊情况除外,比如在序列中的之后事件进行拦截)

这里使用工作中的情况来模拟一下吧:老板(Activity)、项目经理(ViewGroup)、软件工程师(View)

  • 老板分配一个任务给项目经理(Activity#dispatchTouchEvent→ViewGroup#dispatchTouchEvent),项目经理选择自己不做这个任务(ViewGroup#dispatchTouchEvent返回false),交由软件工程师处理这个任务(View#dispatchTouchEvent)(我们忽略总监与组长的情况),软件工程师完成了这个任务(View#onTouchEvent返回true)
  • 把结果告诉项目经理(返回结果true,View#dispatchTouchEvent→ViewGroup#dispatchTouchEvent),项目经理把结果告诉老板(返回结果true,ViewGroup#dispatchTouchEvent→Activity#dispatchTouchEvent)
  • 项目经理完成的不错,老板决定把这个项目的二期、三期等都交给项目经理,同样项目经理也觉得这个软件工程师完成的不错,所以也把二期、三期等都交给这个工程师来做

默认情况下View是消费触摸事件的,现在我们再来看看当View不消费触摸事件(例如:clickable为false)的时候整个流程是怎么样的:

View不消费触摸事件.png
View不消费触摸事件.png

流程图:

ActivityDown.png
ActivityDown.png
ActivityUp.png
ActivityUp.png

原则:

  • 当View没有对Down动作进行事件消费的时候,就交由父布局处理,如果父布局也没有消费的时候就交由Activity处理
  • 如果View没有对Down动作进行事件消费,那么后续的动作也不在交由它处理,而是直接交由处理的了Down动作的容器进行处理。

这里使用工作中的情况来模拟:依旧是老板(Activity)、项目经理(ViewGroup)、软件工程师(View) 从老板交任务给项目经理,项目经理交任务给工程师,这一段流程和之前的例子相同。不同之处是软件工程师没有完成这个任务(View#onTouchEvent返回false),告诉项目经理我没有完成,然后项目经理自己进行了尝试,同样没有完成(ViewGroup#onTouchEvent返回false),项目经理告诉了老板,我没有完成,然后老板自己试了下也没有完成这个任务(Activity#onTouchEvent返回false),但之后的也有项目的二期、三期,不过老板知道你们完成不了,所以都是他自己进行尝试,不过很惨都没完成。(这段有点与正常情况不同,不过只是打个比方)

拦截事件

这里就直接贴图看下Log吧:


拦截事件
拦截事件

流程图:


Down
Down

Up
Up
结论
  • ViewGroup拦截事件之后,直接由自己的onTouchEvent()方法进行处理,并最终将结果返回给Activity
  • ViewGroup拦截事件之后,后续的动作事件序列将不再调用onInterceptTouchEvent()判断是否拦截,而是直接由自己进行处理。

使用工作中的情况来模拟:老板(Activity)、项目经理(ViewGroup)、软件工程师(View) 老板吧任务交给项目经理,项目经理认为这个项目比较难,所以决定自己处理(ViewGroup#onInterceptTouchEvent,return true),项目经理比较厉害他把任务完成了(ViewGroup#onTouchEvent,return true),然后他告诉老板他完成了(return true,ViewGroup#dispatchTouchEvent→Activity#dispatchTouchEvent)。之后老板依旧会把任务交给项目经理,项目经理知道这个任务难度,所以不假思索(也就是这个事件序列中的其余事件没有经过ViewGroup#onInterceptTouchEvent)的自己来做。

通过上面拦截的叙述就知道为什么在侧边栏打开的时候,不能点击主界面了,因为触摸事件被拦截了,所以只要进行好判断在适当的时候不进行拦截就可以了。


图片.png

至于点击主界面不让侧边栏收回,就让主界面在动作为Activity_Up的时候消费事件(onTouchEvent()返回true)就可以了(因为DrawerLayout类在onTouchEvent()方法里关闭了侧边栏)


ViewGroup#onTouchEvent().png
DrawerLayout#onTouchEvent().png

最后在总结一下:

  • 一个事件序列是指从手指触摸屏幕开始,到手指离开屏幕结束,这个过程中产生的一系列事件。一个事件序列以ACTION_DOWN事件开始,中间可能经过若干个MOVE,以ACTION_UP事件结束。
  • 事件的传递过程是由外向内的,即事件总是由父元素分发给子元素
  • 如果某个View消费了ACTION_DOWN事件,那么通常情况下,这个事件序列中的后续事件也将交由其进行处理,但可以通过调用其父View的onInterceptTouchEvent方法,对后续事件进行拦截
  • 如果某个View它不消耗ACTION_DOWN事件,那么这个序列的后续事件也不会再交由它来处理
  • 如果事件没有View对其进行处理,那么最后将有Activity进行处理
  • 如果事件传递的结果为true,回传的结果直接通过不断调用父View#dispatchTouchEvent方法,传递给Activity;如果事件传递的结果为false,回传的结果不断调用父View#onTouchEvent方法,获取返回结果。
  • View默认的onTouchEvent在View可点击的情况下,将会消耗事件,返回true;不可点击的情况下,则不消耗事件,返回false(longClickable的情况,读者可以自行测试,结果与clickable相同)
  • 如果某个ViewGroup的onInterceptTouchEvent返回为true,那么这个事件序列中的后续事件,不会在进行onInterceptTouchEvent的判断,而是由它的dispatchTouchEvent方法直接传递给onTouchEvent方法进行处理
  • 如果某个View接收了ACTION_DOWN之后,这个序列的后续事件中,在某一刻被父View拦截了,则这个字View会收到一个ACTION_CANCEL事件,并且也不会再收到这个事件序列中的后续事件

项目源码
参考文章
有问题欢迎联系我(1021423736@qq.com)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容