View 体系之 View 事件分发源码解析

View 体系之 View 事件分发源码解析

本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。

写在前面:

前两天我们分别总结了
View 的位置与事件:
View 的位置与事件

View 的滑动:
View 的滑动

今天我们来聊聊 View 的事件分发。相信每个人都知道 View 的事件分发实在是太重要了,它不仅仅是一个核心知识点,更是一个难点。在我初学 Android 时,View 的事件分发也前前后后看了好多次。虽然也能复述出一个大概,但是仍然有一些知识盲区。所以今天把 View 的事件分发总结出来,算是自己记下一篇学习笔记,未来复习巩固使用。如果还能为大家解决一些困惑,那就更好了。

当然关于事件分发的文章,前辈们总结了很多,有一篇我认为非常出色:
图解 Android 事件分发

这篇文章通过图解的方式,清晰直观的讲明白了事件分发的原则,本文打算对这篇文章做一些补充,补充一下这篇文章的一些关键 log,和源码分析。所以不理解事件分发的朋友可以阅读下该文。

首先大家想一下,在 Android 中谁是事件分发的掌控者和消费者?没错,是 Activity、ViewGroup、View,一个正常的 Android 应用程序他们三个肯定是存在的。而分发的事件就是 MotionEvent 对象。

关于事件分发,有三个关键的方法

dispatchTouchEventonInterceptTouchEventonTouchEvent

onInterceptTouchEvent 方法是 ViewGroup 特有的。我们在 Activity、ViewGroup、View 中分别打印这几个方法,来看看不同的返回值对事件分发的影响,来印证上文的观点,并且分析出事件分发的传递规则。

当我们不修改任何返回值,全部为默认实现时:

这里写图片描述

可以看到 ACTION_DOWN 事件的传递原则为,U 型原则,ACTION_MOVE、ACTION_UP 传递原则为距离最短原则。

分别来改变 Activity 中dispatchTouchEventonTouchEvent 的返回值,来看看事件传递的 log:

首先分别将 dispatchTouchEvent 的返回值改为 false 或者 true:

这里写图片描述

可以看到我的这次点击按钮的事件在 Activity 中的 dispatchTouchEvent 中消费掉了。

当我改变 MainActivity 中 onTouchEvent 方法的返回值时:

这里写图片描述

可以看到打印的结果与最初所有方法的默认返回值相同,这也很好理解,因为 Activity 的 onTouchEvent 方法本身就是事件 U型 传递的最后一环,不管什么返回值,反正事件都会到这里。

Activity 的看完了,再来看看 ViewGroup 的:

这里写图片描述

可以看到将 ViewGroup 的 dispatchTouchEvent 返回值改为 false 时,事件就不会再下发了,而是直接传递给 Activity 的 onTouchEvent。当 dispatchTouchEvent 返回值改为 true 时,与默认实现相同。

这里写图片描述

onInterceptTouchEvent 的返回值改为 true 时,事件不会再传递给 View ,而是传递给当前 ViewGroup 的 onTouchEvent。当onInterceptTouchEvent返回值为 false 时,与默认相同。

这里写图片描述

首先明确一个概念:事件序列

就是当手指 按下-->滑动-->抬起 的这一完成过程产生的事件流为一个事件序列。

当我将 ViewGroup 的 onTouchEvent 方法的返回值改为 true 时,事件在 ViewGroup 就消费掉了,这里应该注意,onInterceptTouchEvent 如果发生了拦截,那么在一个事件序列中仅调用一次。

关于 View 这两个方法的返回值就不贴图了,与引用文章的结论一致。

一些细节

当给一个 View 设置 onTouchListener 时,它的 onTouch 方法就会回调,如果 onTouch 方法的返回值为 false,则该 View 的 onTouchEvent 方法会被调用,如果 onTouch 方法的返回值为 true,则该 View 的 onTouchEvent 方法就不会调用了,事件会直接在该 View 的 disPatchTouchEvent 中消费。另外 onClick 方法是在 onTouchEvent 方法中调用的。所以这几个方法的优先级关系为:

onTouch>onTouchEvent>onClick

一个 View 的 onTouchEvent 的返回值是与这个 View 本身的 onClick 和 onLongClick 属性相关的,只有这两个属性同时为 false 则 onTouchEvent 才会为 false,View 的 onLongClick 默认都为 false,而 onClick 属性不同,比如 button 的为 true,textview 的为 false。

事件分发的源码解析

在这部分内容中,我们看看事件分发在源码上的处理,事件最初都是在 Acitivity 中产生,然后分发给根 ViewGroup,最后再发给相应的 View。那先来看看 Activity 的 dispatchTouchEvent 方法。

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

这段代码很简单,当 ACTION_DOWN 来了的时候,回调 onUserInteraction 方法作为事件起始的回调。

然后来看看 getWindow().superDispatchTouchEvent(ev) 方法的返回值是如何的。

setContentView

首先关于 Window 和 PhoneWindow 类的关系可以上面这篇我曾经总结的文章。

PhoneWindow:

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

跟进,看看 DectorView 的 superDispatchTouchEvent(event) 方法:

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

DectorView 继承自 FrameLayout,所以这里也是调用到了 ViewGroup 的 dispatchTouchEvent,事件顺利传到了 ViewGroup

来看看 ViewGroup 对事件的分发
代码比较多,我们分段来看:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    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.
                intercepted = true;
            }

这段代码的意义是,判断是否要调用 onInterceptTouchEvent 方法,可以看到 if 判断的条件语句为:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
先看第一个,当事件为 ACTION_DOWN 时,肯定会调用 onInterceptTouchEvent,那 mFirstTouchTarget 是什么呢?由后面的代码可知,当事件不被拦截并且交给子元素处理时,mFirstTouchTarget != null。所以当被当前 View 拦截的时候,mFirstTouchTarget == null,ACTION_DOWN、ACTION_MOVE 事件来的时候,条件就不成立了,所以 onInterceptTouchEvent方法也不会再次调用,这也就是为什么之前说,当此 ViewGroup 确定拦截事件的时候,onInterceptTouchEvent之后在事件为 ACITON_DOWN 的时候调用一次。

这有一个 flag 比较重要,FLAG_DISALLOW_INTERCEPT,它的值由子 View 的 requestDisallowInterceptTouchEvent 决定,由子 View 请求父 View 不要拦截事件。当然此属性对 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();
            }

dispatchTouchEvent 的开头就重置了 FLAG 的状态。

这里我们会有两个结论:

  1. onInterceptTouchEvent 方法并不是每次都调用,而如果事件传递进来,dispatchTouchEvent 才是每次都会调用的。
  2. requestDisallowInterceptTouchEvent 可以干预父 View 的事件分发过程,有助于我们解决滑动冲突。
                    final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

这段代码首先判断所有的子 View 是否具有接受事件的能力,1 没有进行动画 2 点击的位置在 View 的坐标范围内。dispatchTransformedTouchEvent 中有这样一行代码:

handled = child.dispatchTouchEvent(event);

所以到这里,就调用到了子 View 的 dispatchTouchEvent 方法。

在 addTouchTarget 方法中:

 mFirstTouchTarget = target;

mFirstTouchTarget 被赋值,也就是当子 View 处理事件时,mFirstTouchTarget 不为 null.

看完了 ViewGroup 对事件分发的处理,我们来看看 View 对事件的处理吧。

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    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)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        return result;
    }

因为 View 不会再有子 View 了,所以他的 dispatchTouchEvent 方法比较简单,首先如果这个 View 设置了 onTouchListener,并且 onTouch 方法返回值为 true 时,会进入判断条件,方法直接返回 true,就不会走到 onTouchEvent 方法里面了。所以这里也印证了我们之前的观点,也就是 onTouch 方法优先级大于 onTouchEvent。

再来看看 View 的 onTouchEvent 方法的源码:

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

可以看到这里即使 View 是 disable 的,依然可以消耗事件。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
     switch (action) {
                case MotionEvent.ACTION_UP:
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                    break;

可以看到当这个 View 的 LongClick 或者 Clickable 属性有一个为 true,就可以消耗这个事件,并且在 ACTION_UP 调用 performClick():

final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

所以到这里这个 View 的点击事件也响应了。

到这里,我们事件分发的源码就分析完毕了。

写在后面:

本文更多的是对上面那篇引用文章的源码补充,两篇结合起来看,对事件分发的理解就应该足够了。这几天看源码看得头疼。。。关于本文的结论总结,我准备过一阵回头温习的时候补充下,希望大家喜欢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容