Android事件分发过程探索

事件分发机制一直是Android中的一个重难点,最近也正好有点时间,于是决定好好研究一下这个东西,顺便也写下来。
首先我们来看一下当点击一个处在一个layout中的button时,事件究竟是怎么最后被button处理的。这里我分别用一个MyLayout和MyButton分别继承LinearLayout和Button,代码如下:

MyLayout.java
public class MyLayout extends LinearLayout {
    private final String TAG = getClass().getName();

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

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

    public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "on touch event layout down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "on touch event layout move");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "on touch event layout up");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "intercept touch event layout down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "intercept touch event layout move");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "intercept touch event layout up");
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "dispatch touch event layout down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "dispatch touch event layout move");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "dispatch touch event layout up");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}
MyButton.java
public class MyButton extends Button {
    private final String TAG = getClass().getName();

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

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

    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "dispatch touch event button down");
                    break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "dispatch touch event button move");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "dispatch touch event button up");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "on touch event button down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "on touch event button move");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "on touch event button up");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
}

这里我们重写了LinearLayout和Button的dispatchTouchEvent和onTouchEvent还有LinearLayout的onInterceptTouchEvent。
现在我们运行程序并点击button,看看打印的日志都有那些内容:

MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyButton: on touch event button down
MyLayout: dispatch touch event layout move
MyLayout: intercept touch event layout move
MyButton: dispatch touch event button move
MyButton: on touch event button move
MyLayout: dispatch touch event layout up
MyLayout: intercept touch event layout up
MyButton: dispatch touch event button up
MyButton: on touch event button up

从日志中我们可以看出当我们点击的动作被捕获后,点击事件的分发顺序为:
父View的dispatchTouchEvent->父View的onInterceptTouchEvent->子View的dispatchTouchEvent->子View的onTouchEvent最后处理事件
那么现在问题来了,当我们的点击事件被捕获之后,究竟经过了什么过程使得事件最终到达我们的目标View并被处理的,另外我们是否可以根据我们想要的效果对这个过程进行自定义呢?接下来就来小小地研究一下这个过程。

过程分析

这一篇我们先不深入源码进行分析,因为深入源码是个比较复杂的过程,容易被绕进其中,因此这个内容在下一篇进行分析。我们先来看看能不能通过其他的方式把这个过程大概的弄清楚。
先来看一下官方API对这三个方法的描述:
dispatchTouchEvent:Pass the touch screen motion event down to the target view, or this view if it is the target.
onInterceptTouchEvent:Implement this method to intercept all touch screen motion events.This allows you to watch events as they are dispatched to your children, and take ownership of the current gesture at any point.
onTouchEvent:Implement this method to handle touch screen motion events.
官方的对这三个方法的解释十分清楚:dispatchTouchEvent负责分配屏幕事件究竟是由自己处理还是向下传递给它的子View进行处理;onInterceptTouchEvent负责当屏幕事件从父View向子View传递的时候进行拦截,我们可以通过这个方法监视并研究事件向下传递的过程;onTouchEvent负责最后屏幕事件的处理。
刚刚我们在打印日志的过程中发现点击事件并没有经过MyLayout的onTouchEvent,从刚刚的官方的解释中我们知道onTouchEvent是用于处理屏幕事件的,而日志中可以看出屏幕事件最后被我们安排在MyLayout中的子View——MyButton处理了。
那么现在我们想要对这个过程进行简单的分析和处理有需要知道点什么呢?我们再来看看官方对这三个方法的布尔类型的返回值的解释是什么样的:
dispatchTouchEvent:True if the event was handled by the view, false otherwise.
onInterceptTouchEvent:Return true to steal motion events from the children and have them dispatched to this ViewGroup through onTouchEvent().
onTouchEvent:True if the event was handled, false otherwise.
这三个返回值的解释是这样的:dispatchTouchEvent:若返回true则由自己分发屏幕事件,否则传递给子View处理;onInterceptTouchEvent:若返回true则将传递给子View的屏幕事件拦截下,并交给自己的onTouchEvent处理;onTouchEvent:若返回true则代表屏幕事件被自己处理了,反之则不是。
看到这里好像有点意思了,原来事件处理的过程中这三个方法的返回值起到了相当关键的作用,现在我们稍微修改一下MyLayout看看有什么效果。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i(TAG, "dispatch touch event layout down");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.i(TAG, "dispatch touch event layout move");
            break;
        case MotionEvent.ACTION_UP:
            Log.i(TAG, "dispatch touch event layout up");
            break;
        default:
            break;
    }
    return false;
}

这里将dispatchTouchEvent的返回值设置为false,然后打印日志:

MyLayout: dispatch touch event layout down

只有一条,说明当MyLayout捕获了ACTION_DOWN后dispatchTouchEvent方法返回了false,然后紧接其后的其他事件就不再有MyLayout进行分配处理了,那么后面的事件都上哪里进行处理了呢?我们将MyLayout的dispatchTouchEvent的返回值改回去,再改改MyButton中的dispatchTouchEvent。

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i(TAG, "dispatch touch event button down");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.i(TAG, "dispatch touch event button move");
            break;
        case MotionEvent.ACTION_UP:
            Log.i(TAG, "dispatch touch event button up");
            break;
        default:
            break;
    }
    return false;
}

接着打印日志:

MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyLayout: on touch event layout down

我们发现当dispatchTouchEvent返回了false之后,则由它的父View处理ACTION_DOWN事件,但全过程只是处理了ACTION_DOWN,剩下的去哪里了呢?这时候我们想起官方对onTouchEvent的返回值的解释,我们来修改一下MyLayout的onTouchEvent的返回值。

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i(TAG, "on touch event layout down");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.i(TAG, "on touch event layout move");
            break;
        case MotionEvent.ACTION_UP:
            Log.i(TAG, "on touch event layout up");
            break;
        default:
            break;
    }
    return false;
}

我们将返回值设为false,但是打印日志后发现并没有任何改变,我们再将它设为true试试。

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i(TAG, "on touch event layout down");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.i(TAG, "on touch event layout move");
            break;
        case MotionEvent.ACTION_UP:
            Log.i(TAG, "on touch event layout up");
            break;
        default:
            break;
    }
    return true;
}

这时候我们通过打印日志有了一个大发现:

MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyLayout: on touch event layout down
MyLayout: dispatch touch event layout move
MyLayout: on touch event layout move
MyLayout: dispatch touch event layout up
MyLayout: on touch event layout up

原来当onTouchEvent返回false的时候,后续屏幕事件便不会再被处理。相当于做了一个标记,只有当这个标记为true时,ACTION_DOWN之后的一系列屏幕事件之后被处理。
现在我们大致弄懂了dispatchTouchEvent和onTouchEvent的作用,接下来我们再来看看onInterceptTouchEvent,看看事件是怎样由ViewGroup传递给子View的。
我们将MyLayout的onInterceptTouchEvent的返回值改为true:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i(TAG, "intercept touch event layout down");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.i(TAG, "intercept touch event layout move");
            break;
        case MotionEvent.ACTION_UP:
            Log.i(TAG, "intercept touch event layout up");
            break;
        default:
            break;
    }
    return true;
}

这时候的日志内容是这样的:

MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyLayout: on touch event layout down

通过日志我们发现,本应该传递给子View的事件被onInterceptTouchEvent拦截并返回true之后交由MyLayout本身的onTouchEvent去处理了。接下来我们将返回值设为false看看:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i(TAG, "intercept touch event layout down");
            break;
        case MotionEvent.ACTION_MOVE:
            Log.i(TAG, "intercept touch event layout move");
            break;
        case MotionEvent.ACTION_UP:
            Log.i(TAG, "intercept touch event layout up");
            break;
        default:
            break;
    }
    return false;
}

现在的日志就和没有做任何修改的时候一样了:

MyLayout: dispatch touch event layout down
MyLayout: intercept touch event layout down
MyButton: dispatch touch event button down
MyButton: on touch event button down
MyLayout: dispatch touch event layout move
MyLayout: intercept touch event layout move
MyButton: dispatch touch event button move
MyButton: on touch event button move
MyLayout: dispatch touch event layout up
MyLayout: intercept touch event layout up
MyButton: dispatch touch event button up
MyButton: on touch event button up

现在我们懂了onInterceptTouchEvent在父View向子View传递事件的过程中起到的作用就是决定这个事件是否是由本View接下处理还是传递给子View进行处理,当方法返回true的时候就交由本View处理,而返回false的时候就交由子View进行处理。

总结

现在我们基本弄懂了这三个方法在事件分发过程中各自起到的作用,下面我们再重新梳理一下Android事件分发的过程。
首先事件在传递过程中交由dispatchTouchEvent进行事件的分配,若dispatchTouchEvent返回false,则将事件传递回其父View,让其父View的onTouchEvent进行处理;若返回true,则将事件传递给onInterceptTouchEvent。当onInterceptTouchEvent接收到事件之后,它也会有一个布尔类型的返回值,若返回true,则事件不会继续向下传递而是被该View拦截下,交由该View的onTouchEvent进行处理;若返回false,则会将事件传递给其子View进行处理。最后,当目标View接收到这一系列屏幕事件的时候,首先会去处理ACTION_DOWN事件,然后会由一个返回值,若返回值为true,则表明以ACTION_DOWN为开始的一些列事件目标View都会接下并处理;但若是返回false,则后续的一系列事件,整个程序都不会将其接下并处理。
我们通过不停地尝试,最终基本弄清楚了Android事件分发的流程,但是这个过程中究竟发生了什么,这个过程到底是怎么进行的,还需深入View的源码进行探究,以后我会寻找时间认真研究一下,并将我的学习成果分享出来。

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

推荐阅读更多精彩内容