Android触摸事件传递机制

[声明] 本文来自《顾浩鑫-Android高级进阶》第一章 的学习笔记,发文目的在于传递分享知识。如涉及文章内容、版权和其它问题,请与本人联系,我将在第一时间删除内容!

前言:在Android开发中,经常会遇到触摸事件冲突,比如ViewPager的轮播图跟Fragment的划动事件冲突,或者轮播图跟下拉事件冲突,自定义view的事件处理等,本文章将会详细介绍Activity、View、ViewGroup三者的触摸事件传递机制,传递包括三个阶段:分发、拦截、消费。

一.触摸事件的类型

触摸事件对应的是 MotionEvent 类,事件类型主要有三种:

  1. ACTION_DOWN:用户按下操作,表示一次触摸事件的开始。
  2. ACTION_MOVE:在按下的情况下,进行移动。轻微的移动都会传递到该事件。
  3. ACTION_UP:用户手指离开屏幕,表示一次触摸事件的

注 :如果用户仅仅的是点击而已,则只会执行到 ACTION_DOWN 和 ACTION_UP 两个事件,不会执行到 ACTION_MOVE 事件。所以 ACTION_DOWN 和 ACTION_UP 是事件是必须的。

二.触摸事件的传递阶段

1.分发(Dispatch)

在Android系统中所有的触摸事件都是由 dispatchTouchEvent 方法进行分发的。该方法中判断事件是被消费(return true),还是继续分发给子视图处理(return super.dispatchTouchEvent),如果当前视图是ViewGroup或者其子类,则会调用onInterceptTouchEvent 判断是否截拦。

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

2.截拦(Intercept)

事件的截拦 InterceptTouchEvent 只存在于ViewGroup及其子类,activity和View是不存在该方法。该方法判断事件是被截拦 (return true)并交给自身的 OnToucEvent 方法进行消费,还是继续传递给子视图(return super.InterceptTouchEvent 或者 return false)。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

3.消费(Consume)

事件的消费通过 OnTouchEvent 方法判断,是被消费(return true),还是不处理(return false)并将事件传递给父视图的 OnTouchEvent 方法进行处理。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

所有拥有事件传递能力的类:
Activity: 拥有dispatchTouchEvent 、OnTouchEvent
ViewGroup: 拥有dispatchTouchEvent 、OnInterceptTouchEvent 、OnTouchEvent
View:拥有dispatchTouchEvent 、OnTouchEvent

三、View的事件传递机制

3.1 dome

虽然说ViewGroup是View的子类,但是这是说的View指的是除ViewGroup之外的View控件子类,首先定义一个MyTextView继承TextView,打印每次事件的触发以变了解事件传递的流程。

MyTextView 类

public class MyTextView extends TextView {

    private String tag = "MyTextView";

    public MyTextView(Context context) {
        super(context);
    }

    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "dispatchTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
                break;
        }
        return super.dispatchTouchEvent(event);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "onTouchEvent ACTION_DOWN");
                break;
        }
        return super.onTouchEvent(event);
    }
}

定义一个MainActivity来展现这个MyTextView,同时设置点击(onClick)和触摸(onTouch)监听。
MainActivity 类

public class MainActivity extends AppCompatActivity implements View.OnClickListener,View.OnTouchListener{
    private MyTextView mMyTextView;
    private String tag = "MainActiviy";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mMyTextView = findViewById(R.id.text_view);
        //  点击监听
        mMyTextView.setOnClickListener(this);
        //  触碰监听
        mMyTextView.setOnTouchListener(this);
    }


    // MyTextView 点击事件
    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.text_view:
                Log.i(tag, "MyTextView onClick");
                break;
        }
    }

    // MyTextView 触碰事件
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "MyTextView onTouch ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "MyTextView onTouch ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "MyTextView onTouch ACTION_DOWN");
                break;
        }
        return false;
    }

    // Activity 的事件分发
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "dispatchTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    // Activity 的事件消费
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "onTouchEvent ACTION_DOWN");
                break;
        }
        return super.onTouchEvent(event);
    }
}

3.2 打印日志

运行后,点击Text View反馈的打印日志

03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
03-28 08:05:15.044 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick

dispatchTouchEvent 、 OnTouchEvent 这两个方法的返回值存在三种情况:

  1. 直接返回true。
  2. 直接返回false。
  3. 返回父类同名方法,super.dispatchTouchEvent 或者 super.OnTouchEvent。

由于拥有不同的返回值,所以事件传递流程也有不同,经过不断修改返回值测试,最终得到了点击事件的流程图,ACTION_DOWN 和 ACTION_UP 事件的传递流程是相同的。

3.3 事件传递流程图

image

从上面的流程图可以得出结论:

  1. 触摸事件是从 dispatchTouchEvent 开始的,默认返回父类同名方法 super ,事件将会依照嵌套层次从外向内传递( MainActivityMyTextView ),到达最内层的 View 时,将由 ViewOnTouchEvent 方法处理,该方法返回 true 时进行消费不再传递,返回 false 时再由内向外传递,由外层的 OnTouchEvent 处理。
  2. 如果外层向内层传递过程中,人为干扰返回 true 消费,则不会继续继续像内部传递。
  3. View 的事件控制顺序先执行 onTouch 再执行 onClick ,如果 onTouch 返回 true 消费,则不会继续传递,也不会执行 onClick 方法。

四、ViewGroup的事件传递机制

4.1 dome

ViewGroupView 的控件容器存在,拥有 dispatchTouchEventonInterceptTouchEventonTouchEvent 三个方法,比 View 多了一个 onInterceptTouchEvent 方法。为了更好的观察,我们需要自定义 MyRelativeLayout 继承 RelativeLayout

MyRelativeLayout类

public class MyRelativeLayout extends RelativeLayout {

    private final static String tag = "MyRelativeLayout";

    public MyRelativeLayout(Context context) {
        super(context);
    }

    public MyRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "dispatchTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "onInterceptTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "onInterceptTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "onInterceptTouchEvent ACTION_DOWN");
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "onTouchEvent ACTION_DOWN");
                break;
        }
        return super.onTouchEvent(event);
    }
}

main_activity.xml 文件

<?xml version="1.0" encoding="utf-8"?>
<com.mvp.chenzhesheng.androidadvance.MyRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <com.mvp.chenzhesheng.androidadvance.MyTextView
        android:id="@+id/text_view"
        android:clickable="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"

</com.mvp.chenzhesheng.androidadvance.MyRelativeLayout>

4.2 打印日志

04-02 08:47:57.980 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
04-02 08:47:58.260 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick

可以看到 MainActivityMyTextView 的事件传递处理中添加了一层 MyRelativeLayout 。通过不同返回值测试,得到一套流程图。

4.3 流程图

image

从上面的流程图可以得出结论:

  1. 触摸事件传递是从 Activity 传递到 ViewGroup ,再传递到 View 。如果中间没有 ViewGroup 则直接从 Activity 传递到 View
  2. ViewGroup 通过 onInterceptTouchEvent 方法对事件进行截拦,如果返回 false 或者 super.onInterceptTouchEvent ,则事件会继续传递给子 View
  3. View 中对事件进行消费后,ViewGroup 将不会接收到任何事件。

五.总结

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

推荐阅读更多精彩内容