Android事件分发机制——从基础深入源码解析

前言

前段时间找工作,看了好多关于事件分发机制的书,各路大牛从不同的角度进行了分析。本人受益匪浅,于是有了这篇吸取天地之精华的解析。

本文章会从什么是事件分发机制开始,一直深入到源码分析
主要目的是让自己理解更深入,也希望能让读者更容易读懂而不觉干涩。

概念

本节都是基础,我化身十万个为什么提出以下几个问题!如果读者都明了那就直接跳向下一节!

  • 事件分发机制是什么?
    事件分发机制就是点击事件的分发

  • 那么点击事件又是什么?
    在手指接触屏幕后产生的同一个事件序列都是点击事件。

  • 点击事件分为哪几种类型?

    • 手指刚接触屏幕
    • 手指在屏幕上滑动
    • 手指从屏幕上松开的一瞬间
  • 同一个事件序列是什么?
    是从手指接触屏幕的一瞬间起,直到手指从屏幕上松开的一瞬间所产生的一切事件。

  • 点击事件用代码如何表示?
    在源码中MotionEvent就是点击事件,对点击事件的分发就是对MotionEvent对象的分发传递过程

  • MotionEvent的点击事件类型?

    • ACTION_DOWN:手指刚接触屏幕
    • ACTION_MOVE:手指在屏幕上滑动
    • ACTION_UP:手指从屏幕上松开的一瞬间
  • 那这个MotionEvent到底是如何传递的?
    那就来看下一节!

事件分发机制

所谓事件分发机制,其实就是对MotionEvent(点击事件)的分发过程。
当一个MotionEvent(点击事件)产生之后,系统需要把它传递给一个具体的View,这个传递过程就是事件分发机制。

1. 我们来简单描述一次点击事件(不涉及方法调用,先有个大概的体系)

  • 用户接触屏幕产生MotionEvent(点击事件)
  • MotionEvent(点击事件)总是由Activity先接收
  • Activity接收后将MotionEvent(点击事件)进行传递:Activity->Window->DecorView(DecorView是当前界面的底层容器,就是setContentView所设置View的父容器)
  • DecorView是一个ViewGroup,将MotionEvent(点击事件)分发向各个子View

2. 三个方法
相信大家对点击事件已经有所了解,那接下来我们介绍事件分发机制很重要的三个方法,点击事件的分发机制都是根据这三个方法共同完成的:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。

  • dispatchTouchEvent()用来进行事件的分发,如果MotionEvent(点击事件)能够传递给该View,那么该方法一定会被调用。返回值由 本身的onTouchEvent() 和 子View的dispatchTouchEvent()的返回值 共同决定。

    • 返回值为true,则表示该点击事件被本身或者子View消耗。
    • 返回值为false,则表示该ViewGroup没有子元素,或者子元素没有消耗该事件。
  • onInterceptTouchEvent():在dispatchTouchEvent()中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中不会再访问该方法。

  • onTouchEvent():在dispatchTouchEvent()中调用,返回结果表示是否消耗当前事件,如果不消耗(返回false),则在同一个事件序列中View不会再次接收到事件。

3. 三个方法的关系
这么多概念,别头疼!咱们用伪代码看一下三个方法的关系!

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

这段伪代码可以很好地理解事件的传递机制:
用户点击屏幕产生MotionEvent(点击事件),View的dispatchTouchEvent()接收MotionEvent(点击事件)后,先执行该View的onInterceptTouchEvent()判断是否拦截该事件,若拦截执行该View的onTouchEvent()方法,若不拦截则调用子View的dispatchTouchEvent()。在事件传递的源码中,使用的就是类似的逻辑。

4. 事件传递顺序

  • 用户点击屏幕产生MotionEvent(点击事件)
  • Activity接收MotionEvent(点击事件)—>传递给Window—>传递给DecorView(ViewGroup)—>执行ViewGroup的dispatchTouchEvent()
  • ViewGroup接收到MotionEvent(点击事件)之后,按照事件分发机制去分发事件。
  • 若当子View不消耗事件,onTouchEvent()返回false,那么这个事件会传递回其父View的onTouchEvent(),如若父View也不消耗,最后会传递回给Activity进行处理。

总的来说点击事件的传递顺序是由父到子,再由子到父的。

图解事件传递机制

现在网上的大部分文章都是通过源码和log讲解事件的传递,对看文章的人来说体验并没有那么好,看的云里雾里摸不出个头。在这献上一本葵花宝典!看了这张图妈妈再也不用担心我的学习啦!

事件传递机制图解

友情提示:

  1. 还是不理解的同学可以对照上一部分一起看效果更佳。
  2. 图中View的onTouchEvent返回false,将事件传递给ViewGroup的过程,并不是直接传递。是上级ViewGroup的dispatchTouchEvent()方法接收到子View的onTouchEvent()返回的false,再将事件分发给自己(ViewGroup)的onTouchEvent。
  3. ViewGroup里面没有复写onTouchEvent,然而ViewGroup本身就是View,View中有onToucheEvent。

源码解析

看了这么久咱们终于来看源码啦!不多废话!一库!

右手左手来几行源码

1. Activity对点击事件的分发
先来看Activity的dispatchTouEvent,所有点击事件接收的源头

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

这段代码中我们着重看getWindow().superDispatchTouchEvent(ev),方法将点击事件传递给了Window。返回值表示是否消耗掉了该点击事件。如果所有的View都没有消耗掉点击事件,则Activity调用自己的onTouchEvent。

再来看Window的源码:

public abstract boolean superDispatchTouchEvent(MotionEvent event);

发现其实是一个接口,那实现方法在哪?不急,不难找,源码的最上方注释里写道

The only existing implementation of this abstract class is android.view.PhoneWindow,

该接口的唯一实现方法是PhoneWindow,那咱们再去看PhoneWindow的源码:

private DecorView mDecor;

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

是不是很熟!其实他也把这个锅直接甩给了DecorView ,之前介绍过,DecorView是当前界面的底层容器,就是setContentView所设置View的父容器。所以再来看DecorView:

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

码个蛋!竟然又传递出去了,这次是调用了super,而DecorView是继承自ViewGroup,所以调用了ViewGroup的dispatchTouchEvent!那这样咱们就先来瞧一瞧ViewGroup里的源码!

2.ViewGroup对事件的分发
先来看ViewGroup中的dispatchTouchEvent中的一小段

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;
}

咱们从头开始看,MotionEvent.ACTION_DOWN 这个之前介绍过,那mFirstTouchTarget 是什么?后面的代码表示,当ViewGroup的点击事件被子View消耗,那mFirstTouchTarget就会指向该子View。所以如果事件被子View消耗 或者 是ACTION_DOWN事件,那就访问该ViewGroup的onInterceptTouchEvent,如果不那就全部被当前ViewGroup拦截。换句话说,如果View决定拦截事件,那么这一个事件序列都会由这个View来处理。

那么大家也注意到FLAG_DISALLOW_INTERCEPT这个标志位,看起来它可以影响ViewGroup是否拦截该事件。这个标志位是通过requestDisallowInterceptTouchEvent()方法来设置的,一般用于子View中。当标志位设置之后ViewGroup将无法拦截除了ACTION_DOWN以外的事件了。为啥说除了ACTION_DOWN以外呢?因为dispatchTouchEvent每次接收到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();
}

综上所述,requestDisallowInterceptTouchEvent()方法不能影响ACTION_DOWN事件
总结一点,onInterceptTouchEvent()方法不一定会每次都执行,如果想对每个事件都进行处理,那还是在dispatchTouchEvent()里面处理吧

咱们继续往下走,当该ViewGroup不拦截点击事件的时候,事件会传递给他的子View:

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    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 (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) {
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            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;
        }
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}

以上这段是ViewGroup进行事件分发的主要代码,看起来比较简单。当ViewGroup有子View的时候,进行子View的遍历,其中有一个判断条件:

canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null) 

判断当前点击事件是否在子View的坐标范围内,且子View没有在坐标系中移动(执行动画),如果子View符合以上两个情况那么就把点击事件传递给他处理。往下走,会看到这么一个判断条件:

dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)

这个方法其实就是用来将事件分发给子View的,来看一下这个方法的其中一段源码你就会清晰很多:

final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

如果子View为null那就交给该ViewGroup的dispatchTouchEvent(),反之就将点击事件交给该子View(也有可能是ViewGroup)处理,一次分发就完成了。

再跳回到之前那段超长代码,如果dispatchTransformedTouchEvent()返回true,表明点击事件被子View消耗,执行addTouchTarget()方法给最开始的mFirstTouchTarget赋值

如果遍历完了所有的子View,点击事件都没有被消耗掉,可能有两种情况:一、ViewGroup下面没有子View。二、子View没有消耗点击事件。这两种情况下,ViewGroup会自己处理点击事件。当子View不消耗点击事件,那点击事件将交由给他的父View去处理。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

代码里面child参数赋值为null,当child为null时,访问当前ViewGroup的super.dispatchTouchEvent(event),因为ViewGroup是继承自View,所以其实访问的就是View的dispatchTouchEvent()方法。

3.View对事件的分发
再来看看View的dispatchTouchEvent()方法的其中一段代码,注意知识其中一段,篇幅不能太长,想要全部查看一定打开Studio看看源码!

public boolean dispatchTouchEvent(MotionEvent event) {
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        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的dispatchTouchEvent()就比较简单了,onFilterTouchEventForSecurity(event)是用来判断点击事件来到时,窗口有没有被遮挡住,如果被遮挡住则直接返回false,不消耗事件。
反之,接收到事件后看到一个类ListenerInfo,那这是个啥?看源码啊!

static class ListenerInfo {
    public OnClickListener mOnClickListener;

    protected OnLongClickListener mOnLongClickListener;

    private OnKeyListener mOnKeyListener;

    private OnTouchListener mOnTouchListener;
    
    ......
}

看完源码发现它是一个View的静态内部类,定义了一系列的Listener。
继续看View的dispatchTouchEvent()的源码发现,View会先判断自己是否有设置OnTouchListener,如果所设置的OnTouchListener得onTouch返回true,则直接消耗点击事件,不再执行onTouchEvent()方法。

得出一个结论,OnTouchListener的优先级高于onTouchEvent()。这样做的好处是方便在外部处理事件。

如果没有设置OnTouchListener那就会执行到View的onTouchEvent(),继续看下onTouchEvent()的源码,咱们一段一段来,有点长:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    return (((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

当我们把View设置为不可用状态,View依然会消耗点击事件,只是看起来不可用。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

之后如果View设置有代理,那么就会直接执行代理的onTouchEvent()。下面再来看一下点击事件的主要代码:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (prepressed) {
                    setPressed(true, x, y);
               }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    removeLongPressCallback();
                    if (!focusTaken) {
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    mUnsetPressedState.run();
                }

                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;

        case MotionEvent.ACTION_DOWN:
            ......
            break;

        case MotionEvent.ACTION_CANCEL:
            ......
            break;

        case MotionEvent.ACTION_MOVE:
            ......
            break;
    }

    return true;
}

当View的CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE有其中一个为true那么View就会消耗掉这个事件。并且在ACTION_UP的时候会执行performClick()方法:

public boolean 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;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

如果View设置了OnClickListener,performClick()这个方法就会去执行这个监听事件。
再来一个结论,OnTouchListener的优先级高于OnClickListener,OnClickListener是在ACTION_UP的时候执行的。

看到这里事件传递机制的源码分析终于结束了!!!

结论

  • 事件分发机制就是点击事件的分发,在手指接触屏幕后产生的同一个事件序列都是点击事件。
  • 点击事件的传递顺序是由父到子,再由子到父的。
  • 正常情况下事件只能被一个View拦截。
  • 如果View决定拦截事件,那么这一个事件序列都会由这个View来处理。
  • 当子View不消耗点击事件,那点击事件将交由给他的父View去处理,如果所有的View都没有消耗掉点击事件,则Activity调用自己的onTouchEvent。
  • onInterceptTouchEvent()方法不一定会每次都执行,如果想对每个事件都进行处理,那还是在dispatchTouchEvent()里面处理吧。
  • OnTouchListener的优先级高于onTouchEvent()。这样做的好处是方便在外部处理事件。
  • 当我们把View设置为不可用状态,View依然会消耗点击事件,只是看起来不可用。

最后给大家推荐一篇View源码分析的文章,里面有Log日志分析。大家可以看一看增深理解。《Android View 事件分发机制源码详解(View篇)》

最最后再给大家推荐一本书《Android开发艺术探索》,各大网站都有卖,对于突破瓶颈有很大的意义。

结后谈

博主花了一段时间终于理顺完了这篇文章,当然由于博主的技术原因,文章并不是十全十美的,只希望给还处在迷茫期的朋友们指引一条方向。
希望我的文章能给大家带来一点点的福利,那在下就足够开心了。
下次再见!

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

推荐阅读更多精彩内容