震惊,View事件分发机制竟然是这样的

事件类型MotionEvent

MotionEvent的事件有三种类型:

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP

当你点击一次屏幕的时候,整个过程中包含了一个ACTION_DOWN开始事件和多个的ACTION_MOVE以及一个ACTION_UP终止事件。当然如果没有在屏幕上滑动的也就没有ACTION_MOVE事件啦。

事件分发机制

通常我们的点击事件传递过程是Activity->Window->DecorView(View的事件分发机制),接下来具体介绍下这三者的事件传递过程:

1. Activity#dispatchTouchEvent的过程

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();// 该方法默认没有实现内容,子类Activity可在ACTION_DOWN发生时做特定互动
        }
        if (getWindow().superDispatchTouchEvent(ev)) {// 交由Window分发处理
            return true;
        }
        return onTouchEvent(ev);// 整个View树的onTouchEvent都返回false(不消费事件),仍然由Activity自身处理
    }

dispatchTouchEvent是系统事件传递地开端,是Window.Callback的一个重要回调方法,系统将屏幕点击事件传递于此。

2. PhoneWindow#superDispatchTouchEvent的过程

Window是一个抽象类,superDispatchTouchEvent也是个抽象方法,PhoneWindow是其唯一实现类。

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);// mDecor就是DecorView,即getWindow.getDecorView()返回的那个View
    }

3. 顶级View对事件的分发过程

我们先了解下几个会在View事件分发中使用到的方法:

  • onTouchEvent 处理点击事件
  • onInterceptTouchEvent 是否拦截处理点击事件,在ViewGroup才有
  • dispatchTouchEvent 分发事件

以上三个方法的返回结果:

  • false,表示事件在当前view未消耗,则继续往view树下层传递;
  • true,表示事件在当前view已被消耗,则不再传递。

一段伪代码表示以上三个方法的关系:

public boolean dispatchTouchEvent(MotionEvent ev){
        boolean consume = false;
        if (onInterceptTouchEvent(ev)){
                consume = onTouchEvent(ev);
        }else{
                consume = child.dispatchTouchEvent(ev);
        }
        return consume;
}

那么可能有同学会问,不是还有onTouchListener和onClickListener吗?
这三者的优先级是onTouchListener > onTouchEvent > onClickListener,我们可以从View源码来验证。

public boolean dispatchTouchEvent(MotionEvent event) {
        ....
        boolean result = false;
        ....
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {// OnTouchListener优先调用(可用状态下)
                result = true;
            }

            if (!result && onTouchEvent(event)) {// 若OnTouchListener返回true,则onTouchEvent被屏蔽
                result = true;
            }
        }

        ....
        return result;
    }

onTouchListener若存在,onTouch方法返回true则会屏蔽掉onTouchEvent。

现在来看ViewGroup#dispatchTouchEvent,分段来说明:
1) requestDisallowInterceptTouchEvent设置FLAG_DISALLOW_INTERCEPT状态后,将使ViewGroup无法拦截除ACTION_DOWN以外的其他点击事件。换言之,尽管子元素有优先禁用父容器的拦截功能,但是对于ACTION_DOWN事件是个特例;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();// ACTION_DOWN事件重置了FLAG_DISALLOW_INTERCEPT标记位
            }

2) 这部分代码描述当前ViewGroup是否拦截点击事件的这个逻辑

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {// ACTION_DOWN动作和子元素是否成功处理(null表示没有子元素处理)
                // 若是ACTION_DOWN事件,FLAG_DISALLOW_INTERCEPT标记位会被清除,disallowIntercept为false
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {// 若子元素未禁止父容器的拦截功能(ACTION_DOWN肯定会执行)
                    intercepted = onInterceptTouchEvent(ev);// ViewGroup自己决定是否拦截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches. 即没有子view处理时(mFirstTouchTarget 若不为空说明子元素处理成功),事件由当前ViewGroup拦截,不再调用onInterceptTouchEvent来决定是否拦截
                intercepted = true;
            }

由上源码分析,我们可得出结论:

  1. ViewGroup决定拦截事件,那后续事件将默认交由处理,不需要再调用onInterceptTouchEvent
  2. FLAG_DISALLOW_INTERCEPT标记位的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件,ViewGroup若要拦截,则标记位设置了也无效

总结

我们最后完整的理一遍整个流程:一个点击事件首先由Activity接收,Activity调用dispatchTouchEvent进行分发,优先传递给Window进行处理,如果Window不消耗该事件则再由Activity的onTouchEvent来处理;Window处理过程则直接委托给DecorView进行事件分发,这个DecorView是android.R.id.content的父View,而android.R.id.content的子View就是我们Activity的视图view。
接下来就进入了View的事件分发过程
如果我们的Activity视图中的顶级ViewGroup拦截事件,即onInterceptTouchEvent返回true,则事件由该层ViewGroup处理,如果设置了onTouchListener,则onTouch被调用,否则onTouchEvent会被调用,如果还有onClickListener,则onClick最后被调用;若ViewGroup不拦截,则事件传递到点击事件链上的子View,子View的dispatchTouchEvent被调起,依上如此循环,完成事件分发。

注意点

  • 一个事件序列(开始-结束)只能被一个View拦截消耗
  • 子view一旦开始处理事件,ACTION_DOWN事件就应该返回true;若返回false,则后续事件序列不再交由处理,重新由其父元素onTouchEvent去处理
  • ViewGroup默认不拦截任何事件
  • View只要可点击的,onTouchEvent默认会消耗事件
  • 子View可以通过requestDisallowIntercceptTouchEvent干预父元素的事件分发,ACTION_DOWN事件除外
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容