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

来源:互联网 发布:淘宝主图点击率怎么看 编辑:程序博客网 时间:2024/06/07 02:25

转载自:小筐子  原文链接地址:http://www.jianshu.com/p/e6ceb7f767d8

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

前言

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

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


概念

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

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

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

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

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

  • 点击事件用代码如何表示?
    在源码中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;@Overridepublic 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开发艺术探索》,各大网站都有卖,对于突破瓶颈有很大的意义。

结后谈

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


原创粉丝点击