Android事件分发机制详解

转载注明出处:http://www.jianshu.com/p/0b821660b195

1. 概述

Android日常研发时,与View接触占据相当多的时间,而关于View的知识,主要集中在View的绘制和View对于点击事件的处理。关于View的绘制过程,可以查看一下这篇文章的介绍;关于View处理点击事件,可能有人会认为在onTouchEvent()这个方法处理点击事件就行了,不错,具体的处理过程确实是在这个方法中,但是点击事件在View间是怎么分发的?怎么确定当前View想要处理点击事件?这些问题在本篇文章中都会一一解决。

点击事件的分发,在具体的View中是有细小差别的,比如说LinearLayout中事件的分发和RelativeLayout中事件的分发就有一些不同的地方。所以本篇只介绍一个通用的事件分发机制,不针对具体View的源代码进行分析。

2. 点击事件类型

点击事件类型有好多种,参考MotionEvent类,在这里主要介绍四种常见的事件类型。

类型 说明
ACTION_DOWN 手指接触到屏幕事件
ACTION_UP 手指抬起离开屏幕事件
ACTION_MOVE 手指在屏幕上面滑动事件
ACTION_CANCEL 点击事件由于某种原因取消,比如说手指滑到屏幕外

一个正常完整的点击事件一般是从 ACTION_DOWN事件(手指接触到屏幕)开始,到ACTION_UP事件(手指抬起离开屏幕)结束,中间可以有滑动操作,如下所示。

ACTION_DOWN —> (ACTION_MOVE —> ACTION_MOVE —> ACTION_MOVE) —> ACTION_UP

注意手指滑动事件是一个连续的事件,在滑动过程中会一直触发,并不是只触发一次。

3. 方法介绍

点击事件分发机制中,一般会由三个方法控制,并不是上面所说的关注onTouchEvent()方法就行了。

  • dispatchTouchEvent(),事件的分发方法,一般由父布局调用,将点击事件传递到子View。返回true,代表事件被消费;返回false,表示事件未被消费,事件会继续传递下去。
  • onInterceptTouchEvent(),是否拦截点击事件,如果返回true,表示拦截事件,调用自身onTouchEvent()处理点击事件;如果返回false,不拦截点击事件,则将点击事件传递到子View。
  • onTouchEvent(),处理点击事件的具体方法

注意:View类和Activity类中仅仅有dispatchTouchEvent()和onTouchEvent()两个方法,并没有onInterceptTouchEvent()方法;上述三个方法在ViewGroup中都存在

注意:此文中"消费"一词的意思并不是指调用了onTouchEvent()方法,而是指onTouchEvent()方法或者OnTouchListener()返回了true,表示事件被消费

4. 点击事件分发过程

4.1 Activity事件处理

最先获取点击事件的是Activity,也就是所有View获取到的点击事件都是由Activity传递下去的,Activity会调用顶层的ViewGroup的dispatchTouchEvent()方法,将事件分发给ViewGroup。看一下Activity这块的源代码。

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

当接收到ACTION_DOWN事件时候,调用了自身的onUserInteraction()方法,这个方法是个空方法,然后通过调用顶层ViewGroup的dispatchTouchEvent ()方法将事件传递给ViewGroup。如果在ViewGroup中将事件消费了,就直接返回true;如果没有消费,最终会调用Activity的onTouchEvent()方法。也正如上面所说Activity中没有onInterceptTouchEvent()方法。

4.2 ViewGroup事件处理

ViewGroup获取到点击事件后处理过程会根据具体的ViewGroup有不同的操作,这个过程都有一些细小的差别,但是大体思路是不变的,用伪代码来描述ViewGroup处理点击事件的过程。

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 如果拦截点击事件,不将该事件传递给子View,同时调用本身的onTouchEvent方法来处理事件,并返回布尔值标记是否消费该事件,
    if (onInterceptTouchEvent(ev)) {
        return onTouchEvent(ev);
    } 
    // 如果不拦截点击事件,将事件传递给子View,并返回处理结果的布尔值标记是否消费该事件
    if (isConsumed = child.dispatchTouchEvent(ev)){ 
        return true;
    }
    // 如果子View也没有消费事件,事件又会从底层向上层传递,最终到activity
    return onTouchEvent(ev);
}

ViewGroup拿到点击事件后,首先会调用onInterceptTouchEvent()方法来判断是否需要拦截该事件,防止该事件继续传递到子View。如果需要拦截该事件,则返回true,调用本身的onTouchEvent()方法处理该事件;如果不需要拦截该事件,调用子View的dispatchTouchEvent()方法,将事件传递给子View。如果存在多层ViewGroup嵌套,事件的传递过程也是同样的。

4.3 View事件处理

将事件传递给最底层的View后,例如TextView,看看内部是怎么处理的,源代码太多,截取了比较重要的一段源代码,如下。

public boolean dispatchTouchEvent(MotionEvent event) {
    // 其他逻辑
    ....
    
    // 过滤一些不必要的事件
    if (onFilterTouchEventForSecurity(event)) {
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    
    // 其他逻辑
    ....
    
    return result;
}

贴这一段的源代码主要是为了查看OnTouchListener相对于onToucheEvent()的优先级,可以看见,如果View设置了OnTouchListener,并且它消费了点击事件,就不会调用View的onToucheEvent()方法,顺便说一下OnClickListener会在onTouchEvent()方法内部被调用。整个调用的优先级如下。

OnTouchListener > onToucheEvent() > OnClickListener

4.4 点击事件分发小结

上面对Activity、ViewGroup、View对事件的处理过程进行了详细的介绍,主要集中于dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()三个方法,从下面这个列表来总结一下。

Touch事件相关方法 功能说明 Activity ViewGroup View
dispatchTouchEvent() 事件分发,返回true,代表事件已经被消费;返回false,代表事件未被消费,事件会回传给父布局处理。 有该方法 有改方法 有该方法
onInterceptTouchEvent() 事件拦截,返回true,代表当前控件要处理事件;返回false,代表当前控件不拦截事件,事件将会下发给子View 无该方法 有该方法 无该方法
onTouchEvent() 事件处理,返回true,代表事件被消费;返回false,代表事件未被消费 有该方法 有该方法 有该方法

5. Touch日志分析

新建一个Activity,复写里面的dispatchTouchEvent()onTouchEvent()方法,仅仅添加日志,不做其他任何处理。

public class TouchDemoActivity extends AppCompatActivity {

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "activity dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
        }
        boolean result = super.dispatchTouchEvent(ev);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "activity onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        boolean result = super.onTouchEvent(event);
        return result;
    }
}

Activity的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <com.wang.demo.touch.view.TouchLayout
        android:id="@+id/touch_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.wang.demo.touch.view.TouchView
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </com.wang.demo.touch.view.TouchLayout>
</LinearLayout>

TouchLayout继承LinearLayout,并复写了dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()三个方法,在这三个方法里面添加日志。

public class TouchLayout extends LinearLayout {
    
    // 构造方法
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "layout dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
        }
        boolean result = super.dispatchTouchEvent(ev);
        return result;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "layout onInterceptTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
        }
        boolean result = super.onInterceptTouchEvent(ev);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "layout onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        boolean result = super.onTouchEvent(event);
        return result;
    }
}

TouchView直接继承View,并复写了dispatchTouchEvent()onTouchEvent()方法
,添加日志。

public class TouchView extends View {
    
    // 构造方法

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public TouchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "view dispatchTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        boolean result = super.dispatchTouchEvent(event);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "view onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        boolean result = super.onTouchEvent(event);
        return result;
    }
}

5.1 事件未被消费

上面代码中,都是调用控件的默认方法对事件处理,也就是说,事件并没有被我们消费。点击屏幕并滑动最后抬起手指,看一下日志输出。

事件未被消费-日志.jpg

标记出三个区域。

  • 第一个区域,是事件从Activity传递到底层View的过程,在这个过程中,是没有控件对点击事件进行消费的。
  • 第二个区域,事件从上到下没有被消费,所以事件又会从底层View回传到ViewGroup并且最终会回传到Activity。
  • 第三个区域,如果控件对第一个获取的事件(ACTION_DOWN)没有进行处理的话,后续的事件(ACTION_MOVE,ACTION_UP等等)就不会传递到这个控件,所以第三个区域的日志都在Activity这一层处理了,并没有向下层传递。

事件传递示图如下所示。

事件未被消费-事件传递图

5.2 事件被ViewGroup拦截且消费

改动一下TouchLayout中的onInterceptTouchEvent()方法让它返回true,表示拦截事件。修改后文件如下。

public class TouchLayout extends LinearLayout {

    // 构造函数

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "layout dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
        }
        boolean result = super.dispatchTouchEvent(ev);
        return result;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "layout onInterceptTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
        }
        // 返回true,拦截事件
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "layout onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        // 返回true,事件被消费
        return true;
    }
}

运行一下,日志如下。

事件被拦截并消费-日志

标记出两个区域。

  • 第一个区域,事件正常由Activity向下传递,到ViewGroup层后,事件被拦截,ViewGroup直接调用自身的onTouchEvent()方法消费该事件
  • 第二个区域,因为ViewGroup消费了最初的事件,所以后续的事件直接传递到了该ViewGroup并进行处理

事件传递如下,黑色线是第一次事件传递过程,红色线为后续事件传递过程。

事件被拦截并消费-事件传递图

5.3 事件由最底层的View消费

复原TouchLayout类,修改TouchView中的onTouchEvent()方法,让它返回true,表示事件被消费。

public class TouchView extends View {

    // 构造函数

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "view dispatchTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        boolean result = super.dispatchTouchEvent(event);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "view onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        return true;
    }
}

运行日志如下。

事件被底层View消费-日志

从日志中可以看见,事件正常由Activity向下传递,最终传递到底层View,并被消费。

这种情况的事件传递如下。


事件被底层View消费-事件传递图

5.4 ViewGroup设置了OnTouchListener、OnClickListener

DemoMainActivity中设置TouchLayout的OnTouchListener。

public class TouchDemoActivity extends AppCompatActivity {

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

        findViewById(R.id.touch_layout).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    Log.d(Constants.LOG_TAG_TOUCH, "layout OnTouchListener, ev : " + MotionEvent.actionToString(event.getAction()));
                }
                return true;
            }
        });
        
        findViewById(R.id.touch_layout).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(Constants.LOG_TAG_TOUCH, "layout OnClickListener");
            }
        });
}

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "activity dispatchTouchEvent, ev : " + MotionEvent.actionToString(ev.getAction()));
        }

        boolean result = super.dispatchTouchEvent(ev);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "activity onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        boolean result = super.onTouchEvent(event);
        return result;
    }
}

同时设置TouchView中的onTouchEvent()返回true,表示消费了该事件。

public class TouchView extends View {

    // 构造函数

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "view dispatchTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        boolean result = super.dispatchTouchEvent(event);
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Log.d(Constants.LOG_TAG_TOUCH, "view onTouchEvent, ev : " + MotionEvent.actionToString(event.getAction()));
        }
        return true;
    }
}

查看一下事件日志。

事件分发机制图

可以看见,虽然ViewGroup设置了OnTouchListener,但是事件是被最底层的View消费的。

我们再设置TouchView中的onTouchEvent()返回false,不让最底层的View消费事件。查看一下运行日志

事件分发机制图

从日志中看,确实是ViewGroup的OnTouchListener将事件消费了,但是为什么没有调用OnClickListenr呢?是因为OnTouchListener高于OnTouchEvent,而OnClickListener的调用时在OnTouchEvent内部,OnTouchListener将事件消费后,并不会调用OnTouchEvent,自然也不会调用OnClickListener了。

5.4 小结

从上面日志中,可以得出一些结论,帮助我们加深对事件分发的理解。

  • 事件虽然是从Activity向底层View传递,在不考虑ViewGroup拦截事件的情况下,最先处理事件(onTouchEvent)的是底层View,如果事件未被底层View消费,事件将会回传给上层的ViewGroup处理(onTouchEvent),若所有的ViewGroup都未消费事件,事件最终会回传到Activity由它做最后的处理(onTouchEvent)。
  • 事件在传递过程中,如果被ViewGroup拦截(onInterceptTouchEvent),该ViewGroup会优先处理该事件。
  • 底层的View或者ViewGroup如果将事件消费了,上层的ViewGroup的OnTouchListener、OnTouchEvetn,OnClickListener都不会被调用。
  • 在同一个View或者ViewGroup的事件处理中,OnTouchListener优先级最高,OnTouchEvent其次,OnClickListener最低。

6. 总结

文章大体介绍了Android的事件分发机制,并没有针对具体的源代码进行讲解,主要是不同的ViewGroup对于事件的处理在细节上有许多不同,但是在事件处理的大体思路上还是一致的。

而在平时中遇见关于事件处理的问题,去查看具体的View或者ViewGroup中对于事件的处理才是最快捷的解决问题的方式。

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

推荐阅读更多精彩内容