Android事件分发机制

首先,我们要明白事件的定义『当用户触摸屏幕时,将产生的触摸行为』
其实,我们需要处理的就是把一个MotionEvent对象处理掉,而能处理它的其实只有三个方法,dispatchTouchEvent(MotionEvent event)、onInterceptTouchEvent(MotionEvent event)、onTouchEvent(MotionEvent event),但是如果搭配上ViewGroup、View和Activity处理的流程可能就要变的复杂一点了,下面我们来具体分析。

来源与传递

首先我们一定要先了解,Activity和View是什么样的关系,为什么Activity中的事件都能传递给View去处理。
本质上Activity其实只是一个『容器』,但是这个容器并不是直接承载了View,而是通过Window,这里我们不深究,你只需要把Activity也看成是一个ViewGroup就好了。


Activity与View的层级关系

我们可以认为,MotionEvent就是从Activity来的(其实并不是,具体参看)而它调用的第一个方法就是dispatchTouchEvent,我们看源码它将这个事件交给了谁。

//1. Activity.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

随后,我们找到Window的实现类PhoneWindow,查看superDispatchTouchEvent方法。

//2. PhoneWindow.superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

很明显,MotionEvent进入到了DecorView

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

而DecorView继承于FrameLayout,但是FrameLayout中并没有这个方法,我们继续往上层找即ViewGroup中,这里事件的分发才真正开始。

传递开始

在这里我们要先明白一个概念,在一次点击事件中其实是有多MotionEvent的,MotionEvent也根据Action的不同来表达不同的动作,比如DOWN、MOVE、UP、CANCEL。
一次快速的点击包含一个DOWN和UP,而点击屏幕、拖动、抬起手指却是包含了一个DOWN、一个UP和若干个MOVE,再明白了这一系列动作之后,我们开始去传递事件了。

在ViewGroup的dispatchTouchEvent方法中这个方法的作用是去分发事件,分发——就是要找到真正需要处理事件的子View,根据我们上面说的,难道一系列的事件来了之后,我们要一个一个的去寻找处理它的子View吗?当然没有必要,我们只需要通过Down事件找到那个View,之后的后续事件都交给它处理就好了,如果没有子View去『吃掉』这个事件,再考虑自己处理或者交给上层。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        //清空上一次的DOWN事件遗留
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    // 检查是否需要拦截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 从字面意思上我们可以猜出,不允许拦截,如果为false,就去调用onInterceptTouchEvent()方法去拦截事件
        // 这个字段非常有用,子View可以通过requestDisallowInterceptTouchEvent()方法来控制父ViewGroup是否去拦截点击事件
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
        } 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;
    }
    if (!canceled && !intercepted) {
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            // 获取到每个child view
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);
            // 这两个方法也很关键,canViewReceivePointerEvents()表示:view是否可见或者是否正在执行动画
            // isTransformedTouchPointInView() 表示:此次滑动或者点击的范围是否在View的范围内
            // 也就是说,如果这个子View正在执行动画或者不可见,或者不在滑动范围内,是不能处理点击事件的。
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                continue;
            }
            // 这里其实就是调用了child.dispatchTouchEvent(event);
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // 为mFirstTouchTarget赋值,就是找到了处理事件的那个View
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                break;
            }
        }
    }

    // 把事件传递给目标View
    if (mFirstTouchTarget == null) {
        // 没有找到目标View,交给子View处理或者自己调用TouchEvent处理
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // 处理MOVE、UP等后续事件(因为上面都是针对DOWN事件的,也因为找到了mFirstTouchTarget,后续事件可以不参与上面的判断而直接来分发)
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
        }
    }
    return handled;
}

这里是ViewGroup中事件的分发方法,省去了很多不必要的行,大家可以对照注释去理解。

ViewGroup如果找到了处理的View,就会调用子View的dispatchTouchEvent()方法,这个方法要相对简单很多,但是对于各种listener的调用会对大家的今后的使用有一定帮助。

public boolean dispatchTouchEvent(MotionEvent event) {
    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        // 各种listener的合集(OnClickListener、OnLongClickListener、OnTouchListener等等)
        ListenerInfo li = mListenerInfo;
        // 如果我们设置了OnTouchListener监听,同时OnTouchListener的onTouch方法返回的是true,并且是可用状态,
        // 那么,这个View就不会调用自己的onTouchEvent方法了
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        // 如果上面不调用result = true,这里去调用onTouchEvent方法。注意:View里面没有onInterceptTouchEvent方法
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    return result;
}

于是,我们的事件传递到了TouchEvent中。
但是有一个问题,我们收到这个MotionEvent之后该做什么?回想我们在开发中用到的东西,最多的难道不是setOnClickListener或者setOnLongClickListener,再或者setOnTouchListener吗?而这里就是处理这些方法的中心。

public boolean onTouchEvent(MotionEvent event) {
    // 这里主要看viewFlags这个参数,如果设置了Clicklistener或者LongClickener都会把这个值置位相应的标志位,也就是说是可点击的。
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    // 这里注意,虽然有的组件设置为了不可用,但是也是会『吃掉』点击事件的,只不过没有回应。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        return clickable;
    }

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                // 把各种回调都移除,因为一次点击已经结束
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    break;
                }
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // View是否获取了焦点
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    // 这里其实判断OnClick事件的一个重要分支,mHasPerformedLongPress顾名思义就是是否触发的长按事件,但是如果
                    // OnLongClick方法返回false,依然是会触发OnClick事件
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // 这里其实就是触发OnClick方法的触发点,但是触发的时候不是直接调用,而是用post的方式,
                            // 这样可以在点击事件的反馈效果之前,让其他的视觉效果也能相继触发
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                }
                break;

            case MotionEvent.ACTION_DOWN:
                // 开启触发LongClick的触发点,如果是在可滚动的容器内,需要延迟100ms判断,如果不在开始触发,触发也是通过post执行,delay的时间是500ms
                if (isInScrollingContainer) {
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                // 取消各种回调
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    // 绘制各个热点区域,比如水波纹效果
                    drawableHotspotChanged(x, y);
                }
                // 是否已经移出了View中
                if (!pointInView(x, y, mTouchSlop)) {
                    // 移除tap和LongPress的触发回调
                    removeTapCallback();
                    removeLongPressCallback();
                }
                break;
        }
        return true;
    }
    return false;
}

由源码我们可以看出,View的TouchEvent方法就是一个大型的回调调用方法,里面判断各种条件,去把合适的回调方法调用,来满足我们开发的需求。比如我们可以明显的看出Onclick方法是在UP事件中触发的,LongClick可以制约Click的触发。

结论
  1. 一个事件序列从手指接触屏幕到手指离开屏幕,在这个过程中产生一系列的事件,以DOWN事件为开始,包含若干个MOVE,以UP事件为结尾。
  2. 正常情况下,一个事件序列只能被一个View拦截并且消耗。
  3. 某个View一旦决定拦截,那么这个事件序列都将由它的onTouchEvent处理,并且它的onInterceptTouchEvent调用。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中其他事件都不会再交给它处理。并且重新交由它的父元素处理。
  5. 事件传递的过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过reqeustDisallowInterceptTouchEvent方法可以在子View中干预父元素的事件分发过程,但ACTION_DOWN事件除外。
  6. ViewGroup默认不拦截任何事件,onInterceptTouchEvent默认返回false。View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
  7. View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable默认都为false,clickable要分情况,不如Button的clickable默认为true,TextView的clickable默认为false。
  8. View的enable属性不影响onTouchEvent的返回值。哪怕一个View为disable状态,只要它的clickable或者LongClickable有一个为true,那么它的onTouchEvent就返回true。
  9. onClick事件会响应的前提是当前View是可点击的,并且收到了ACTION_DOWN的ACTION_UP的事件,并且受长按事件的影响,当长按事件返回true时,onClick不会响应。
  10. onLongClick在ACTION_DOWN里判断是否进行响应,要想执行长按事件该View必须是longClickable的并且设置了OnLongClickListener。
总结

事件的分发其实并不复杂,但是这个设计思路以及细节的设计特别值得我们推敲,我一直在想,如果让我们自己设计一个事件分发模型,我们会怎么做呢?


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

推荐阅读更多精彩内容